diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..1c1266938 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,98 @@ +[*.cs] +indent_style = space +tab_width = 4 + +# StyleCop.Analyzer + +# SA0001: XML comment analysis is disabled due to project configuration +dotnet_diagnostic.SA0001.severity = none + +# SA1009: Closing parenthesis should not be preceded by a space +dotnet_diagnostic.SA1009.severity = none + +# SA1100: Do not prefix calls with base unless local implementation exists +dotnet_diagnostic.SA1100.severity = none + +# SA1108: Block statements should not contain embedded comments +dotnet_diagnostic.SA1108.severity = none + +# SA1111: Closing parenthesis should be on line of last parameter +dotnet_diagnostic.SA1111.severity = none + +# SA1114: Parameter list should follow declaration +dotnet_diagnostic.SA1114.severity = none + +# SA1115: The parameter should begin on the line after the previous parameter +dotnet_diagnostic.SA1115.severity = none + +# SA1116: The parameters should begin on the line after the declaration, whenever the parameter span across multiple lines +dotnet_diagnostic.SA1116.severity = none + +# SA1122:Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + +# SA1124: Do not use regions +dotnet_diagnostic.SA1124.severity = none + +# SA1129: Do not use default value type constructor +dotnet_diagnostic.SA1129.severity = none + +# SA1201: Elements must appear in the correct order +dotnet_diagnostic.SA1201.severity = none + +# SA1202: Elements must be ordered by access +dotnet_diagnostic.SA1202.severity = none + +# SA1203: Constants must appear before fields +dotnet_diagnostic.SA1203.severity = none + +# SA1204: Static members should appear before non-static members +dotnet_diagnostic.SA1204.severity = none + +# SA1214: Readonly fields should appear before non-readonly fields +dotnet_diagnostic.SA1214.severity = none + +# SA1401: Field should be private +dotnet_diagnostic.SA1401.severity = none + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# SA1404: Code analysis suppression should have justification +dotnet_diagnostic.SA1404.severity = none + +# SA1407: Arithmetic expressions should declare precedence +dotnet_diagnostic.SA1407.severity = none + +# SA1408: Conditional expressions should declare precedence +dotnet_diagnostic.SA1408.severity = none + +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none + +# SA1512: Single-line comments should not be followed by blank line +dotnet_diagnostic.SA1512.severity = none + +# SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = none + +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = none + +# SA1623: PropertySummaryDocumentation must match accessors +dotnet_diagnostic.SA1623.severity = none + +# SA1627: The documentation text within the 'exception' tag should not be empty +dotnet_diagnostic.SA1627.severity = none + +# SA1629: Documentation text should end with a period +dotnet_diagnostic.SA1629.severity = none + +# SA1633: The file header XML is invalid +dotnet_diagnostic.SA1633.severity = none + +# conflict with StyleCop rules +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_property = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_event = true diff --git a/.gitignore b/.gitignore index 3222133e3..8b53f4f24 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /*/bin /*/obj /.vs +/__Instrumented +/coverage.xml Thumbs.db -TestResult.xml *.VisualState.xml diff --git a/OpenTween.Tests/AnyOrderComparer.cs b/OpenTween.Tests/AnyOrderComparer.cs index 1aa5f8b50..c4cb10edf 100644 --- a/OpenTween.Tests/AnyOrderComparer.cs +++ b/OpenTween.Tests/AnyOrderComparer.cs @@ -32,7 +32,7 @@ namespace OpenTween internal class AnyOrderComparer : IEqualityComparer> where T : IEquatable { - public static readonly AnyOrderComparer Instance = new AnyOrderComparer(); + public static readonly AnyOrderComparer Instance = new(); public bool Equals(IEnumerable x, IEnumerable y) { diff --git a/OpenTween.Tests/Api/ApiLimitTest.cs b/OpenTween.Tests/Api/ApiLimitTest.cs index e2e53d1d6..26afe5909 100644 --- a/OpenTween.Tests/Api/ApiLimitTest.cs +++ b/OpenTween.Tests/Api/ApiLimitTest.cs @@ -30,7 +30,7 @@ namespace OpenTween.Api { public class ApiLimitTest { - public static readonly TheoryData Equals_TestCase = new TheoryData + public static readonly TheoryData EqualsTestCase = new() { { new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), true }, { new ApiLimit(350, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), false }, @@ -41,7 +41,7 @@ public class ApiLimitTest }; [Theory] - [MemberData(nameof(Equals_TestCase))] + [MemberData(nameof(EqualsTestCase))] public void EqualsTest(object? obj2, bool expected) { var obj1 = new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)); diff --git a/OpenTween.Tests/Api/DataModel/TwitterMessageEventTest.cs b/OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs similarity index 100% rename from OpenTween.Tests/Api/DataModel/TwitterMessageEventTest.cs rename to OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs diff --git a/OpenTween.Tests/Api/MobypictureApiTest.cs b/OpenTween.Tests/Api/MobypictureApiTest.cs index cbde60a5b..42f45a490 100644 --- a/OpenTween.Tests/Api/MobypictureApiTest.cs +++ b/OpenTween.Tests/Api/MobypictureApiTest.cs @@ -30,7 +30,7 @@ namespace OpenTween.Api { - public class MobypictureApiText + public class MobypictureApiTest { [Fact] public async Task UploadFileAsync_Test() diff --git a/OpenTween.Tests/Api/TwitterApiStatusTest.cs b/OpenTween.Tests/Api/TwitterApiStatusTest.cs index 0c8712691..187c01568 100644 --- a/OpenTween.Tests/Api/TwitterApiStatusTest.cs +++ b/OpenTween.Tests/Api/TwitterApiStatusTest.cs @@ -49,10 +49,11 @@ public void ResetTest() Assert.Equal(TwitterApiAccessLevel.Anonymous, apiStatus.AccessLevel); } - public static readonly TheoryData, ApiLimit?> ParseRateLimit_TestCase = new TheoryData, ApiLimit?> + public static readonly TheoryData, ApiLimit?> ParseRateLimitTestCase = new() { { - new Dictionary { + new Dictionary + { ["X-RateLimit-Limit"] = "150", ["X-RateLimit-Remaining"] = "100", ["X-RateLimit-Reset"] = "1356998400", @@ -60,7 +61,8 @@ public void ResetTest() new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)) }, { - new Dictionary(StringComparer.OrdinalIgnoreCase) { + new Dictionary(StringComparer.OrdinalIgnoreCase) + { ["x-ratelimit-limit"] = "150", ["x-ratelimit-remaining"] = "100", ["x-ratelimit-reset"] = "1356998400", @@ -68,7 +70,8 @@ public void ResetTest() new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)) }, { - new Dictionary(StringComparer.OrdinalIgnoreCase) { + new Dictionary(StringComparer.OrdinalIgnoreCase) + { ["X-RateLimit-Limit"] = "150", ["X-RateLimit-Remaining"] = "100", ["X-RateLimit-Reset"] = "hogehoge", @@ -76,7 +79,8 @@ public void ResetTest() null }, { - new Dictionary { + new Dictionary + { ["X-RateLimit-Limit"] = "150", ["X-RateLimit-Remaining"] = "100", }, @@ -85,17 +89,18 @@ public void ResetTest() }; [Theory] - [MemberData(nameof(ParseRateLimit_TestCase))] + [MemberData(nameof(ParseRateLimitTestCase))] public void ParseRateLimitTest(IDictionary header, ApiLimit? expected) { var limit = TwitterApiStatus.ParseRateLimit(header, "X-RateLimit-"); Assert.Equal(expected, limit); } - public static readonly TheoryData, ApiLimit?> ParseMediaRateLimit_TestCase = new TheoryData, ApiLimit?> + public static readonly TheoryData, ApiLimit?> ParseMediaRateLimitTestCase = new() { { - new Dictionary { + new Dictionary + { ["X-MediaRateLimit-Limit"] = "30", ["X-MediaRateLimit-Remaining"] = "20", ["X-MediaRateLimit-Reset"] = "1234567890", @@ -103,7 +108,8 @@ public void ParseRateLimitTest(IDictionary header, ApiLimit? exp new ApiLimit(30, 20, new DateTimeUtc(2009, 2, 13, 23, 31, 30)) }, { - new Dictionary { + new Dictionary + { ["X-MediaRateLimit-Limit"] = "30", ["X-MediaRateLimit-Remaining"] = "20", ["X-MediaRateLimit-Reset"] = "hogehoge", @@ -111,7 +117,8 @@ public void ParseRateLimitTest(IDictionary header, ApiLimit? exp null }, { - new Dictionary { + new Dictionary + { ["X-MediaRateLimit-Limit"] = "30", ["X-MediaRateLimit-Remaining"] = "20", }, @@ -120,29 +127,29 @@ public void ParseRateLimitTest(IDictionary header, ApiLimit? exp }; [Theory] - [MemberData(nameof(ParseMediaRateLimit_TestCase))] + [MemberData(nameof(ParseMediaRateLimitTestCase))] public void ParseMediaRateLimitTest(IDictionary header, ApiLimit? expected) { var limit = TwitterApiStatus.ParseRateLimit(header, "X-MediaRateLimit-"); Assert.Equal(expected, limit); } - public static readonly TheoryData, TwitterApiAccessLevel?> ParseAccessLevel_TestCase = new TheoryData, TwitterApiAccessLevel?> + public static readonly TheoryData, TwitterApiAccessLevel?> ParseAccessLevelTestCase = new() { { - new Dictionary { {"X-Access-Level", "read"} }, + new Dictionary { { "X-Access-Level", "read" } }, TwitterApiAccessLevel.Read }, { - new Dictionary { {"X-Access-Level", "read-write"} }, + new Dictionary { { "X-Access-Level", "read-write" } }, TwitterApiAccessLevel.ReadWrite }, { - new Dictionary { {"X-Access-Level", "read-write-directmessages"} }, + new Dictionary { { "X-Access-Level", "read-write-directmessages" } }, TwitterApiAccessLevel.ReadWriteAndDirectMessage }, { - new Dictionary { {"X-Access-Level", ""} }, // 何故かたまに出てくるやつ + new Dictionary { { "X-Access-Level", "" } }, // 何故かたまに出てくるやつ null }, { @@ -152,7 +159,7 @@ public void ParseMediaRateLimitTest(IDictionary header, ApiLimit }; [Theory] - [MemberData(nameof(ParseAccessLevel_TestCase))] + [MemberData(nameof(ParseAccessLevelTestCase))] public void ParseAccessLevelTest(IDictionary header, TwitterApiAccessLevel? expected) { var accessLevel = TwitterApiStatus.ParseAccessLevel(header, "X-Access-Level"); diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index da1ebc3ba..85ea02e00 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -52,13 +52,13 @@ private void MyCommonSetup() public void Initialize_Test() { using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - Assert.Null(twitterApi.apiConnection); + Assert.Null(twitterApi.ApiConnection); twitterApi.Initialize("*** AccessToken ***", "*** AccessSecret ***", userId: 100L, screenName: "hogehoge"); - Assert.IsType(twitterApi.apiConnection); + Assert.IsType(twitterApi.ApiConnection); - var apiConnection = (TwitterApiConnection)twitterApi.apiConnection!; + var apiConnection = (TwitterApiConnection)twitterApi.ApiConnection!; Assert.Equal("*** AccessToken ***", apiConnection.AccessToken); Assert.Equal("*** AccessSecret ***", apiConnection.AccessSecret); @@ -71,9 +71,9 @@ public void Initialize_Test() var oldApiConnection = apiConnection; Assert.True(oldApiConnection.IsDisposed); - Assert.IsType(twitterApi.apiConnection); + Assert.IsType(twitterApi.ApiConnection); - apiConnection = (TwitterApiConnection)twitterApi.apiConnection!; + apiConnection = (TwitterApiConnection)twitterApi.ApiConnection!; Assert.Equal("*** AccessToken2 ***", apiConnection.AccessToken); Assert.Equal("*** AccessSecret2 ***", apiConnection.AccessSecret); @@ -88,7 +88,8 @@ public async Task StatusesHomeTimeline_Test() mock.Setup(x => x.GetAsync( new Uri("statuses/home_timeline.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -101,7 +102,7 @@ public async Task StatusesHomeTimeline_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesHomeTimeline(200, maxId: 900L, sinceId: 100L) .ConfigureAwait(false); @@ -116,7 +117,8 @@ public async Task StatusesMentionsTimeline_Test() mock.Setup(x => x.GetAsync( new Uri("statuses/mentions_timeline.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -129,7 +131,7 @@ public async Task StatusesMentionsTimeline_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesMentionsTimeline(200, maxId: 900L, sinceId: 100L) .ConfigureAwait(false); @@ -144,7 +146,8 @@ public async Task StatusesUserTimeline_Test() mock.Setup(x => x.GetAsync( new Uri("statuses/user_timeline.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "include_rts", "true" }, { "include_entities", "true" }, @@ -159,7 +162,7 @@ public async Task StatusesUserTimeline_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesUserTimeline("twitterapi", count: 200, maxId: 900L, sinceId: 100L) .ConfigureAwait(false); @@ -174,7 +177,8 @@ public async Task StatusesShow_Test() mock.Setup(x => x.GetAsync( new Uri("statuses/show.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "id", "100" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -185,7 +189,7 @@ public async Task StatusesShow_Test() .ReturnsAsync(new TwitterStatus { Id = 100L }); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesShow(statusId: 100L) .ConfigureAwait(false); @@ -193,6 +197,34 @@ await twitterApi.StatusesShow(statusId: 100L) mock.VerifyAll(); } + [Fact] + public async Task StatusesLookup_Test() + { + var mock = new Mock(); + mock.Setup(x => + x.GetAsync( + new Uri("statuses/lookup.json", UriKind.Relative), + new Dictionary + { + { "id", "100,200" }, + { "include_entities", "true" }, + { "include_ext_alt_text", "true" }, + { "tweet_mode", "extended" }, + }, + "/statuses/lookup" + ) + ) + .ReturnsAsync(Array.Empty()); + + using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + twitterApi.ApiConnection = mock.Object; + + await twitterApi.StatusesLookup(statusIds: new[] { "100", "200" }) + .ConfigureAwait(false); + + mock.VerifyAll(); + } + [Fact] public async Task StatusesUpdate_Test() { @@ -200,7 +232,8 @@ public async Task StatusesUpdate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("statuses/update.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "status", "hogehoge" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -215,11 +248,16 @@ public async Task StatusesUpdate_Test() .ReturnsAsync(LazyJson.Create(new TwitterStatus())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; - - await twitterApi.StatusesUpdate("hogehoge", replyToId: 100L, mediaIds: new[] { 10L, 20L }, - autoPopulateReplyMetadata: true, excludeReplyUserIds: new[] { 100L, 200L }, - attachmentUrl: "https://twitter.com/twitterapi/status/22634515958") + twitterApi.ApiConnection = mock.Object; + + await twitterApi.StatusesUpdate( + "hogehoge", + replyToId: 100L, + mediaIds: new[] { 10L, 20L }, + autoPopulateReplyMetadata: true, + excludeReplyUserIds: new[] { 100L, 200L }, + attachmentUrl: "https://twitter.com/twitterapi/status/22634515958" + ) .IgnoreResponse() .ConfigureAwait(false); @@ -233,18 +271,19 @@ public async Task StatusesUpdate_ExcludeReplyUserIdsEmptyTest() mock.Setup(x => x.PostLazyAsync( new Uri("statuses/update.json", UriKind.Relative), - new Dictionary { - { "status", "hogehoge" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, + new Dictionary + { + { "status", "hogehoge" }, + { "include_entities", "true" }, + { "include_ext_alt_text", "true" }, + { "tweet_mode", "extended" }, // exclude_reply_user_ids は空の場合には送信されない }) ) .ReturnsAsync(LazyJson.Create(new TwitterStatus())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesUpdate("hogehoge", replyToId: null, mediaIds: null, excludeReplyUserIds: Array.Empty()) .IgnoreResponse() @@ -265,7 +304,7 @@ public async Task StatusesDestroy_Test() .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesDestroy(statusId: 100L) .IgnoreResponse() @@ -281,7 +320,8 @@ public async Task StatusesRetweet_Test() mock.Setup(x => x.PostLazyAsync( new Uri("statuses/retweet.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "id", "100" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -291,7 +331,7 @@ public async Task StatusesRetweet_Test() .ReturnsAsync(LazyJson.Create(new TwitterStatus())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesRetweet(100L) .IgnoreResponse() @@ -307,7 +347,8 @@ public async Task SearchTweets_Test() mock.Setup(x => x.GetAsync( new Uri("search/tweets.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "q", "from:twitterapi" }, { "result_type", "recent" }, { "include_entities", "true" }, @@ -323,7 +364,7 @@ public async Task SearchTweets_Test() .ReturnsAsync(new TwitterSearchResult()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.SearchTweets("from:twitterapi", "en", count: 200, maxId: 900L, sinceId: 100L) .ConfigureAwait(false); @@ -338,7 +379,8 @@ public async Task ListsOwnerships_Test() mock.Setup(x => x.GetAsync( new Uri("lists/ownerships.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "cursor", "-1" }, { "count", "100" }, @@ -348,7 +390,7 @@ public async Task ListsOwnerships_Test() .ReturnsAsync(new TwitterLists()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsOwnerships("twitterapi", cursor: -1L, count: 100) .ConfigureAwait(false); @@ -363,7 +405,8 @@ public async Task ListsSubscriptions_Test() mock.Setup(x => x.GetAsync( new Uri("lists/subscriptions.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "cursor", "-1" }, { "count", "100" }, @@ -373,7 +416,7 @@ public async Task ListsSubscriptions_Test() .ReturnsAsync(new TwitterLists()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsSubscriptions("twitterapi", cursor: -1L, count: 100) .ConfigureAwait(false); @@ -388,7 +431,8 @@ public async Task ListsMemberships_Test() mock.Setup(x => x.GetAsync( new Uri("lists/memberships.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "cursor", "-1" }, { "count", "100" }, @@ -399,7 +443,7 @@ public async Task ListsMemberships_Test() .ReturnsAsync(new TwitterLists()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMemberships("twitterapi", cursor: -1L, count: 100, filterToOwnedLists: true) .ConfigureAwait(false); @@ -414,7 +458,8 @@ public async Task ListsCreate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("lists/create.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "name", "hogehoge" }, { "description", "aaaa" }, { "mode", "private" }, @@ -423,7 +468,7 @@ public async Task ListsCreate_Test() .ReturnsAsync(LazyJson.Create(new TwitterList())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsCreate("hogehoge", description: "aaaa", @private: true) .IgnoreResponse() @@ -439,7 +484,8 @@ public async Task ListsUpdate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("lists/update.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "name", "hogehoge" }, { "description", "aaaa" }, @@ -449,7 +495,7 @@ public async Task ListsUpdate_Test() .ReturnsAsync(LazyJson.Create(new TwitterList())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsUpdate(12345L, name: "hogehoge", description: "aaaa", @private: true) .IgnoreResponse() @@ -465,14 +511,15 @@ public async Task ListsDestroy_Test() mock.Setup(x => x.PostLazyAsync( new Uri("lists/destroy.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, }) ) .ReturnsAsync(LazyJson.Create(new TwitterList())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsDestroy(12345L) .IgnoreResponse() @@ -488,7 +535,8 @@ public async Task ListsStatuses_Test() mock.Setup(x => x.GetAsync( new Uri("lists/statuses.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -503,7 +551,7 @@ public async Task ListsStatuses_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsStatuses(12345L, count: 200, maxId: 900L, sinceId: 100L, includeRTs: true) .ConfigureAwait(false); @@ -518,7 +566,8 @@ public async Task ListsMembers_Test() mock.Setup(x => x.GetAsync( new Uri("lists/members.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -530,7 +579,7 @@ public async Task ListsMembers_Test() .ReturnsAsync(new TwitterUsers()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembers(12345L, cursor: -1) .ConfigureAwait(false); @@ -545,7 +594,8 @@ public async Task ListsMembersShow_Test() mock.Setup(x => x.GetAsync( new Uri("lists/members/show.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "screen_name", "twitterapi" }, { "include_entities", "true" }, @@ -557,7 +607,7 @@ public async Task ListsMembersShow_Test() .ReturnsAsync(new TwitterUser()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembersShow(12345L, "twitterapi") .ConfigureAwait(false); @@ -572,7 +622,8 @@ public async Task ListsMembersCreate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("lists/members/create.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "screen_name", "twitterapi" }, { "include_entities", "true" }, @@ -583,7 +634,7 @@ public async Task ListsMembersCreate_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembersCreate(12345L, "twitterapi") .IgnoreResponse() @@ -599,7 +650,8 @@ public async Task ListsMembersDestroy_Test() mock.Setup(x => x.PostLazyAsync( new Uri("lists/members/destroy.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "list_id", "12345" }, { "screen_name", "twitterapi" }, { "include_entities", "true" }, @@ -610,7 +662,7 @@ public async Task ListsMembersDestroy_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembersDestroy(12345L, "twitterapi") .IgnoreResponse() @@ -626,7 +678,8 @@ public async Task DirectMessagesEventsList_Test() mock.Setup(x => x.GetAsync( new Uri("direct_messages/events/list.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "count", "50" }, { "cursor", "12345abcdefg" }, }, @@ -635,7 +688,7 @@ public async Task DirectMessagesEventsList_Test() .ReturnsAsync(new TwitterMessageEventList()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg") .ConfigureAwait(false); @@ -647,10 +700,7 @@ await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg") public async Task DirectMessagesEventsNew_Test() { var mock = new Mock(); - mock.Setup(x => - x.PostJsonAsync( - new Uri("direct_messages/events/new.json", UriKind.Relative), - @"{ + var responseText = @"{ ""event"": { ""type"": ""message_create"", ""message_create"": { @@ -668,12 +718,16 @@ public async Task DirectMessagesEventsNew_Test() } } } -}") +}"; + mock.Setup(x => + x.PostJsonAsync( + new Uri("direct_messages/events/new.json", UriKind.Relative), + responseText) ) .ReturnsAsync(LazyJson.Create(new TwitterMessageEventSingle())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.DirectMessagesEventsNew(recipientId: 12345L, text: "hogehoge", mediaId: 67890L) .ConfigureAwait(false); @@ -692,7 +746,7 @@ public async Task DirectMessagesEventsDestroy_Test() .Returns(Task.CompletedTask); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.DirectMessagesEventsDestroy(eventId: "100") .ConfigureAwait(false); @@ -707,7 +761,8 @@ public async Task UsersShow_Test() mock.Setup(x => x.GetAsync( new Uri("users/show.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -718,7 +773,7 @@ public async Task UsersShow_Test() .ReturnsAsync(new TwitterUser { ScreenName = "twitterapi" }); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.UsersShow(screenName: "twitterapi") .ConfigureAwait(false); @@ -733,7 +788,8 @@ public async Task UsersLookup_Test() mock.Setup(x => x.GetAsync( new Uri("users/lookup.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "user_id", "11111,22222" }, { "include_entities", "true" }, { "include_ext_alt_text", "true" }, @@ -744,7 +800,7 @@ public async Task UsersLookup_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.UsersLookup(userIds: new[] { "11111", "22222" }) .ConfigureAwait(false); @@ -759,7 +815,8 @@ public async Task UsersReportSpam_Test() mock.Setup(x => x.PostLazyAsync( new Uri("users/report_spam.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "tweet_mode", "extended" }, }) @@ -767,7 +824,7 @@ public async Task UsersReportSpam_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser { ScreenName = "twitterapi" })); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.UsersReportSpam(screenName: "twitterapi") .IgnoreResponse() @@ -783,7 +840,8 @@ public async Task FavoritesList_Test() mock.Setup(x => x.GetAsync( new Uri("favorites/list.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -796,7 +854,7 @@ public async Task FavoritesList_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FavoritesList(200, maxId: 900L, sinceId: 100L) .ConfigureAwait(false); @@ -811,7 +869,8 @@ public async Task FavoritesCreate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("favorites/create.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "id", "100" }, { "tweet_mode", "extended" }, }) @@ -819,7 +878,7 @@ public async Task FavoritesCreate_Test() .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FavoritesCreate(statusId: 100L) .IgnoreResponse() @@ -835,7 +894,8 @@ public async Task FavoritesDestroy_Test() mock.Setup(x => x.PostLazyAsync( new Uri("favorites/destroy.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "id", "100" }, { "tweet_mode", "extended" }, }) @@ -843,7 +903,7 @@ public async Task FavoritesDestroy_Test() .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FavoritesDestroy(statusId: 100L) .IgnoreResponse() @@ -865,7 +925,7 @@ public async Task FriendshipsShow_Test() .ReturnsAsync(new TwitterFriendship()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FriendshipsShow(sourceScreenName: "twitter", targetScreenName: "twitterapi") .ConfigureAwait(false); @@ -885,7 +945,7 @@ public async Task FriendshipsCreate_Test() .ReturnsAsync(LazyJson.Create(new TwitterFriendship())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FriendshipsCreate(screenName: "twitterapi") .IgnoreResponse() @@ -906,7 +966,7 @@ public async Task FriendshipsDestroy_Test() .ReturnsAsync(LazyJson.Create(new TwitterFriendship())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FriendshipsDestroy(screenName: "twitterapi") .IgnoreResponse() @@ -928,7 +988,7 @@ public async Task NoRetweetIds_Test() .ReturnsAsync(Array.Empty()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.NoRetweetIds() .ConfigureAwait(false); @@ -949,7 +1009,7 @@ public async Task FollowersIds_Test() .ReturnsAsync(new TwitterIds()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.FollowersIds(cursor: -1L) .ConfigureAwait(false); @@ -970,7 +1030,7 @@ public async Task MutesUsersIds_Test() .ReturnsAsync(new TwitterIds()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MutesUsersIds(cursor: -1L) .ConfigureAwait(false); @@ -991,7 +1051,7 @@ public async Task BlocksIds_Test() .ReturnsAsync(new TwitterIds()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.BlocksIds(cursor: -1L) .ConfigureAwait(false); @@ -1006,7 +1066,8 @@ public async Task BlocksCreate_Test() mock.Setup(x => x.PostLazyAsync( new Uri("blocks/create.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "tweet_mode", "extended" }, }) @@ -1014,7 +1075,7 @@ public async Task BlocksCreate_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.BlocksCreate(screenName: "twitterapi") .IgnoreResponse() @@ -1030,7 +1091,8 @@ public async Task BlocksDestroy_Test() mock.Setup(x => x.PostLazyAsync( new Uri("blocks/destroy.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "screen_name", "twitterapi" }, { "tweet_mode", "extended" }, }) @@ -1038,7 +1100,7 @@ public async Task BlocksDestroy_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.BlocksDestroy(screenName: "twitterapi") .IgnoreResponse() @@ -1054,7 +1116,8 @@ public async Task AccountVerifyCredentials_Test() mock.Setup(x => x.GetAsync( new Uri("account/verify_credentials.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -1068,7 +1131,7 @@ public async Task AccountVerifyCredentials_Test() }); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.AccountVerifyCredentials() .ConfigureAwait(false); @@ -1086,7 +1149,8 @@ public async Task AccountUpdateProfile_Test() mock.Setup(x => x.PostLazyAsync( new Uri("account/update_profile.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -1099,7 +1163,7 @@ public async Task AccountUpdateProfile_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.AccountUpdateProfile(name: "Name", url: "http://example.com/", location: "Location", description: "") .IgnoreResponse() @@ -1117,7 +1181,8 @@ public async Task AccountUpdateProfileImage_Test() mock.Setup(x => x.PostLazyAsync( new Uri("account/update_profile_image.json", UriKind.Relative), - new Dictionary { + new Dictionary + { { "include_entities", "true" }, { "include_ext_alt_text", "true" }, { "tweet_mode", "extended" }, @@ -1127,7 +1192,7 @@ public async Task AccountUpdateProfileImage_Test() .ReturnsAsync(LazyJson.Create(new TwitterUser())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.AccountUpdateProfileImage(media) .IgnoreResponse() @@ -1149,7 +1214,7 @@ public async Task ApplicationRateLimitStatus_Test() .ReturnsAsync(new TwitterRateLimits()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.ApplicationRateLimitStatus() .ConfigureAwait(false); @@ -1170,7 +1235,7 @@ public async Task Configuration_Test() .ReturnsAsync(new TwitterConfiguration()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.Configuration() .ConfigureAwait(false); @@ -1185,7 +1250,8 @@ public async Task MediaUploadInit_Test() mock.Setup(x => x.PostLazyAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary { + new Dictionary + { { "command", "INIT" }, { "total_bytes", "123456" }, { "media_type", "image/png" }, @@ -1195,7 +1261,7 @@ public async Task MediaUploadInit_Test() .ReturnsAsync(LazyJson.Create(new TwitterUploadMediaInit())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadInit(totalBytes: 123456L, mediaType: "image/png", mediaCategory: "dm_image") .IgnoreResponse() @@ -1213,7 +1279,8 @@ public async Task MediaUploadAppend_Test() mock.Setup(x => x.PostAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary { + new Dictionary + { { "command", "APPEND" }, { "media_id", "11111" }, { "segment_index", "1" }, @@ -1223,7 +1290,7 @@ public async Task MediaUploadAppend_Test() .Returns(Task.CompletedTask); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadAppend(mediaId: 11111L, segmentIndex: 1, media: media) .ConfigureAwait(false); @@ -1238,7 +1305,8 @@ public async Task MediaUploadFinalize_Test() mock.Setup(x => x.PostLazyAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary { + new Dictionary + { { "command", "FINALIZE" }, { "media_id", "11111" }, }) @@ -1246,7 +1314,7 @@ public async Task MediaUploadFinalize_Test() .ReturnsAsync(LazyJson.Create(new TwitterUploadMediaResult())); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadFinalize(mediaId: 11111L) .IgnoreResponse() @@ -1262,7 +1330,8 @@ public async Task MediaUploadStatus_Test() mock.Setup(x => x.GetAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary { + new Dictionary + { { "command", "STATUS" }, { "media_id", "11111" }, }, @@ -1271,7 +1340,7 @@ public async Task MediaUploadStatus_Test() .ReturnsAsync(new TwitterUploadMediaResult()); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadStatus(mediaId: 11111L) .ConfigureAwait(false); @@ -1291,7 +1360,7 @@ public async Task MediaMetadataCreate_Test() .Returns(Task.CompletedTask); using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - twitterApi.apiConnection = mock.Object; + twitterApi.ApiConnection = mock.Object; await twitterApi.MediaMetadataCreate(mediaId: 12345L, altText: "hogehoge") .ConfigureAwait(false); diff --git a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs new file mode 100644 index 000000000..82ed7e59a --- /dev/null +++ b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs @@ -0,0 +1,65 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2922 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using OpenTween.Api.DataModel; +using OpenTween.Connection; +using Xunit; + +namespace OpenTween.Api.TwitterV2 +{ + public class GetTimelineRequestTest + { + [Fact] + public async Task StatusesMentionsTimeline_Test() + { + var mock = new Mock(); + mock.Setup(x => + x.GetAsync( + new Uri("/2/users/100/timelines/reverse_chronological", UriKind.Relative), + new Dictionary + { + { "tweet.fields", "id" }, + { "max_results", "200" }, + { "until_id", "900" }, + { "since_id", "100" }, + }, + "/2/users/:id/timelines/reverse_chronological" + ) + ) + .ReturnsAsync(new TwitterV2TweetIds()); + + var request = new GetTimelineRequest(userId: 100L) + { + MaxResults = 200, + SinceId = "100", + UntilId = "900", + }; + + await request.Send(mock.Object).ConfigureAwait(false); + + mock.VerifyAll(); + } + } +} diff --git a/OpenTween.Tests/ApplicationInstanceMutexTest.cs b/OpenTween.Tests/ApplicationInstanceMutexTest.cs new file mode 100644 index 000000000..d6fb25cbf --- /dev/null +++ b/OpenTween.Tests/ApplicationInstanceMutexTest.cs @@ -0,0 +1,48 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using Xunit; + +namespace OpenTween +{ + public class ApplicationInstanceMutexTest + { + [Fact] + public void InstanceExists_SameNameTest() + { + using var mutex1 = new ApplicationInstanceMutex("OpenTweenTest", "."); + Assert.False(mutex1.InstanceExists); + + using var mutex2 = new ApplicationInstanceMutex("OpenTweenTest", "."); + Assert.True(mutex2.InstanceExists); + } + + [Fact] + public void InstanceExists_NotSameNameTest() + { + using var mutex1 = new ApplicationInstanceMutex("OpenTweenTest", "aaa"); + Assert.False(mutex1.InstanceExists); + + using var mutex2 = new ApplicationInstanceMutex("OpenTweenTest", "bbb"); + Assert.False(mutex2.InstanceExists); + } + } +} diff --git a/OpenTween.Tests/AsyncTimerTest.cs b/OpenTween.Tests/AsyncTimerTest.cs index 1c7d130a2..c3f1a785a 100644 --- a/OpenTween.Tests/AsyncTimerTest.cs +++ b/OpenTween.Tests/AsyncTimerTest.cs @@ -69,11 +69,13 @@ public void Change_PropertiesTest() public async Task UnhandledException_Test() { var tcs = new TaskCompletionSource(); - EventHandler handler = (_, ev) => tcs.TrySetResult(ev.Exception); + + void Handler(object sender, ThreadExceptionEventArgs ev) + => tcs.TrySetResult(ev.Exception); try { - AsyncTimer.UnhandledException += handler; + AsyncTimer.UnhandledException += Handler; using var timer = new AsyncTimer(() => { @@ -88,7 +90,7 @@ public async Task UnhandledException_Test() } finally { - AsyncTimer.UnhandledException -= handler; + AsyncTimer.UnhandledException -= Handler; } } } diff --git a/OpenTween.Tests/CommandLineArgsTest.cs b/OpenTween.Tests/CommandLineArgsTest.cs new file mode 100644 index 000000000..128b8fd18 --- /dev/null +++ b/OpenTween.Tests/CommandLineArgsTest.cs @@ -0,0 +1,97 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using Xunit; + +namespace OpenTween +{ + public class CommandLineArgsTest + { + [Fact] + public void ParseArguments_NoOptionsTest() + { + var args = new string[] { }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Empty(parsedArgs); + } + + [Fact] + public void ParseArguments_SingleOptionTest() + { + var args = new[] { "/foo" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Single(parsedArgs); + Assert.Equal("", parsedArgs["foo"]); + } + + [Fact] + public void ParseArguments_MultipleOptionsTest() + { + var args = new[] { "/foo", "/bar" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Equal(2, parsedArgs.Count); + Assert.Equal("", parsedArgs["foo"]); + Assert.Equal("", parsedArgs["bar"]); + } + + [Fact] + public void ParseArguments_OptionWithArgumentTest() + { + var args = new[] { "/foo:hogehoge" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Single(parsedArgs); + Assert.Equal("hogehoge", parsedArgs["foo"]); + } + + [Fact] + public void ParseArguments_OptionWithEmptyArgumentTest() + { + var args = new[] { "/foo:" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Single(parsedArgs); + Assert.Equal("", parsedArgs["foo"]); + } + + [Fact] + public void ParseArguments_IgroreInvalidOptionsTest() + { + var args = new string[] { "--foo", "/" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Empty(parsedArgs); + } + + [Fact] + public void ParseArguments_DuplicateOptionsTest() + { + var args = new[] { "/foo:abc", "/foo:123" }; + var parsedArgs = new CommandLineArgs(args); + + Assert.Single(parsedArgs); + Assert.Equal("123", parsedArgs["foo"]); + } + } +} diff --git a/OpenTween.Tests/Connection/LazyJsonTest.cs b/OpenTween.Tests/Connection/LazyJsonTest.cs index 242797fdc..0c003aca9 100644 --- a/OpenTween.Tests/Connection/LazyJsonTest.cs +++ b/OpenTween.Tests/Connection/LazyJsonTest.cs @@ -92,14 +92,18 @@ await Task.FromResult(lazyJson) Assert.True(bodyStream.IsDisposed); } - class InvalidStream : Stream + private class InvalidStream : Stream { public bool IsDisposed { get; private set; } = false; public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => 100L; + public override long Position { get => 0L; diff --git a/OpenTween.Tests/Connection/OAuthUtilityTest.cs b/OpenTween.Tests/Connection/OAuthUtilityTest.cs index 0c607b444..e87c0a4d5 100644 --- a/OpenTween.Tests/Connection/OAuthUtilityTest.cs +++ b/OpenTween.Tests/Connection/OAuthUtilityTest.cs @@ -52,8 +52,12 @@ public void CreateSignature_Test() { // GET http://example.com/hoge?aaa=foo に対する署名を生成 // 実際の param は oauth_consumer_key などのパラメーターが加わった状態で渡される - var oauthSignature = OAuthUtility.CreateSignature(ApiKey.Create("ConsumerSecret"), "TokenSecret", - "GET", new Uri("http://example.com/hoge"), new Dictionary { ["aaa"] = "foo" }); + var oauthSignature = OAuthUtility.CreateSignature( + ApiKey.Create("ConsumerSecret"), + "TokenSecret", + "GET", + new Uri("http://example.com/hoge"), + new Dictionary { ["aaa"] = "foo" }); var expectSignatureBase = "GET&http%3A%2F%2Fexample.com%2Fhoge&aaa%3Dfoo"; var expectSignatureKey = "ConsumerSecret&TokenSecret"; @@ -68,8 +72,13 @@ public void CreateSignature_NormarizeParametersTest() { // GET http://example.com/hoge?aaa=foo&bbb=bar に対する署名を生成 // 複数のパラメータが渡される場合は name 順でソートされる - var oauthSignature = OAuthUtility.CreateSignature(ApiKey.Create("ConsumerSecret"), "TokenSecret", - "GET", new Uri("http://example.com/hoge"), new Dictionary { + var oauthSignature = OAuthUtility.CreateSignature( + ApiKey.Create("ConsumerSecret"), + "TokenSecret", + "GET", + new Uri("http://example.com/hoge"), + new Dictionary + { ["bbb"] = "bar", ["aaa"] = "foo", }); @@ -87,8 +96,12 @@ public void CreateSignature_EmptyTokenSecretTest() { // GET http://example.com/hoge?aaa=foo に対する署名を生成 // リクエストトークンの発行時は tokenSecret が空の状態で署名を生成することになる - var oauthSignature = OAuthUtility.CreateSignature(ApiKey.Create("ConsumerSecret"), null, - "GET", new Uri("http://example.com/hoge"), new Dictionary { ["aaa"] = "foo" }); + var oauthSignature = OAuthUtility.CreateSignature( + ApiKey.Create("ConsumerSecret"), + null, + "GET", + new Uri("http://example.com/hoge"), + new Dictionary { ["aaa"] = "foo" }); var expectSignatureBase = "GET&http%3A%2F%2Fexample.com%2Fhoge&aaa%3Dfoo"; var expectSignatureKey = "ConsumerSecret&"; // 末尾の & は除去されない @@ -102,8 +115,14 @@ public void CreateSignature_EmptyTokenSecretTest() public void CreateAuthorization_Test() { var authorization = OAuthUtility.CreateAuthorization( - "GET", new Uri("http://example.com/hoge"), new Dictionary { ["aaa"] = "hoge" }, - ApiKey.Create("ConsumerKey"), ApiKey.Create("ConsumerSecret"), "AccessToken", "AccessSecret", "Realm"); + "GET", + new Uri("http://example.com/hoge"), + new Dictionary { ["aaa"] = "hoge" }, + ApiKey.Create("ConsumerKey"), + ApiKey.Create("ConsumerSecret"), + "AccessToken", + "AccessSecret", + "Realm"); Assert.StartsWith("OAuth ", authorization, StringComparison.Ordinal); @@ -112,8 +131,17 @@ public void CreateAuthorization_Test() .Select(x => x.Split(new[] { '=' }, 2)) .ToDictionary(x => x[0], x => x[1].Substring(1, x[1].Length - 2)); // x[1] は前後の「"」を除去する - var expectAuthzParamKeys = new[] { "realm", "oauth_consumer_key", "oauth_nonce", "oauth_signature_method", - "oauth_timestamp", "oauth_token", "oauth_version", "oauth_signature" }; + var expectAuthzParamKeys = new[] + { + "realm", + "oauth_consumer_key", + "oauth_nonce", + "oauth_signature_method", + "oauth_timestamp", + "oauth_token", + "oauth_version", + "oauth_signature", + }; Assert.Equal(expectAuthzParamKeys, parsedParams.Keys, AnyOrderComparer.Instance); Assert.Equal("Realm", parsedParams["realm"]); diff --git a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs index 612e73e8e..8f840e8e1 100644 --- a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs +++ b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs @@ -57,7 +57,7 @@ public async Task GetAsync_Test() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -96,7 +96,7 @@ public async Task GetAsync_AbsoluteUriTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -134,7 +134,7 @@ public async Task GetAsync_UpdateRateLimitTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -175,7 +175,7 @@ public async Task GetAsync_ErrorStatusTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -203,7 +203,7 @@ public async Task GetAsync_ErrorJsonTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -234,7 +234,7 @@ public async Task GetStreamAsync_Test() using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); using var image = TestUtils.CreateDummyImage(); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { @@ -280,7 +280,7 @@ public async Task PostLazyAsync_Test() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(async x => { @@ -311,8 +311,7 @@ public async Task PostLazyAsync_Test() var result = await apiConnection.PostLazyAsync(endpoint, param) .ConfigureAwait(false); - Assert.Equal("hogehoge", await result.LoadJsonAsync() - .ConfigureAwait(false)); + Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); Assert.Equal(0, mockHandler.QueueCount); } @@ -323,7 +322,7 @@ public async Task PostLazyAsync_MultipartTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.httpUpload = http; + apiConnection.HttpUpload = http; using var image = TestUtils.CreateDummyImage(); using var media = new MemoryImageMediaItem(image); @@ -339,8 +338,8 @@ public async Task PostLazyAsync_MultipartTest() var boundary = x.Content.Headers.ContentType.Parameters.Cast() .First(y => y.Name == "boundary").Value; - // 前後のダブルクオーテーションを除去 - boundary = boundary.Substring(1, boundary.Length - 2); + // 前後のダブルクオーテーションを除去 + boundary = boundary.Substring(1, boundary.Length - 2); var expectedText = $"--{boundary}\r\n" + @@ -383,8 +382,7 @@ public async Task PostLazyAsync_MultipartTest() var result = await apiConnection.PostLazyAsync(endpoint, param, mediaParam) .ConfigureAwait(false); - Assert.Equal("hogehoge", await result.LoadJsonAsync() - .ConfigureAwait(false)); + Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); Assert.Equal(0, mockHandler.QueueCount); } @@ -395,7 +393,7 @@ public async Task PostLazyAsync_Multipart_NullTest() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.httpUpload = http; + apiConnection.HttpUpload = http; mockHandler.Enqueue(async x => { @@ -408,8 +406,8 @@ public async Task PostLazyAsync_Multipart_NullTest() var boundary = x.Content.Headers.ContentType.Parameters.Cast() .First(y => y.Name == "boundary").Value; - // 前後のダブルクオーテーションを除去 - boundary = boundary.Substring(1, boundary.Length - 2); + // 前後のダブルクオーテーションを除去 + boundary = boundary.Substring(1, boundary.Length - 2); var expectedText = $"--{boundary}\r\n" + @@ -430,8 +428,7 @@ public async Task PostLazyAsync_Multipart_NullTest() var result = await apiConnection.PostLazyAsync(endpoint, param: null, media: null) .ConfigureAwait(false); - Assert.Equal("hogehoge", await result.LoadJsonAsync() - .ConfigureAwait(false)); + Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); Assert.Equal(0, mockHandler.QueueCount); } @@ -442,7 +439,7 @@ public async Task PostJsonAsync_Test() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(async x => { @@ -474,7 +471,7 @@ public async Task PostJsonAsync_T_Test() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(async x => { @@ -514,7 +511,7 @@ public async Task DeleteAsync_Test() using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.http = http; + apiConnection.Http = http; mockHandler.Enqueue(x => { diff --git a/OpenTween.Tests/CultureServiceTest.cs b/OpenTween.Tests/CultureServiceTest.cs new file mode 100644 index 000000000..9844b551e --- /dev/null +++ b/OpenTween.Tests/CultureServiceTest.cs @@ -0,0 +1,45 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2013 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Xunit; +using Xunit.Extensions; + +namespace OpenTween +{ + public class CultureServiceTest + { + [Theory] + [InlineData("ja-JP", "ja-JP")] + [InlineData("fr-FR", "en")] // 対応するカルチャが無い場合は en にフォールバックする + [InlineData("zh-CN", "en")] // zh-CHS リソースは v1.3.7 から削除 + [InlineData("zh-TW", "en")] + public void GetPreferredCulture_Test(string currentCulture, string expectedCulture) + { + var actual = CultureService.GetPreferredCulture(new CultureInfo(currentCulture)); + Assert.Equal(expectedCulture, actual.Name); + } + } +} diff --git a/OpenTween.Tests/DateTimeUtcTest.cs b/OpenTween.Tests/DateTimeUtcTest.cs index 9ed6cd291..8f9d9f167 100644 --- a/OpenTween.Tests/DateTimeUtcTest.cs +++ b/OpenTween.Tests/DateTimeUtcTest.cs @@ -269,7 +269,7 @@ public void FromUnixTime_Test() utc.ToDateTimeUnsafe()); } - public static readonly TheoryData Parse_Test_Fixtures = new TheoryData + public static readonly TheoryData ParseTestFixtures = new() { { "2018-05-06T11:22:33.111", new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, { "2018-05-06T11:22:33.111+00:00", new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, @@ -277,7 +277,7 @@ public void FromUnixTime_Test() }; [Theory] - [MemberData(nameof(Parse_Test_Fixtures))] + [MemberData(nameof(ParseTestFixtures))] public void Parse_Test(string input, DateTimeUtc expected) => Assert.Equal(expected, DateTimeUtc.Parse(input, DateTimeFormatInfo.InvariantInfo)); @@ -285,7 +285,7 @@ public void Parse_Test(string input, DateTimeUtc expected) public void Parse_ErrorTest() => Assert.Throws(() => DateTimeUtc.Parse("### INVALID ###", DateTimeFormatInfo.InvariantInfo)); - public static readonly TheoryData TryParse_Test_Fixtures = new TheoryData + public static readonly TheoryData TryParseTestFixtures = new() { { "2018-05-06T11:22:33.111", true, new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, { "2018-05-06T11:22:33.111+00:00", true, new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, @@ -294,7 +294,7 @@ public void Parse_ErrorTest() }; [Theory] - [MemberData(nameof(TryParse_Test_Fixtures))] + [MemberData(nameof(TryParseTestFixtures))] public void TryParse_Test(string input, bool expectedParsed, DateTimeUtc expectedResult) { var parsed = DateTimeUtc.TryParse(input, DateTimeFormatInfo.InvariantInfo, out var result); @@ -303,7 +303,7 @@ public void TryParse_Test(string input, bool expectedParsed, DateTimeUtc expecte Assert.Equal(expectedResult, result); } - public static readonly TheoryData TryParseExact_Test_Fixtures = new TheoryData + public static readonly TheoryData TryParseExactTestFixtures = new() { { "2018-05-06 11:22:33.111", "yyyy-MM-dd HH:mm:ss.fff", true, new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, { "2018-05-06 11:22:33.111 +00:00", "yyyy-MM-dd HH:mm:ss.fff zzz", true, new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, @@ -313,7 +313,7 @@ public void TryParse_Test(string input, bool expectedParsed, DateTimeUtc expecte }; [Theory] - [MemberData(nameof(TryParseExact_Test_Fixtures))] + [MemberData(nameof(TryParseExactTestFixtures))] public void TryParseExact_Test(string input, string format, bool expectedParsed, DateTimeUtc expectedResult) { var parsed = DateTimeUtc.TryParseExact(input, new[] { format }, DateTimeFormatInfo.InvariantInfo, out var result); diff --git a/OpenTween.Tests/DebounceTimerTest.cs b/OpenTween.Tests/DebounceTimerTest.cs index 6949084b6..aadda5b2c 100644 --- a/OpenTween.Tests/DebounceTimerTest.cs +++ b/OpenTween.Tests/DebounceTimerTest.cs @@ -32,7 +32,7 @@ public class DebounceTimerTest { private class TestDebounceTimer : DebounceTimer { - public MockTimer mockTimer = new MockTimer(() => Task.CompletedTask); + public MockTimer MockTimer = new(() => Task.CompletedTask); public TestDebounceTimer(Func timerCallback, TimeSpan interval, bool leading, bool trailing) : base(timerCallback, interval, leading, trailing) @@ -40,7 +40,7 @@ public TestDebounceTimer(Func timerCallback, TimeSpan interval, bool leadi } protected override ITimer CreateTimer(Func callback) - => this.mockTimer = new MockTimer(callback); + => this.MockTimer = new MockTimer(callback); } [Fact] @@ -49,16 +49,18 @@ public async Task Callback_Debounce_Trailing_Test() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: false, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -104,16 +106,18 @@ public async Task Callback_Debounce_Trailing_CallOnceTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: false, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -142,16 +146,18 @@ public async Task Callback_Debounce_Trailing_ResumeTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: false, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -196,16 +202,18 @@ public async Task Callback_Debounce_LeadingAndTrailing_Test() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: true, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -251,16 +259,18 @@ public async Task Callback_Debounce_LeadingAndTrailing_CallOnceTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: true, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -289,16 +299,18 @@ public async Task Callback_Debounce_LeadingAndTrailing_ResumeTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: true, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -360,16 +372,18 @@ public async Task Callback_Debounce_SystemClockChangedTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 1, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.MaxValue; - using var debouncing = new TestDebounceTimer(callback, interval, leading: false, trailing: true); - var mockTimer = debouncing.mockTimer; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); diff --git a/OpenTween.Tests/DisposableLazyTest.cs b/OpenTween.Tests/DisposableLazyTest.cs new file mode 100644 index 000000000..15296f178 --- /dev/null +++ b/OpenTween.Tests/DisposableLazyTest.cs @@ -0,0 +1,85 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace OpenTween +{ + public class DisposableLazyTest + { + [Fact] + public void Value_Test() + { + var mock = new Mock(); + var obj = mock.Object; + + var lazy = new DisposableLazy(() => obj); + Assert.False(lazy.IsValueCreated); + Assert.Equal(obj, lazy.Value); + Assert.True(lazy.IsValueCreated); + } + + [Fact] + public void Value_DisposedErrorTest() + { + var mock = new Mock(); + var obj = mock.Object; + + var lazy = new DisposableLazy(() => obj); + lazy.Dispose(); + + Assert.Throws(() => lazy.Value); + } + + [Fact] + public void Dispose_BeforeValueCreatedTest() + { + var mock = new Mock(); + + var lazy = new DisposableLazy(() => mock.Object); + lazy.Dispose(); + + Assert.False(lazy.IsValueCreated); + Assert.True(lazy.IsDisposed); + mock.Verify(x => x.Dispose(), Times.Never()); + } + + [Fact] + public void Dispose_AfterValueCreatedTest() + { + var mock = new Mock(); + + var lazy = new DisposableLazy(() => mock.Object); + _ = lazy.Value; + lazy.Dispose(); + + Assert.True(lazy.IsValueCreated); + Assert.True(lazy.IsDisposed); + mock.Verify(x => x.Dispose(), Times.Once()); + } + } +} diff --git a/OpenTween.Tests/ExtensionsTest.cs b/OpenTween.Tests/ExtensionsTest.cs index f117b73b5..7a6bbd90b 100644 --- a/OpenTween.Tests/ExtensionsTest.cs +++ b/OpenTween.Tests/ExtensionsTest.cs @@ -19,7 +19,6 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using Moq; using System; using System.Collections.Generic; using System.Globalization; @@ -27,6 +26,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Moq; using Xunit; namespace OpenTween diff --git a/OpenTween.Tests/HttpMessageHandlerMock.cs b/OpenTween.Tests/HttpMessageHandlerMock.cs index feb8ae568..60994232d 100644 --- a/OpenTween.Tests/HttpMessageHandlerMock.cs +++ b/OpenTween.Tests/HttpMessageHandlerMock.cs @@ -31,21 +31,21 @@ namespace OpenTween { public class HttpMessageHandlerMock : HttpMessageHandler { - private readonly Queue>> Queue = - new Queue>>(); + private readonly Queue>> queue = + new(); public int QueueCount - => this.Queue.Count; + => this.queue.Count; public void Enqueue(Func> handler) - => this.Queue.Enqueue(handler); + => this.queue.Enqueue(handler); public void Enqueue(Func handler) - => this.Queue.Enqueue(x => Task.Run(() => handler(x))); + => this.queue.Enqueue(x => Task.Run(() => handler(x))); protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var handler = this.Queue.Dequeue(); + var handler = this.queue.Dequeue(); return await handler(request); } } diff --git a/OpenTween.Tests/LRUCacheDictionaryTest.cs b/OpenTween.Tests/LRUCacheDictionaryTest.cs index 046d6b436..d9d56bd39 100644 --- a/OpenTween.Tests/LRUCacheDictionaryTest.cs +++ b/OpenTween.Tests/LRUCacheDictionaryTest.cs @@ -31,7 +31,7 @@ namespace OpenTween { public class LRUCacheDictionaryTest { - private static readonly AnyOrderComparer collComparer = AnyOrderComparer.Instance; + private static readonly AnyOrderComparer CollComparer = AnyOrderComparer.Instance; [Fact] public void InnerListTest() @@ -43,7 +43,7 @@ public void InnerListTest() ["key3"] = "value3", }; - var node = dict.innerList.First; + var node = dict.InnerList.First; Assert.Equal("key3", node.Value.Key); node = node.Next; Assert.Equal("key2", node.Value.Key); @@ -56,7 +56,7 @@ public void InnerListTest() _ = dict["key1"]; // 直近に参照した順で並んでいるかテスト - node = dict.innerList.First; + node = dict.InnerList.First; Assert.Equal("key1", node.Value.Key); node = node.Next; Assert.Equal("key3", node.Value.Key); @@ -140,7 +140,7 @@ public void AutoTrimTest() dict["key4"] = "value4"; // 4アクセス目 (この直後にTrim) // 1 -> 2 -> 3 -> 4 の順にアクセスしたため、直近 3 件の 2, 3, 4 だけが残る - Assert.Equal(new[] { "key2", "key3", "key4" }, dict.innerDict.Keys, collComparer); + Assert.Equal(new[] { "key2", "key3", "key4" }, dict.InnerDict.Keys, CollComparer); dict["key5"] = "value5"; // 5アクセス目 dict.Add("key6", "value6"); // 6アクセス目 @@ -148,7 +148,7 @@ public void AutoTrimTest() dict.TryGetValue("key4", out _); // 8アクセス目 (この直後にTrim) // 5 -> 6 -> 2 -> 4 の順でアクセスしたため、直近 3 件の 6, 2, 4 だけが残る - Assert.Equal(new[] { "key6", "key2", "key4" }, dict.innerDict.Keys, collComparer); + Assert.Equal(new[] { "key6", "key2", "key4" }, dict.InnerDict.Keys, CollComparer); } [Fact] @@ -170,7 +170,7 @@ public void CacheRemovedEventTest() dict.Trim(); // 直近に参照された 3, 4 以外のアイテムに対してイベントが発生しているはず - Assert.Equal(new[] { "key1", "key2" }, removedList, collComparer); + Assert.Equal(new[] { "key1", "key2" }, removedList, CollComparer); } // ここから下は IDictionary としての機能が正しく動作するかのテスト @@ -182,17 +182,17 @@ public void AddTest() dict.Add("key1", "value1"); - Assert.Single(dict.innerDict); - Assert.True(dict.innerDict.ContainsKey("key1")); - var internalNode = dict.innerDict["key1"]; + Assert.Single(dict.InnerDict); + Assert.True(dict.InnerDict.ContainsKey("key1")); + var internalNode = dict.InnerDict["key1"]; Assert.Equal("key1", internalNode.Value.Key); Assert.Equal("value1", internalNode.Value.Value); dict.Add("key2", "value2"); - Assert.Equal(2, dict.innerDict.Count); - Assert.True(dict.innerDict.ContainsKey("key2")); - internalNode = dict.innerDict["key2"]; + Assert.Equal(2, dict.InnerDict.Count); + Assert.True(dict.InnerDict.ContainsKey("key2")); + internalNode = dict.InnerDict["key2"]; Assert.Equal("key2", internalNode.Value.Key); Assert.Equal("value2", internalNode.Value.Value); } @@ -242,17 +242,17 @@ public void RemoveTest() var ret = dict.Remove("key1"); Assert.True(ret); - Assert.Equal(2, dict.innerDict.Count); - Assert.Equal(2, dict.innerList.Count); - Assert.False(dict.innerDict.ContainsKey("key1")); - Assert.True(dict.innerDict.ContainsKey("key2")); - Assert.True(dict.innerDict.ContainsKey("key3")); + Assert.Equal(2, dict.InnerDict.Count); + Assert.Equal(2, dict.InnerList.Count); + Assert.False(dict.InnerDict.ContainsKey("key1")); + Assert.True(dict.InnerDict.ContainsKey("key2")); + Assert.True(dict.InnerDict.ContainsKey("key3")); dict.Remove("key2"); dict.Remove("key3"); - Assert.Empty(dict.innerDict); - Assert.Empty(dict.innerList); + Assert.Empty(dict.InnerDict); + Assert.Empty(dict.InnerList); ret = dict.Remove("hogehoge"); Assert.False(ret); @@ -271,11 +271,11 @@ public void Remove2Test() var ret = dict.Remove(new KeyValuePair("key1", "value1")); Assert.True(ret); - Assert.Equal(2, dict.innerDict.Count); - Assert.Equal(2, dict.innerList.Count); - Assert.False(dict.innerDict.ContainsKey("key1")); - Assert.True(dict.innerDict.ContainsKey("key2")); - Assert.True(dict.innerDict.ContainsKey("key3")); + Assert.Equal(2, dict.InnerDict.Count); + Assert.Equal(2, dict.InnerList.Count); + Assert.False(dict.InnerDict.ContainsKey("key1")); + Assert.True(dict.InnerDict.ContainsKey("key2")); + Assert.True(dict.InnerDict.ContainsKey("key3")); ret = dict.Remove(new KeyValuePair("key2", "hogehoge")); Assert.False(ret); @@ -283,8 +283,8 @@ public void Remove2Test() dict.Remove(new KeyValuePair("key2", "value2")); dict.Remove(new KeyValuePair("key3", "value3")); - Assert.Empty(dict.innerDict); - Assert.Empty(dict.innerList); + Assert.Empty(dict.InnerDict); + Assert.Empty(dict.InnerList); ret = dict.Remove(new KeyValuePair("hogehoge", "hogehoge")); Assert.False(ret); @@ -318,11 +318,11 @@ public void SetterTest() }; dict["key1"] = "foo"; - Assert.Equal("foo", dict.innerDict["key1"].Value.Value); + Assert.Equal("foo", dict.InnerDict["key1"].Value.Value); dict["hogehoge"] = "bar"; - Assert.True(dict.innerDict.ContainsKey("hogehoge")); - Assert.Equal("bar", dict.innerDict["hogehoge"].Value.Value); + Assert.True(dict.InnerDict.ContainsKey("hogehoge")); + Assert.Equal("bar", dict.InnerDict["hogehoge"].Value.Value); } [Fact] @@ -354,13 +354,13 @@ public void KeysTest() ["key3"] = "value3", }; - Assert.Equal(new[] { "key1", "key2", "key3" }, dict.Keys, collComparer); + Assert.Equal(new[] { "key1", "key2", "key3" }, dict.Keys, CollComparer); dict.Add("foo", "bar"); - Assert.Equal(new[] { "key1", "key2", "key3", "foo" }, dict.Keys, collComparer); + Assert.Equal(new[] { "key1", "key2", "key3", "foo" }, dict.Keys, CollComparer); dict.Remove("key2"); - Assert.Equal(new[] { "key1", "key3", "foo" }, dict.Keys, collComparer); + Assert.Equal(new[] { "key1", "key3", "foo" }, dict.Keys, CollComparer); dict.Clear(); Assert.Empty(dict.Keys); @@ -376,13 +376,13 @@ public void ValuesTest() ["key3"] = "value3", }; - Assert.Equal(new[] { "value1", "value2", "value3" }, dict.Values, collComparer); + Assert.Equal(new[] { "value1", "value2", "value3" }, dict.Values, CollComparer); dict.Add("foo", "bar"); - Assert.Equal(new[] { "value1", "value2", "value3", "bar" }, dict.Values, collComparer); + Assert.Equal(new[] { "value1", "value2", "value3", "bar" }, dict.Values, CollComparer); dict.Remove("key2"); - Assert.Equal(new[] { "value1", "value3", "bar" }, dict.Values, collComparer); + Assert.Equal(new[] { "value1", "value3", "bar" }, dict.Values, CollComparer); dict.Clear(); Assert.Empty(dict.Values); diff --git a/OpenTween.Tests/ListViewItemCacheTest.cs b/OpenTween.Tests/ListViewItemCacheTest.cs new file mode 100644 index 000000000..005f37fab --- /dev/null +++ b/OpenTween.Tests/ListViewItemCacheTest.cs @@ -0,0 +1,136 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Linq; +using System.Windows.Forms; +using Xunit; + +namespace OpenTween +{ + public class ListViewItemCacheTest + { + [Fact] + public void Cache_InvalidSizeTest() + { + var startIndex = 10; + var endIndex = 19; + Assert.Throws( + () => new ListViewItemCache(startIndex, endIndex, new (ListViewItem, ListItemStyle)[9]) + ); + Assert.Throws( + () => new ListViewItemCache(startIndex, endIndex, new (ListViewItem, ListItemStyle)[11]) + ); + } + + [Fact] + public void Count_Test() + { + var cache = new ListViewItemCache(10, 19, new (ListViewItem, ListItemStyle)[10]); + Assert.Equal(10, cache.Count); + } + + [Theory] + [InlineData(9, false)] + [InlineData(10, true)] + [InlineData(19, true)] + [InlineData(20, false)] + public void Contains_Test(int index, bool expected) + { + var cache = new ListViewItemCache(10, 19, new (ListViewItem, ListItemStyle)[10]); + Assert.Equal(expected, cache.Contains(index)); + } + + [Theory] + [InlineData(9, 19, false)] + [InlineData(9, 20, false)] + [InlineData(10, 19, true)] + [InlineData(10, 20, false)] + public void IsSupersetOf_Test(int start, int end, bool expected) + { + var cache = new ListViewItemCache(10, 19, new (ListViewItem, ListItemStyle)[10]); + Assert.Equal(expected, cache.IsSupersetOf(start, end)); + } + + [Fact] + public void TryGetValue_FoundTest() + { + var item = new ListViewItem(); + var style = new ListItemStyle(); + var cache = new ListViewItemCache(10, 10, new[] { (item, style) }); + + Assert.True(cache.TryGetValue(10, out var actualItem, out var actualStyle)); + Assert.Equal(item, actualItem); + Assert.Equal(style, actualStyle); + } + + [Fact] + public void TryGetValue_NotFoundTest() + { + var item = new ListViewItem(); + var style = new ListItemStyle(); + var cache = new ListViewItemCache(10, 10, new[] { (item, style) }); + + Assert.False(cache.TryGetValue(9, out _, out _)); + Assert.False(cache.TryGetValue(11, out _, out _)); + } + + [Fact] + public void WithIndex_Test() + { + var item1 = new ListViewItem(); + var style1 = new ListItemStyle(); + var item2 = new ListViewItem(); + var style2 = new ListItemStyle(); + var cache = new ListViewItemCache(10, 11, new[] { (item1, style1), (item2, style2) }); + + var actualArray = cache.WithIndex().ToArray(); + Assert.Equal(2, actualArray.Length); + Assert.Equal((item1, style1, 10), actualArray[0]); + Assert.Equal((item2, style2, 11), actualArray[1]); + } + + [Fact] + public void UpdateStyle_Test() + { + var item = new ListViewItem(); + var style = new ListItemStyle { UnreadMark = false }; + var cache = new ListViewItemCache(10, 10, new[] { (item, style) }); + + var newStyle = style with { UnreadMark = true }; + cache.UpdateStyle(10, newStyle); + + Assert.True(cache.TryGetValue(10, out _, out var actualStyle)); + Assert.True(actualStyle.UnreadMark); + } + + [Fact] + public void UpdateStyle_OutOfRangeTest() + { + var item = new ListViewItem(); + var style = new ListItemStyle { UnreadMark = false }; + var cache = new ListViewItemCache(10, 10, new[] { (item, style) }); + + var newStyle = style with { UnreadMark = true }; + cache.UpdateStyle(11, newStyle); // 特にエラーは起こさず無視する + } + } +} diff --git a/OpenTween.Tests/MemoryImageTest.cs b/OpenTween.Tests/MemoryImageTest.cs index 390d5dca7..b038455c0 100644 --- a/OpenTween.Tests/MemoryImageTest.cs +++ b/OpenTween.Tests/MemoryImageTest.cs @@ -36,70 +36,101 @@ public class MemoryImageTest [Fact] public async Task ImageFormat_GifTest() { - using (var imgStream = File.OpenRead("Resources/re.gif")) - using (var image = await MemoryImage.CopyFromStreamAsync(imgStream).ConfigureAwait(false)) - { - Assert.Equal(ImageFormat.Gif, image.ImageFormat); - Assert.Equal(".gif", image.ImageFormatExt); - } + using var imgStream = File.OpenRead("Resources/re.gif"); + using var image = await MemoryImage.CopyFromStreamAsync(imgStream).ConfigureAwait(false); + Assert.Equal(ImageFormat.Gif, image.ImageFormat); + Assert.Equal(".gif", image.ImageFormatExt); } [Fact] public void ImageFormat_CopyFromImageTest() { - using (var bitmap = new Bitmap(width: 200, height: 200)) - using (var image = MemoryImage.CopyFromImage(bitmap)) - { - // CopyFromImage から作成した MemoryImage は PNG 画像として扱われる - Assert.Equal(ImageFormat.Png, image.ImageFormat); - Assert.Equal(".png", image.ImageFormatExt); - } + using var bitmap = new Bitmap(width: 200, height: 200); + using var image = MemoryImage.CopyFromImage(bitmap); + + // CopyFromImage から作成した MemoryImage は PNG 画像として扱われる + Assert.Equal(ImageFormat.Png, image.ImageFormat); + Assert.Equal(".png", image.ImageFormatExt); + } + + [Fact] + public async Task CopyFromStream_Test() + { + using var stream = File.OpenRead("Resources/re.gif"); + using var memstream = new MemoryStream(); + await stream.CopyToAsync(memstream) + .ConfigureAwait(false); + + stream.Seek(0, SeekOrigin.Begin); + + using var image = MemoryImage.CopyFromStream(stream); + Assert.Equal(memstream.ToArray(), image.Stream.ToArray()); + } + + [Fact] + public async Task CopyFromStreamAsync_Test() + { + using var stream = File.OpenRead("Resources/re.gif"); + using var memstream = new MemoryStream(); + await stream.CopyToAsync(memstream) + .ConfigureAwait(false); + + stream.Seek(0, SeekOrigin.Begin); + + using var image = await MemoryImage.CopyFromStreamAsync(stream) + .ConfigureAwait(false); + Assert.Equal(memstream.ToArray(), image.Stream.ToArray()); + } + + [Fact] + public async Task CopyFromBytes_Test() + { + using var stream = File.OpenRead("Resources/re.gif"); + using var memstream = new MemoryStream(); + await stream.CopyToAsync(memstream) + .ConfigureAwait(false); + var imageBytes = memstream.ToArray(); + + using var image = MemoryImage.CopyFromBytes(imageBytes); + Assert.Equal(imageBytes, image.Stream.ToArray()); } [Fact] public void CopyFromImage_Test() { - using (var bitmap = new Bitmap(width: 200, height: 200)) - { - // MemoryImage をエラー無く作成できることをテストする - using (var image = MemoryImage.CopyFromImage(bitmap)) { } - } + using var bitmap = new Bitmap(width: 200, height: 200); + + // MemoryImage をエラー無く作成できることをテストする + using var image = MemoryImage.CopyFromImage(bitmap); } [Fact] public void Dispose_Test() { - using (var image = TestUtils.CreateDummyImage()) - { - Assert.False(image.IsDisposed); + using var image = TestUtils.CreateDummyImage(); + Assert.False(image.IsDisposed); - image.Dispose(); + image.Dispose(); - Assert.True(image.IsDisposed); - Assert.Throws(() => image.Image); - Assert.Throws(() => image.ImageFormat); - } + Assert.True(image.IsDisposed); + Assert.Throws(() => image.Image); + Assert.Throws(() => image.ImageFormat); } [Fact] public async Task Equals_Test() { - using (var imgStream1 = File.OpenRead("Resources/re.gif")) - using (var image1 = await MemoryImage.CopyFromStreamAsync(imgStream1).ConfigureAwait(false)) - { - using (var imgStream2 = File.OpenRead("Resources/re.gif")) - using (var image2 = await MemoryImage.CopyFromStreamAsync(imgStream2).ConfigureAwait(false)) - { - Assert.True(image1.Equals(image2)); - Assert.True(image2.Equals(image1)); - } - - using (var image3 = TestUtils.CreateDummyImage()) - { - Assert.False(image1.Equals(image3)); - Assert.False(image3.Equals(image1)); - } - } + using var imgStream1 = File.OpenRead("Resources/re.gif"); + using var image1 = await MemoryImage.CopyFromStreamAsync(imgStream1).ConfigureAwait(false); + + using var imgStream2 = File.OpenRead("Resources/re.gif"); + using var image2 = await MemoryImage.CopyFromStreamAsync(imgStream2).ConfigureAwait(false); + Assert.True(image1.Equals(image2)); + Assert.True(image2.Equals(image1)); + + using var image3 = TestUtils.CreateDummyImage(); + Assert.False(image1.Equals(image3)); + Assert.False(image3.Equals(image1)); } } } diff --git a/OpenTween.Tests/MockTimer.cs b/OpenTween.Tests/MockTimer.cs index 6be4ac5ad..ed537e1c7 100644 --- a/OpenTween.Tests/MockTimer.cs +++ b/OpenTween.Tests/MockTimer.cs @@ -31,7 +31,9 @@ namespace OpenTween public class MockTimer : ITimer { public bool IsTimerRunning { get; private set; } = false; + public TimeSpan DueTime { get; private set; } = Timeout.InfiniteTimeSpan; + public TimeSpan Period { get; private set; } = Timeout.InfiniteTimeSpan; private readonly Func callback; diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 63c115b1d..eebcf63e4 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -32,29 +32,49 @@ namespace OpenTween.Models { public class PostClassTest { - class TestPostClass : PostClass + private class PostClassGroup { + private readonly Dictionary testCases; + + public PostClassGroup(params TestPostClass[] postClasses) + { + this.testCases = new Dictionary(); + foreach (var p in postClasses) + { + p.Group = this; + this.testCases.Add(p.StatusId, p); + } + } + + public PostClass this[long id] => this.testCases[id]; + } + + private class TestPostClass : PostClass + { + public PostClassGroup? Group; + protected override PostClass RetweetSource { get { var retweetedId = this.RetweetedId!.Value; + var group = this.Group; + if (group == null) + throw new InvalidOperationException("TestPostClass needs group"); - return PostClassTest.TestCases[retweetedId]; + return group[retweetedId]; } } } - private static Dictionary TestCases; + private readonly PostClassGroup postGroup; - static PostClassTest() + public PostClassTest() { - PostClassTest.TestCases = new Dictionary - { - [1L] = new TestPostClass { StatusId = 1L }, - [2L] = new TestPostClass { StatusId = 2L, IsFav = true }, - [3L] = new TestPostClass { StatusId = 3L, IsFav = false, RetweetedId = 2L }, - }; + this.postGroup = new PostClassGroup( + new TestPostClass { StatusId = 1L }, + new TestPostClass { StatusId = 2L, IsFav = true }, + new TestPostClass { StatusId = 3L, IsFav = false, RetweetedId = 2L }); } [Fact] @@ -81,7 +101,7 @@ public void TextSingleLineTest(string text, string expected) [InlineData(2L, true)] [InlineData(3L, true)] public void GetIsFavTest(long statusId, bool expected) - => Assert.Equal(expected, PostClassTest.TestCases[statusId].IsFav); + => Assert.Equal(expected, this.postGroup[statusId].IsFav); [Theory] [InlineData(2L, true)] @@ -90,15 +110,16 @@ public void GetIsFavTest(long statusId, bool expected) [InlineData(3L, false)] public void SetIsFavTest(long statusId, bool isFav) { - var post = PostClassTest.TestCases[statusId]; + var post = this.postGroup[statusId]; post.IsFav = isFav; Assert.Equal(isFav, post.IsFav); if (post.RetweetedId != null) - Assert.Equal(isFav, PostClassTest.TestCases[post.RetweetedId.Value].IsFav); + Assert.Equal(isFav, this.postGroup[post.RetweetedId.Value].IsFav); } +#pragma warning disable SA1008 // Opening parenthesis should be spaced correctly [Theory] [InlineData(false, false, false, false, -0x01)] [InlineData( true, false, false, false, 0x00)] @@ -116,6 +137,7 @@ public void SetIsFavTest(long statusId, bool isFav) [InlineData( true, false, true, true, 0x0C)] [InlineData(false, true, true, true, 0x0D)] [InlineData( true, true, true, true, 0x0E)] +#pragma warning restore SA1008 public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int expected) { var post = new TestPostClass @@ -379,9 +401,9 @@ public void ConvertToOriginalPost_ErrorTest() Assert.Throws(() => post.ConvertToOriginalPost()); } - class FakeExpandedUrlInfo : PostClass.ExpandedUrlInfo + private class FakeExpandedUrlInfo : PostClass.ExpandedUrlInfo { - public TaskCompletionSource fakeResult = new TaskCompletionSource(); + public TaskCompletionSource FakeResult = new(); public FakeExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) : base(url, expandedUrl, deepExpand) @@ -389,12 +411,14 @@ public FakeExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) } protected override async Task DeepExpandAsync() - => this._expandedUrl = await this.fakeResult.Task; + => this.expandedUrl = await this.FakeResult.Task; } [Fact] public async Task ExpandedUrls_BasicScenario() { + PostClass.ExpandedUrlInfo.AutoExpand = true; + var post = new PostClass { Text = "bit.ly/abcde", @@ -402,7 +426,7 @@ public async Task ExpandedUrls_BasicScenario() { new FakeExpandedUrlInfo( // 展開前の t.co ドメインの URL - url: "http://t.co/aaaaaaa", + url: "http://t.co/aaaaaaa", // Entity の expanded_url に含まれる URL expandedUrl: "http://bit.ly/abcde", @@ -424,7 +448,7 @@ public async Task ExpandedUrls_BasicScenario() Assert.Equal("bit.ly/abcde", post.Text); // bit.ly 展開後の URL は「http://example.com/abcde」 - urlInfo.fakeResult.SetResult("http://example.com/abcde"); + urlInfo.FakeResult.SetResult("http://example.com/abcde"); await urlInfo.ExpandTask; // ExpandedUrlInfo による展開が完了した後の状態 diff --git a/OpenTween.Tests/Models/PostFilterRuleTest.cs b/OpenTween.Tests/Models/PostFilterRuleTest.cs index 4e9485b53..5c6846ad7 100644 --- a/OpenTween.Tests/Models/PostFilterRuleTest.cs +++ b/OpenTween.Tests/Models/PostFilterRuleTest.cs @@ -1438,7 +1438,8 @@ public void SetProperty_Test() var filter = new PostFilterRule(); Assert.PropertyChanged( - filter, "FilterName", + filter, + "FilterName", () => filter.FilterName = "hogehoge" ); @@ -1454,7 +1455,8 @@ public void SetProperty_SameValueTest() // 値に変化がないので PropertyChanged イベントは発生しない TestUtils.NotPropertyChanged( - filter, "FilterName", + filter, + "FilterName", () => filter.FilterName = "hogehoge" ); diff --git a/OpenTween.Tests/Models/PostFilterRuleVersion113DeserializeTest.cs b/OpenTween.Tests/Models/PostFilterRuleVersion113DeserializeTest.cs index 52f09cb53..ac928f126 100644 --- a/OpenTween.Tests/Models/PostFilterRuleVersion113DeserializeTest.cs +++ b/OpenTween.Tests/Models/PostFilterRuleVersion113DeserializeTest.cs @@ -40,24 +40,43 @@ public class PostFilterRuleVersion113DeserializeTest public sealed class FiltersClass { public string? NameFilter { get; set; } + public string? ExNameFilter { get; set; } + public string[] BodyFilterArray { get; set; } = Array.Empty(); + public string[] ExBodyFilterArray { get; set; } = Array.Empty(); + public bool SearchBoth { get; set; } + public bool ExSearchBoth { get; set; } + public bool MoveFrom { get; set; } + public bool SetMark { get; set; } + public bool SearchUrl { get; set; } + public bool ExSearchUrl { get; set; } + public bool CaseSensitive { get; set; } + public bool ExCaseSensitive { get; set; } + public bool UseLambda { get; set; } + public bool ExUseLambda { get; set; } + public bool UseRegex { get; set; } + public bool ExUseRegex { get; set; } + public bool IsRt { get; set; } + public bool IsExRt { get; set; } + public string? Source { get; set; } + public string? ExSource { get; set; } } diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index 8d1259f2a..4545169f6 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -38,7 +38,7 @@ public TabInformationTest() this.tabinfo = this.CreateInstance(); // TabInformation.GetInstance() で取得できるようにする - var field = typeof(TabInformations).GetField("_instance", + var field = typeof(TabInformations).GetField("Instance", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.SetField); field.SetValue(null, this.tabinfo); @@ -108,6 +108,67 @@ public void SelectTab_Test() public void SelectTab_NotExistTest() => Assert.Throws(() => this.tabinfo.SelectTab("INVALID")); + [Theory] + [InlineData(MyCommon.TabUsageType.Home, typeof(HomeTabModel))] + [InlineData(MyCommon.TabUsageType.Mentions, typeof(MentionsTabModel))] + [InlineData(MyCommon.TabUsageType.DirectMessage, typeof(DirectMessagesTabModel))] + [InlineData(MyCommon.TabUsageType.Favorites, typeof(FavoritesTabModel))] + [InlineData(MyCommon.TabUsageType.UserDefined, typeof(FilterTabModel))] + [InlineData(MyCommon.TabUsageType.UserTimeline, typeof(UserTimelineTabModel))] + [InlineData(MyCommon.TabUsageType.PublicSearch, typeof(PublicSearchTabModel))] + [InlineData(MyCommon.TabUsageType.Lists, typeof(ListTimelineTabModel))] + [InlineData(MyCommon.TabUsageType.Mute, typeof(MuteTabModel))] + public void CreateTabFromSettings_TabTypeTest(MyCommon.TabUsageType tabType, Type expected) + { + var tabSetting = new SettingTabs.SettingTabItem + { + TabName = "tab", + TabType = tabType, + }; + var tabinfo = this.CreateInstance(); + var tab = tabinfo.CreateTabFromSettings(tabSetting); + Assert.IsType(expected, tab); + } + + [Fact] + public void CreateTabFromSettings_FilterTabTest() + { + var tabSetting = new SettingTabs.SettingTabItem + { + TabName = "tab", + TabType = MyCommon.TabUsageType.UserDefined, + FilterArray = new PostFilterRule[] + { + new() { FilterName = "foo" }, + }, + }; + var tabinfo = this.CreateInstance(); + var tab = tabinfo.CreateTabFromSettings(tabSetting); + Assert.IsType(tab); + + var filterTab = (FilterTabModel)tab!; + Assert.Equal("foo", filterTab.FilterArray.First().FilterName); + } + + [Fact] + public void CreateTabFromSettings_PublicSearchTabTest() + { + var tabSetting = new SettingTabs.SettingTabItem + { + TabName = "tab", + TabType = MyCommon.TabUsageType.PublicSearch, + SearchWords = "foo", + SearchLang = "ja", + }; + var tabinfo = this.CreateInstance(); + var tab = tabinfo.CreateTabFromSettings(tabSetting); + Assert.IsType(tab); + + var searchTab = (PublicSearchTabModel)tab!; + Assert.Equal("foo", searchTab.SearchWords); + Assert.Equal("ja", searchTab.SearchLang); + } + [Fact] public void AddDefaultTabs_Test() { @@ -830,13 +891,76 @@ public void FilterAll_ExcludeReplyFilterTest() Assert.True(this.tabinfo[200L]!.IsExcludeReply); } - class TestPostFilterRule : PostFilterRule + [Fact] + public void RefreshOwl_HomeTabTest() + { + var post = new PostClass + { + StatusId = 100L, + ScreenName = "aaa", + UserId = 123L, + IsOwl = true, + }; + this.tabinfo.AddPost(post); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + var followerIds = new HashSet { 123L }; + this.tabinfo.RefreshOwl(followerIds); + + Assert.False(post.IsOwl); + } + + [Fact] + public void RefreshOwl_InnerStoregeTabTest() + { + var tab = new PublicSearchTabModel("search"); + this.tabinfo.AddTab(tab); + + var post = new PostClass + { + StatusId = 100L, + ScreenName = "aaa", + UserId = 123L, + IsOwl = true, + }; + tab.AddPostQueue(post); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + var followerIds = new HashSet { 123L }; + this.tabinfo.RefreshOwl(followerIds); + + Assert.False(post.IsOwl); + } + + [Fact] + public void RefreshOwl_UnfollowedTest() + { + var post = new PostClass + { + StatusId = 100L, + ScreenName = "aaa", + UserId = 123L, + IsOwl = false, + }; + this.tabinfo.AddPost(post); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + var followerIds = new HashSet { 456L }; + this.tabinfo.RefreshOwl(followerIds); + + Assert.True(post.IsOwl); + } + + private class TestPostFilterRule : PostFilterRule { public static PostFilterRule Create(Func filterDelegate) { return new TestPostFilterRule { - FilterDelegate = filterDelegate, + filterDelegate = filterDelegate, IsDirty = false, }; } diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index bf865fb44..2cf139d1a 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -32,6 +32,68 @@ namespace OpenTween.Models { public class TabModelTest { + [Fact] + public void AnchorPost_Test() + { + var tab = new PublicSearchTabModel("search"); + + var posts = new[] + { + new PostClass { StatusId = 100L }, + new PostClass { StatusId = 110L }, + }; + tab.AddPostQueue(posts[0]); + tab.AddPostQueue(posts[1]); + tab.AddSubmit(); + + Assert.Null(tab.AnchorStatusId); + Assert.Null(tab.AnchorPost); + + tab.AnchorPost = posts[1]; + + Assert.Equal(110L, tab.AnchorStatusId); + Assert.Equal(110L, tab.AnchorPost.StatusId); + } + + [Fact] + public void AnchorPost_DeletedTest() + { + var tab = new PublicSearchTabModel("search"); + + var posts = new[] + { + new PostClass { StatusId = 100L }, + }; + tab.AddPostQueue(posts[0]); + tab.AddSubmit(); + tab.AnchorPost = posts[0]; + + Assert.Equal(100L, tab.AnchorPost.StatusId); + + tab.EnqueueRemovePost(100L, setIsDeleted: true); + tab.RemoveSubmit(); + + Assert.Null(tab.AnchorPost); + } + + [Fact] + public void ClearAnchor_Test() + { + var tab = new PublicSearchTabModel("search"); + + var posts = new[] + { + new PostClass { StatusId = 100L }, + }; + tab.AddPostQueue(posts[0]); + tab.AddSubmit(); + tab.AnchorPost = posts[0]; + + Assert.Equal(100L, tab.AnchorPost.StatusId); + tab.ClearAnchor(); + Assert.Null(tab.AnchorPost); + } + [Fact] public void SelectPosts_Test() { diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs new file mode 100644 index 000000000..a62d8a347 --- /dev/null +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -0,0 +1,643 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2013 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using OpenTween.Api.DataModel; +using Xunit; + +namespace OpenTween.Models +{ + public class TwitterPostFactoryTest + { + private static readonly ISet EmptyIdSet = new HashSet(); + + private readonly Random random = new(); + + public TwitterPostFactoryTest() + => PostClass.ExpandedUrlInfo.AutoExpand = false; + + private TabInformations CreateTabinfo() + { + var tabinfo = new TabInformations(); + tabinfo.AddDefaultTabs(); + return tabinfo; + } + + private TwitterStatus CreateStatus() + { + var statusId = this.random.Next(10000); + + return new() + { + Id = statusId, + IdStr = statusId.ToString(), + CreatedAt = "Sat Jan 01 00:00:00 +0000 2022", + FullText = "hoge", + Source = "OpenTween", + Entities = new(), + User = this.CreateUser(), + }; + } + + private TwitterUser CreateUser() + { + var userId = this.random.Next(10000); + + return new() + { + Id = userId, + IdStr = userId.ToString(), + ScreenName = "tetete", + Name = "ててて", + ProfileImageUrlHttps = "https://example.com/profile.png", + }; + } + + [Fact] + public void CreateFromStatus_Test() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + var status = this.CreateStatus(); + var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet); + + Assert.Equal(status.Id, post.StatusId); + Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); + Assert.Equal("hoge", post.Text); + Assert.Equal("hoge", post.TextFromApi); + Assert.Equal("hoge", post.TextSingleLine); + Assert.Equal("hoge", post.AccessibleText); + Assert.Empty(post.ReplyToList); + Assert.Empty(post.QuoteStatusIds); + Assert.Empty(post.ExpandedUrls); + Assert.Empty(post.Media); + Assert.Null(post.PostGeo); + Assert.Equal("OpenTween", post.Source); + Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); + Assert.Equal(0, post.FavoritedCount); + Assert.False(post.IsFav); + Assert.False(post.IsDm); + Assert.False(post.IsDeleted); + Assert.False(post.IsRead); + Assert.False(post.IsExcludeReply); + Assert.False(post.FilterHit); + Assert.False(post.IsMark); + + Assert.False(post.IsReply); + Assert.Null(post.InReplyToStatusId); + Assert.Null(post.InReplyToUserId); + Assert.Null(post.InReplyToUser); + + Assert.Null(post.RetweetedId); + Assert.Null(post.RetweetedBy); + Assert.Null(post.RetweetedByUserId); + + Assert.Equal(status.User.Id, post.UserId); + Assert.Equal("tetete", post.ScreenName); + Assert.Equal("ててて", post.Nickname); + Assert.Equal("https://example.com/profile.png", post.ImageUrl); + Assert.False(post.IsProtect); + Assert.False(post.IsOwl); + Assert.False(post.IsMe); + } + + [Fact] + public void CreateFromStatus_AuthorTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + var status = this.CreateStatus(); + var selfUserId = status.User.Id; + var post = factory.CreateFromStatus(status, selfUserId, followerIds: EmptyIdSet); + + Assert.True(post.IsMe); + } + + [Fact] + public void CreateFromStatus_FollowerTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + var status = this.CreateStatus(); + var followerIds = new HashSet { status.User.Id }; + var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); + + Assert.False(post.IsOwl); + } + + [Fact] + public void CreateFromStatus_NotFollowerTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + var status = this.CreateStatus(); + var followerIds = new HashSet { 30000L }; + var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); + + Assert.True(post.IsOwl); + } + + [Fact] + public void CreateFromStatus_RetweetTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + var originalStatus = this.CreateStatus(); + + var retweetStatus = this.CreateStatus(); + retweetStatus.RetweetedStatus = originalStatus; + retweetStatus.Source = "Twitter Web App"; + + var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); + + Assert.Equal(retweetStatus.Id, post.StatusId); + Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId); + Assert.Equal(originalStatus.Id, post.RetweetedId); + Assert.Equal(originalStatus.User.Id, post.UserId); + + Assert.Equal("OpenTween", post.Source); + Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); + } + + private TwitterMessageEvent CreateDirectMessage(string senderId, string recipientId) + { + var messageId = this.random.Next(10000); + + return new() + { + Type = "message_create", + Id = messageId.ToString(), + CreatedTimestamp = "1640995200000", + MessageCreate = new() + { + SenderId = senderId, + Target = new() + { + RecipientId = recipientId, + }, + MessageData = new() + { + Text = "hoge", + Entities = new(), + }, + SourceAppId = "22519141", + }, + }; + } + + private Dictionary CreateApps() + { + return new() + { + ["22519141"] = new() + { + Id = "22519141", + Name = "OpenTween", + Url = "https://www.opentween.org/", + }, + }; + } + + [Fact] + public void CreateFromDirectMessageEvent_Test() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var selfUser = this.CreateUser(); + var otherUser = this.CreateUser(); + var eventItem = this.CreateDirectMessage(senderId: otherUser.IdStr, recipientId: selfUser.IdStr); + var users = new Dictionary() + { + [selfUser.IdStr] = selfUser, + [otherUser.IdStr] = otherUser, + }; + var apps = this.CreateApps(); + var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); + + Assert.Equal(long.Parse(eventItem.Id), post.StatusId); + Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); + Assert.Equal("hoge", post.Text); + Assert.Equal("hoge", post.TextFromApi); + Assert.Equal("hoge", post.TextSingleLine); + Assert.Equal("hoge", post.AccessibleText); + Assert.Empty(post.ReplyToList); + Assert.Empty(post.QuoteStatusIds); + Assert.Empty(post.ExpandedUrls); + Assert.Empty(post.Media); + Assert.Null(post.PostGeo); + Assert.Equal("OpenTween", post.Source); + Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); + Assert.Equal(0, post.FavoritedCount); + Assert.False(post.IsFav); + Assert.True(post.IsDm); + Assert.False(post.IsDeleted); + Assert.False(post.IsRead); + Assert.False(post.IsExcludeReply); + Assert.False(post.FilterHit); + Assert.False(post.IsMark); + + Assert.False(post.IsReply); + Assert.Null(post.InReplyToStatusId); + Assert.Null(post.InReplyToUserId); + Assert.Null(post.InReplyToUser); + + Assert.Null(post.RetweetedId); + Assert.Null(post.RetweetedBy); + Assert.Null(post.RetweetedByUserId); + + Assert.Equal(otherUser.Id, post.UserId); + Assert.Equal("tetete", post.ScreenName); + Assert.Equal("ててて", post.Nickname); + Assert.Equal("https://example.com/profile.png", post.ImageUrl); + Assert.False(post.IsProtect); + Assert.True(post.IsOwl); + Assert.False(post.IsMe); + } + + [Fact] + public void CreateFromDirectMessageEvent_SenderTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var selfUser = this.CreateUser(); + var otherUser = this.CreateUser(); + var eventItem = this.CreateDirectMessage(senderId: selfUser.IdStr, recipientId: otherUser.IdStr); + var users = new Dictionary() + { + [selfUser.IdStr] = selfUser, + [otherUser.IdStr] = otherUser, + }; + var apps = this.CreateApps(); + var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); + + Assert.Equal(otherUser.Id, post.UserId); + Assert.False(post.IsOwl); + Assert.True(post.IsMe); + } + + [Fact] + public void CreateFromStatus_MediaAltTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var status = this.CreateStatus(); + status.FullText = "https://t.co/hoge"; + status.ExtendedEntities = new() + { + Media = new[] + { + new TwitterEntityMedia + { + Indices = new[] { 0, 17 }, + Url = "https://t.co/hoge", + DisplayUrl = "pic.twitter.com/hoge", + ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", + AltText = "代替テキスト", + }, + }, + }; + + var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + + var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); + Assert.Equal(accessibleText, post.AccessibleText); + Assert.Equal("pic.twitter.com/hoge", post.Text); + Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); + Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); + } + + [Fact] + public void CreateFromStatus_MediaNoAltTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var status = this.CreateStatus(); + status.FullText = "https://t.co/hoge"; + status.ExtendedEntities = new() + { + Media = new[] + { + new TwitterEntityMedia + { + Indices = new[] { 0, 17 }, + Url = "https://t.co/hoge", + DisplayUrl = "pic.twitter.com/hoge", + ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", + AltText = null, + }, + }, + }; + + var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + + Assert.Equal("pic.twitter.com/hoge", post.AccessibleText); + Assert.Equal("pic.twitter.com/hoge", post.Text); + Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); + Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); + } + + [Fact] + public void CreateFromStatus_QuotedUrlTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var status = this.CreateStatus(); + status.FullText = "https://t.co/hoge"; + status.Entities = new() + { + Urls = new[] + { + new TwitterEntityUrl + { + Indices = new[] { 0, 17 }, + Url = "https://t.co/hoge", + DisplayUrl = "twitter.com/hoge/status/1…", + ExpandedUrl = "https://twitter.com/hoge/status/1234567890", + }, + }, + }; + status.QuotedStatus = new() + { + Id = 1234567890L, + IdStr = "1234567890", + User = new() + { + Id = 1111, + IdStr = "1111", + ScreenName = "foo", + }, + FullText = "test", + }; + + var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + + var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); + Assert.Equal(accessibleText, post.AccessibleText); + Assert.Equal("twitter.com/hoge/status/1…", post.Text); + Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); + Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); + } + + [Fact] + public void CreateFromStatus_QuotedUrlWithPermelinkTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var status = this.CreateStatus(); + status.FullText = "hoge"; + status.QuotedStatus = new() + { + Id = 1234567890L, + IdStr = "1234567890", + User = new TwitterUser + { + Id = 1111, + IdStr = "1111", + ScreenName = "foo", + }, + FullText = "test", + }; + status.QuotedStatusPermalink = new() + { + Url = "https://t.co/hoge", + Display = "twitter.com/hoge/status/1…", + Expanded = "https://twitter.com/hoge/status/1234567890", + }; + + var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + + var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); + Assert.Equal(accessibleText, post.AccessibleText); + Assert.Equal("hoge twitter.com/hoge/status/1…", post.Text); + Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextFromApi); + Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextSingleLine); + } + + [Fact] + public void CreateFromStatus_QuotedUrlNoReferenceTest() + { + var factory = new TwitterPostFactory(this.CreateTabinfo()); + + var status = this.CreateStatus(); + status.FullText = "https://t.co/hoge"; + status.Entities = new() + { + Urls = new[] + { + new TwitterEntityUrl + { + Indices = new[] { 0, 17 }, + Url = "https://t.co/hoge", + DisplayUrl = "twitter.com/hoge/status/1…", + ExpandedUrl = "https://twitter.com/hoge/status/1234567890", + }, + }, + }; + status.QuotedStatus = null; + + var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); + + var accessibleText = "twitter.com/hoge/status/1…"; + Assert.Equal(accessibleText, post.AccessibleText); + Assert.Equal("twitter.com/hoge/status/1…", post.Text); + Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); + Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); + } + + [Fact] + public void CreateHtmlAnchor_Test() + { + var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3"; + var entities = new TwitterEntities + { + UserMentions = new[] + { + new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" }, + }, + Hashtags = new[] + { + new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" }, + }, + Urls = new[] + { + new TwitterEntityUrl + { + Indices = new[] { 31, 54 }, + Url = "https://t.co/mIJcSoVSK3", + DisplayUrl = "apps-of-a-feather.com", + ExpandedUrl = "http://apps-of-a-feather.com/", + }, + }, + }; + + var expectedHtml = @"@twitterapi" + + @" #BreakingMyTwitter" + + @" apps-of-a-feather.com"; + + Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); + } + + [Fact] + public void CreateHtmlAnchor_NicovideoTest() + { + var text = "sm9"; + var entities = new TwitterEntities(); + + var expectedHtml = @"sm9"; + + Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); + } + + [Fact] + public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() + { + var text = "hoge"; + var entities = new TwitterEntities(); + var quotedStatusLink = new TwitterQuotedStatusPermalink + { + Url = "https://t.co/hoge", + Display = "twitter.com/hoge/status/1…", + Expanded = "https://twitter.com/hoge/status/1234567890", + }; + + var expectedHtml = @"hoge" + + @" twitter.com/hoge/status/1…"; + + Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink)); + } + + [Fact] + public void ParseSource_Test() + { + var sourceHtml = "Twitter Web Client"; + + var expected = ("Twitter Web Client", new Uri("http://twitter.com/")); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_PlainTextTest() + { + var sourceHtml = "web"; + + var expected = ("web", (Uri?)null); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_RelativeUriTest() + { + // 参照: https://twitter.com/kim_upsilon/status/477796052049752064 + var sourceHtml = "erased_45416"; + + var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416")); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_EmptyTest() + { + // 参照: https://twitter.com/kim_upsilon/status/595156014032244738 + var sourceHtml = ""; + + var expected = ("", (Uri?)null); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_NullTest() + { + string? sourceHtml = null; + + var expected = ("", (Uri?)null); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_UnescapeTest() + { + var sourceHtml = "<<hogehoge>>"; + + var expected = ("<>", new Uri("http://example.com/?aaa=123&bbb=456")); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void ParseSource_UnescapeNoUriTest() + { + var sourceHtml = "<<hogehoge>>"; + + var expected = ("<>", (Uri?)null); + Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); + } + + [Fact] + public void GetQuoteTweetStatusIds_EntityTest() + { + var entities = new[] + { + new TwitterEntityUrl + { + Url = "https://t.co/3HXq0LrbJb", + ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640", + }, + }; + + var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink: null); + Assert.Equal(new[] { 599261132361072640L }, statusIds); + } + + [Fact] + public void GetQuoteTweetStatusIds_QuotedStatusLinkTest() + { + var entities = new TwitterEntities(); + var quotedStatusLink = new TwitterQuotedStatusPermalink + { + Url = "https://t.co/3HXq0LrbJb", + Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640", + }; + + var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink); + Assert.Equal(new[] { 599261132361072640L }, statusIds); + } + + [Fact] + public void GetQuoteTweetStatusIds_UrlStringTest() + { + var urls = new[] + { + "https://twitter.com/kim_upsilon/status/599261132361072640", + }; + + var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls); + Assert.Equal(new[] { 599261132361072640L }, statusIds); + } + + [Fact] + public void GetQuoteTweetStatusIds_OverflowTest() + { + var urls = new[] + { + // 符号付き 64 ビット整数の範囲を超える値 + "https://twitter.com/kim_upsilon/status/9999999999999999999", + }; + + var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls); + Assert.Empty(statusIds); + } + } +} diff --git a/OpenTween.Tests/MyApplicationTest.cs b/OpenTween.Tests/MyApplicationTest.cs deleted file mode 100644 index 7457f52c4..000000000 --- a/OpenTween.Tests/MyApplicationTest.cs +++ /dev/null @@ -1,122 +0,0 @@ -// OpenTween - Client of Twitter -// Copyright (c) 2013 kim_upsilon (@kim_upsilon) -// All rights reserved. -// -// This file is part of OpenTween. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see , or write to -// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, -// Boston, MA 02110-1301, USA. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using Xunit; -using Xunit.Extensions; - -namespace OpenTween -{ - public class MyApplicationTest - { - [Fact] - public void ParseArguments_NoOptionsTest() - { - var args = new string[] { }; - - Assert.Empty(MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_SingleOptionTest() - { - var args = new[] { "/foo" }; - - Assert.Equal(new Dictionary - { - ["foo"] = "", - }, - MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_MultipleOptionsTest() - { - var args = new[] { "/foo", "/bar" }; - - Assert.Equal(new Dictionary - { - ["foo"] = "", - ["bar"] = "", - }, - MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_OptionWithArgumentTest() - { - var args = new[] { "/foo:hogehoge" }; - - Assert.Equal(new Dictionary - { - ["foo"] = "hogehoge", - }, - MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_OptionWithEmptyArgumentTest() - { - var args = new[] { "/foo:" }; - - Assert.Equal(new Dictionary - { - ["foo"] = "", - }, - MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_IgroreInvalidOptionsTest() - { - var args = new string[] { "--foo", "/" }; - - Assert.Empty(MyApplication.ParseArguments(args)); - } - - [Fact] - public void ParseArguments_DuplicateOptionsTest() - { - var args = new[] { "/foo:abc", "/foo:123" }; - - Assert.Equal(new Dictionary - { - ["foo"] = "123", - }, - MyApplication.ParseArguments(args)); - } - - [Theory] - [InlineData("ja-JP", "ja-JP")] - [InlineData("fr-FR", "en")] // 対応するカルチャが無い場合は en にフォールバックする - [InlineData("zh-CN", "en")] // zh-CHS リソースは v1.3.7 から削除 - [InlineData("zh-TW", "en")] - public void GetPreferredCulture_Test(string currentCulture, string expectedCulture) - { - var actual = MyApplication.GetPreferredCulture(new CultureInfo(currentCulture)); - Assert.Equal(expectedCulture, actual.Name); - } - } -} diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index 6c04b36af..dff7e0a92 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -104,24 +104,28 @@ public void ResizeBytesArrayTest(byte[] bytes, int size, byte[] expected) public void IsAnimatedGifTest(string filename, bool expected) => Assert.Equal(expected, MyCommon.IsAnimatedGif(filename)); - public static readonly TheoryData DateTimeParse_TestCase = new TheoryData + public static readonly TheoryData DateTimeParseTestCase = new() { { "Sun Nov 25 06:10:00 +00:00 2012", new DateTimeUtc(2012, 11, 25, 6, 10, 0) }, { "Sun, 25 Nov 2012 06:10:00 +00:00", new DateTimeUtc(2012, 11, 25, 6, 10, 0) }, }; [Theory] - [MemberData(nameof(DateTimeParse_TestCase))] + [MemberData(nameof(DateTimeParseTestCase))] public void DateTimeParseTest(string date, DateTimeUtc excepted) => Assert.Equal(excepted, MyCommon.DateTimeParse(date)); [DataContract] public struct JsonData { - [DataMember(Name = "id")] public string Id { get; set; } - [DataMember(Name = "body")] public string Body { get; set; } + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "body")] + public string Body { get; set; } } - public static readonly TheoryData CreateDataFromJson_TestCase = new TheoryData + + public static readonly TheoryData CreateDataFromJsonTestCase = new() { { @"{""id"":""1"", ""body"":""hogehoge""}", @@ -130,7 +134,7 @@ public struct JsonData }; [Theory] - [MemberData(nameof(CreateDataFromJson_TestCase))] + [MemberData(nameof(CreateDataFromJsonTestCase))] public void CreateDataFromJsonTest(string json, T expected) => Assert.Equal(expected, MyCommon.CreateDataFromJson(json)); @@ -152,7 +156,7 @@ public void IsValidEmailTest(string email, bool expected) [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Control, Keys.Alt }, true)] [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Shift }, false)] public void IsKeyDownTest(Keys modifierKeys, Keys[] checkKeys, bool expected) - => Assert.Equal(expected, MyCommon._IsKeyDown(modifierKeys, checkKeys)); + => Assert.Equal(expected, MyCommon.IsKeyDownInternal(modifierKeys, checkKeys)); [Fact] public void GetAssemblyNameTest() @@ -182,20 +186,20 @@ public void ReplaceAppNameTest(string str, string excepted) public void GetReadableVersionTest(string fileVersion, string expected) => Assert.Equal(expected, MyCommon.GetReadableVersion(fileVersion)); - public static readonly TheoryData GetStatusUrlTest1_TestCase = new TheoryData + public static readonly TheoryData GetStatusUrlTest1TestCase = new() { { new PostClass { StatusId = 249493863826350080L, ScreenName = "Favstar_LM", RetweetedId = null, RetweetedBy = null }, "https://twitter.com/Favstar_LM/status/249493863826350080" }, { - new PostClass { StatusId = 216033842434289664L, ScreenName = "haru067", RetweetedId = 200245741443235840L, RetweetedBy = "re4k"}, + new PostClass { StatusId = 216033842434289664L, ScreenName = "haru067", RetweetedId = 200245741443235840L, RetweetedBy = "re4k" }, "https://twitter.com/haru067/status/200245741443235840" }, }; [Theory] - [MemberData(nameof(GetStatusUrlTest1_TestCase))] + [MemberData(nameof(GetStatusUrlTest1TestCase))] public void GetStatusUrlTest1(PostClass post, string expected) => Assert.Equal(expected, MyCommon.GetStatusUrl(post)); @@ -309,7 +313,7 @@ public void CircularCountDown_StartFromLastIndexTest() [Fact] public void CreateBrowserProcessStartInfo_DefaultBrowserTest() { - var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPathWithArgs: null, "https://example.com/"); + var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPathWithArgs: null, "https://example.com/"); Assert.Equal("https://example.com/", startInfo.FileName); Assert.Equal("", startInfo.Arguments); Assert.True(startInfo.UseShellExecute); @@ -350,5 +354,28 @@ public void CreateBrowserProcessStartInfo_QuotedBrowserPathWithArgsTest() Assert.Equal("/hoge \"https://example.com/\"", startInfo.Arguments); Assert.False(startInfo.UseShellExecute); } + + public static readonly TheoryData ToRangeChunkTestCase = new() + { + { + new[] { 1 }, + new[] { (1, 1) } + }, + { + new[] { 1, 2 }, + new[] { (1, 2) } + }, + { + new[] { 1, 3 }, + new[] { (1, 1), (3, 3) } + }, + }; + + [Theory] + [MemberData(nameof(ToRangeChunkTestCase))] + public void ToRangeChunk_Test(int[] values, (int Start, int End)[] expected) + { + Assert.Equal(expected, MyCommon.ToRangeChunk(values)); + } } } diff --git a/OpenTween.Tests/OTBaseFormTest.cs b/OpenTween.Tests/OTBaseFormTest.cs index 5186a26aa..d8ded2720 100644 --- a/OpenTween.Tests/OTBaseFormTest.cs +++ b/OpenTween.Tests/OTBaseFormTest.cs @@ -32,7 +32,9 @@ namespace OpenTween { public class OTBaseFormTest { - private class TestForm : OTBaseForm { } + private class TestForm : OTBaseForm + { + } public OTBaseFormTest() => this.SetupSynchronizationContext(); @@ -40,100 +42,29 @@ public OTBaseFormTest() protected void SetupSynchronizationContext() => WindowsFormsSynchronizationContext.AutoInstall = false; - [Fact] - public async Task InvokeAsync_Test() - { - using (var form = new TestForm()) - { - await Task.Run(async () => - { - await form.InvokeAsync(() => form.Text = "hoge"); - }); - - Assert.Equal("hoge", form.Text); - } - } - - [Fact] - public async Task InvokeAsync_ReturnValueTest() - { - using (var form = new TestForm()) - { - form.Text = "hoge"; - - await Task.Run(async () => - { - var ret = await form.InvokeAsync(() => form.Text); - Assert.Equal("hoge", ret); - }); - } - } - - [Fact] - public async Task InvokeAsync_TaskTest() - { - using (var form = new TestForm()) - { - await Task.Run(async () => - { - await form.InvokeAsync(async () => - { - await Task.Delay(1); - form.Text = "hoge"; - }); - }); - - Assert.Equal("hoge", form.Text); - } - } - - [Fact] - public async Task InvokeAsync_TaskWithValueTest() - { - using (var form = new TestForm()) - { - form.Text = "hoge"; - - await Task.Run(async () => - { - var ret = await form.InvokeAsync(async () => - { - await Task.Delay(1); - return form.Text; - }); - - Assert.Equal("hoge", ret); - }); - } - } - [Fact] public void ScaleChildControl_ListViewTest() { - using (var listview = new ListView { Width = 200, Height = 200 }) + using var listview = new ListView { Width = 200, Height = 200 }; + listview.Columns.AddRange(new[] { - listview.Columns.AddRange(new[] - { - new ColumnHeader { Width = 60 }, - new ColumnHeader { Width = 140 }, - }); + new ColumnHeader { Width = 60 }, + new ColumnHeader { Width = 140 }, + }); - OTBaseForm.ScaleChildControl(listview, new SizeF(1.25f, 1.25f)); + OTBaseForm.ScaleChildControl(listview, new SizeF(1.25f, 1.25f)); - Assert.Equal(75, listview.Columns[0].Width); - Assert.Equal(175, listview.Columns[1].Width); - } + Assert.Equal(75, listview.Columns[0].Width); + Assert.Equal(175, listview.Columns[1].Width); } [Fact] public void ScaleChildControl_VScrollBarTest() { - using (var scrollBar = new VScrollBar { Width = 20, Height = 200 }) - { - OTBaseForm.ScaleChildControl(scrollBar, new SizeF(2.0f, 2.0f)); + using var scrollBar = new VScrollBar { Width = 20, Height = 200 }; + OTBaseForm.ScaleChildControl(scrollBar, new SizeF(2.0f, 2.0f)); - Assert.Equal(40, scrollBar.Width); - } + Assert.Equal(40, scrollBar.Width); } } } diff --git a/OpenTween.Tests/OTPictureBoxTest.cs b/OpenTween.Tests/OTPictureBoxTest.cs index 2d154a793..6afa4f043 100644 --- a/OpenTween.Tests/OTPictureBoxTest.cs +++ b/OpenTween.Tests/OTPictureBoxTest.cs @@ -32,100 +32,90 @@ public class OTPictureBoxTest [Fact] public void SizeMode_SetterGetterTest() { - using (var picbox = new OTPictureBox()) - { - picbox.SizeMode = PictureBoxSizeMode.Zoom; + using var picbox = new OTPictureBox(); + picbox.SizeMode = PictureBoxSizeMode.Zoom; - Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); - Assert.Equal(PictureBoxSizeMode.Zoom, ((PictureBox)picbox).SizeMode); - } + Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); + Assert.Equal(PictureBoxSizeMode.Zoom, ((PictureBox)picbox).SizeMode); } [Fact] public void SizeMode_ErrorImageTest() { - using (var picbox = new OTPictureBox()) - { - picbox.SizeMode = PictureBoxSizeMode.Zoom; + using var picbox = new OTPictureBox(); + picbox.SizeMode = PictureBoxSizeMode.Zoom; - picbox.ShowErrorImage(); + picbox.ShowErrorImage(); - Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); - Assert.Equal(PictureBoxSizeMode.CenterImage, ((PictureBox)picbox).SizeMode); - } + Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); + Assert.Equal(PictureBoxSizeMode.CenterImage, ((PictureBox)picbox).SizeMode); } [Fact] public void SizeMode_ErrorImageTest2() { - using (var picbox = new OTPictureBox()) - { - picbox.ShowErrorImage(); + using var picbox = new OTPictureBox(); + picbox.ShowErrorImage(); - picbox.SizeMode = PictureBoxSizeMode.Zoom; + picbox.SizeMode = PictureBoxSizeMode.Zoom; - Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); - Assert.Equal(PictureBoxSizeMode.CenterImage, ((PictureBox)picbox).SizeMode); - } + Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); + Assert.Equal(PictureBoxSizeMode.CenterImage, ((PictureBox)picbox).SizeMode); } [Fact] public void SizeMode_RestoreTest() { - using (var picbox = new OTPictureBox()) - { - picbox.SizeMode = PictureBoxSizeMode.Zoom; + using var picbox = new OTPictureBox(); + picbox.SizeMode = PictureBoxSizeMode.Zoom; - picbox.ShowErrorImage(); + picbox.ShowErrorImage(); - picbox.Image = TestUtils.CreateDummyImage(); + picbox.Image = TestUtils.CreateDummyImage(); - Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); - Assert.Equal(PictureBoxSizeMode.Zoom, ((PictureBox)picbox).SizeMode); - } + Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); + Assert.Equal(PictureBoxSizeMode.Zoom, ((PictureBox)picbox).SizeMode); } [Fact] public async Task SetImageFromAsync_Test() { - using (var picbox = new OTPictureBox()) - { - // Mono でのテスト実行時にデッドロックする問題の対策 - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + using var picbox = new OTPictureBox(); - var tcs = new TaskCompletionSource(); + // Mono でのテスト実行時にデッドロックする問題の対策 + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - var setImageTask = picbox.SetImageFromTask(() => tcs.Task); + var tcs = new TaskCompletionSource(); - Assert.Equal(picbox.InitialImage, ((PictureBox)picbox).Image); + var setImageTask = picbox.SetImageFromTask(() => tcs.Task); - var image = TestUtils.CreateDummyImage(); - tcs.SetResult(image); - await setImageTask; + Assert.Equal(picbox.InitialImage, ((PictureBox)picbox).Image); - Assert.Equal(image, picbox.Image); - } + var image = TestUtils.CreateDummyImage(); + tcs.SetResult(image); + await setImageTask; + + Assert.Equal(image, picbox.Image); } [Fact] public async Task SetImageFromAsync_ErrorTest() { - using (var picbox = new OTPictureBox()) - { - // Mono でのテスト実行時にデッドロックする問題の対策 - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + using var picbox = new OTPictureBox(); + + // Mono でのテスト実行時にデッドロックする問題の対策 + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - var setImageTask = picbox.SetImageFromTask(() => tcs.Task); + var setImageTask = picbox.SetImageFromTask(() => tcs.Task); - Assert.Equal(picbox.InitialImage, ((PictureBox)picbox).Image); + Assert.Equal(picbox.InitialImage, ((PictureBox)picbox).Image); - tcs.SetException(new InvalidImageException()); - await setImageTask; + tcs.SetException(new InvalidImageException()); + await setImageTask; - Assert.Equal(picbox.ErrorImage, ((PictureBox)picbox).Image); - } + Assert.Equal(picbox.ErrorImage, ((PictureBox)picbox).Image); } } } diff --git a/OpenTween.Tests/OTSplitContainerTest.cs b/OpenTween.Tests/OTSplitContainerTest.cs index b70844388..c196dc671 100644 --- a/OpenTween.Tests/OTSplitContainerTest.cs +++ b/OpenTween.Tests/OTSplitContainerTest.cs @@ -35,255 +35,234 @@ public class OTSplitContainerTest [Fact] public void IsPanelInvertedGetter_Test() { - using (var splitContainer = new OTSplitContainer()) - { - Assert.False(splitContainer.IsPanelInverted); // デフォルト値 + using var splitContainer = new OTSplitContainer(); + Assert.False(splitContainer.IsPanelInverted); // デフォルト値 - splitContainer.IsPanelInverted = true; - Assert.True(splitContainer.IsPanelInverted); + splitContainer.IsPanelInverted = true; + Assert.True(splitContainer.IsPanelInverted); - splitContainer.IsPanelInverted = false; - Assert.False(splitContainer.IsPanelInverted); - } + splitContainer.IsPanelInverted = false; + Assert.False(splitContainer.IsPanelInverted); } [Fact] public void IsPanelInvertedSetter_InnerControlsTest() { - using (var splitContainer = new OTSplitContainer()) - using (var buttonA = new Button()) - using (var buttonB = new Button()) - using (var buttonC = new Button()) - using (var buttonD = new Button()) - { - splitContainer.Panel1.Controls.AddRange(new[] { buttonA, buttonB }); - splitContainer.Panel2.Controls.AddRange(new[] { buttonC, buttonD }); - - var baseSplitContainer = (SplitContainer)splitContainer; - - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel1.Controls.Cast()); - Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel2.Controls.Cast()); - - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - - // Panel1, Panel2 内のコントロールが入れ替わる - Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel1.Controls.Cast()); - Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel2.Controls.Cast()); - - // 元に戻す - splitContainer.IsPanelInverted = false; - - // Panel1, Panel2 内のコントロールも元に戻る - Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel1.Controls.Cast()); - Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel2.Controls.Cast()); - } + using var splitContainer = new OTSplitContainer(); + using var buttonA = new Button(); + using var buttonB = new Button(); + using var buttonC = new Button(); + using var buttonD = new Button(); + + splitContainer.Panel1.Controls.AddRange(new[] { buttonA, buttonB }); + splitContainer.Panel2.Controls.AddRange(new[] { buttonC, buttonD }); + + var baseSplitContainer = (SplitContainer)splitContainer; + + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel1.Controls.Cast()); + Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel2.Controls.Cast()); + + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + + // Panel1, Panel2 内のコントロールが入れ替わる + Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel1.Controls.Cast()); + Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel2.Controls.Cast()); + + // 元に戻す + splitContainer.IsPanelInverted = false; + + // Panel1, Panel2 内のコントロールも元に戻る + Assert.Equal(new[] { buttonA, buttonB }, baseSplitContainer.Panel1.Controls.Cast()); + Assert.Equal(new[] { buttonC, buttonD }, baseSplitContainer.Panel2.Controls.Cast()); } [Fact] public void IsPanelInvertedSetter_Panel1FixedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.FixedPanel = FixedPanel.Panel1; + using var splitContainer = new OTSplitContainer(); + splitContainer.FixedPanel = FixedPanel.Panel1; - var baseSplitContainer = (SplitContainer)splitContainer; + var baseSplitContainer = (SplitContainer)splitContainer; - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); - } + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); } [Fact] public void IsPanelInvertedSetter_Panel2FixedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.FixedPanel = FixedPanel.Panel2; + using var splitContainer = new OTSplitContainer(); + splitContainer.FixedPanel = FixedPanel.Panel2; - var baseSplitContainer = (SplitContainer)splitContainer; + var baseSplitContainer = (SplitContainer)splitContainer; - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(FixedPanel.Panel1, baseSplitContainer.FixedPanel); - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); - } + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(FixedPanel.Panel2, baseSplitContainer.FixedPanel); } [Fact] public void IsPanelInvertedSetter_NoneFixedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.FixedPanel = FixedPanel.None; + using var splitContainer = new OTSplitContainer(); + splitContainer.FixedPanel = FixedPanel.None; - var baseSplitContainer = (SplitContainer)splitContainer; + var baseSplitContainer = (SplitContainer)splitContainer; - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); - } + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(FixedPanel.None, baseSplitContainer.FixedPanel); } [Fact] public void IsPanelInvertedSetter_PanelMinSizeTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Panel1MinSize = 200; - splitContainer.Panel2MinSize = 300; - - var baseSplitContainer = (SplitContainer)splitContainer; - - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(200, baseSplitContainer.Panel1MinSize); - Assert.Equal(300, baseSplitContainer.Panel2MinSize); - - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(300, baseSplitContainer.Panel1MinSize); - Assert.Equal(200, baseSplitContainer.Panel2MinSize); - - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(200, baseSplitContainer.Panel1MinSize); - Assert.Equal(300, baseSplitContainer.Panel2MinSize); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Panel1MinSize = 200; + splitContainer.Panel2MinSize = 300; + + var baseSplitContainer = (SplitContainer)splitContainer; + + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(200, baseSplitContainer.Panel1MinSize); + Assert.Equal(300, baseSplitContainer.Panel2MinSize); + + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(300, baseSplitContainer.Panel1MinSize); + Assert.Equal(200, baseSplitContainer.Panel2MinSize); + + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(200, baseSplitContainer.Panel1MinSize); + Assert.Equal(300, baseSplitContainer.Panel2MinSize); } [Fact] public void IsPanelInvertedSetter_Panel1CollapsedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.Panel1Collapsed = true; - - var baseSplitContainer = (SplitContainer)splitContainer; - - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.True(baseSplitContainer.Panel1Collapsed); - Assert.False(baseSplitContainer.Panel2Collapsed); - - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.False(baseSplitContainer.Panel1Collapsed); - Assert.True(baseSplitContainer.Panel2Collapsed); - - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.True(baseSplitContainer.Panel1Collapsed); - Assert.False(baseSplitContainer.Panel2Collapsed); - } + using var splitContainer = new OTSplitContainer(); + splitContainer.Panel1Collapsed = true; + + var baseSplitContainer = (SplitContainer)splitContainer; + + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.True(baseSplitContainer.Panel1Collapsed); + Assert.False(baseSplitContainer.Panel2Collapsed); + + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.False(baseSplitContainer.Panel1Collapsed); + Assert.True(baseSplitContainer.Panel2Collapsed); + + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.True(baseSplitContainer.Panel1Collapsed); + Assert.False(baseSplitContainer.Panel2Collapsed); } [Fact] public void IsPanelInvertedSetter_Panel2CollapsedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.Panel2Collapsed = true; - - var baseSplitContainer = (SplitContainer)splitContainer; - - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.False(baseSplitContainer.Panel1Collapsed); - Assert.True(baseSplitContainer.Panel2Collapsed); - - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.True(baseSplitContainer.Panel1Collapsed); - Assert.False(baseSplitContainer.Panel2Collapsed); - - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.False(baseSplitContainer.Panel1Collapsed); - Assert.True(baseSplitContainer.Panel2Collapsed); - } + using var splitContainer = new OTSplitContainer(); + splitContainer.Panel2Collapsed = true; + + var baseSplitContainer = (SplitContainer)splitContainer; + + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.False(baseSplitContainer.Panel1Collapsed); + Assert.True(baseSplitContainer.Panel2Collapsed); + + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.True(baseSplitContainer.Panel1Collapsed); + Assert.False(baseSplitContainer.Panel2Collapsed); + + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.False(baseSplitContainer.Panel1Collapsed); + Assert.True(baseSplitContainer.Panel2Collapsed); } [Fact] public void IsPanelInvertedSetter_SplitterDistanceHorizontalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Horizontal; // 上下に分割された状態 - splitContainer.SplitterWidth = 5; // 分割線の幅は 5px - splitContainer.SplitterDistance = 500; // 上から 500px で分割 (下から 300px - 5px) + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Horizontal; // 上下に分割された状態 + splitContainer.SplitterWidth = 5; // 分割線の幅は 5px + splitContainer.SplitterDistance = 500; // 上から 500px で分割 (下から 300px - 5px) - var baseSplitContainer = (SplitContainer)splitContainer; + var baseSplitContainer = (SplitContainer)splitContainer; - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(500, baseSplitContainer.SplitterDistance); + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(500, baseSplitContainer.SplitterDistance); - // 上下パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(95, baseSplitContainer.SplitterDistance); // 上から 100px - 5px (下から 500px) + // 上下パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(95, baseSplitContainer.SplitterDistance); // 上から 100px - 5px (下から 500px) - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(500, baseSplitContainer.SplitterDistance); - } + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(500, baseSplitContainer.SplitterDistance); } [Fact] public void IsPanelInvertedSetter_SplitterDistanceVerticalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Vertical; // 左右に分割された状態 - splitContainer.SplitterWidth = 5; // 分割線の幅は 5px - splitContainer.SplitterDistance = 500; // 左から 500px で分割 (右から 300px - 5px) + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Vertical; // 左右に分割された状態 + splitContainer.SplitterWidth = 5; // 分割線の幅は 5px + splitContainer.SplitterDistance = 500; // 左から 500px で分割 (右から 300px - 5px) - var baseSplitContainer = (SplitContainer)splitContainer; + var baseSplitContainer = (SplitContainer)splitContainer; - // 反転前の状態 (通常の SplitContainer と同じ挙動) - Assert.Equal(500, baseSplitContainer.SplitterDistance); + // 反転前の状態 (通常の SplitContainer と同じ挙動) + Assert.Equal(500, baseSplitContainer.SplitterDistance); - // 左右パネルを反転する - splitContainer.IsPanelInverted = true; - Assert.Equal(295, baseSplitContainer.SplitterDistance); // 左から 300px - 5px (右から 500px) + // 左右パネルを反転する + splitContainer.IsPanelInverted = true; + Assert.Equal(295, baseSplitContainer.SplitterDistance); // 左から 300px - 5px (右から 500px) - // 元に戻す - splitContainer.IsPanelInverted = false; - Assert.Equal(500, baseSplitContainer.SplitterDistance); - } + // 元に戻す + splitContainer.IsPanelInverted = false; + Assert.Equal(500, baseSplitContainer.SplitterDistance); } [Fact] public void PanelGetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer()) - { - var panel1 = splitContainer.Panel1; - var panel2 = splitContainer.Panel2; + using var splitContainer = new OTSplitContainer(); + var panel1 = splitContainer.Panel1; + var panel2 = splitContainer.Panel2; - splitContainer.IsPanelInverted = true; + splitContainer.IsPanelInverted = true; - Assert.Same(panel2, splitContainer.Panel1); - Assert.Same(panel1, splitContainer.Panel2); - } + Assert.Same(panel2, splitContainer.Panel1); + Assert.Same(panel1, splitContainer.Panel2); } [Theory] @@ -292,16 +271,14 @@ public void PanelGetter_InvertedTest() [InlineData(FixedPanel.Panel2)] public void FixedPanelGetter_InvertedTest(FixedPanel fixedPanel) { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.FixedPanel = fixedPanel; + using var splitContainer = new OTSplitContainer(); + splitContainer.FixedPanel = fixedPanel; - Assert.Equal(fixedPanel, splitContainer.FixedPanel); + Assert.Equal(fixedPanel, splitContainer.FixedPanel); - // 反転した状態でも OTSplitterContainer.FixedPanel の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.Equal(fixedPanel, splitContainer.FixedPanel); - } + // 反転した状態でも OTSplitterContainer.FixedPanel の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.Equal(fixedPanel, splitContainer.FixedPanel); } [Theory] @@ -310,179 +287,157 @@ public void FixedPanelGetter_InvertedTest(FixedPanel fixedPanel) [InlineData(FixedPanel.Panel2, FixedPanel.Panel1)] public void FixedPanelSetter_InvertedTest(FixedPanel inputValue, FixedPanel internalValue) { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.IsPanelInverted = true; - - // 反転中に FixedPanel を変更する - splitContainer.FixedPanel = inputValue; - Assert.Equal(internalValue, ((SplitContainer)splitContainer).FixedPanel); - } + using var splitContainer = new OTSplitContainer(); + splitContainer.IsPanelInverted = true; + + // 反転中に FixedPanel を変更する + splitContainer.FixedPanel = inputValue; + Assert.Equal(internalValue, ((SplitContainer)splitContainer).FixedPanel); } [Fact] public void SplitterDistanceGetter_InvertedVerticalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Vertical; - splitContainer.SplitterWidth = 5; - splitContainer.SplitterDistance = 500; - - // setter で代入した長さと一致しているか、SplitterDistance と Panel1.Width が一致しているかをテスト - Assert.Equal(500, splitContainer.SplitterDistance); - Assert.Equal(splitContainer.Panel1.Width, splitContainer.SplitterDistance); - - // 反転した状態でも OTSplitterContainer.SplitterDistance の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.Equal(500, splitContainer.SplitterDistance); - Assert.Equal(splitContainer.Panel1.Width, splitContainer.SplitterDistance); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Vertical; + splitContainer.SplitterWidth = 5; + splitContainer.SplitterDistance = 500; + + // setter で代入した長さと一致しているか、SplitterDistance と Panel1.Width が一致しているかをテスト + Assert.Equal(500, splitContainer.SplitterDistance); + Assert.Equal(splitContainer.Panel1.Width, splitContainer.SplitterDistance); + + // 反転した状態でも OTSplitterContainer.SplitterDistance の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.Equal(500, splitContainer.SplitterDistance); + Assert.Equal(splitContainer.Panel1.Width, splitContainer.SplitterDistance); } [Fact] public void SplitterDistanceGetter_InvertedHorizontalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Horizontal; - splitContainer.SplitterWidth = 5; - splitContainer.SplitterDistance = 500; - - // setter で代入した長さと一致しているか、SplitterDistance と Panel1.Height が一致しているかをテスト - Assert.Equal(500, splitContainer.SplitterDistance); - Assert.Equal(splitContainer.Panel1.Height, splitContainer.SplitterDistance); - - // 反転した状態でも OTSplitterContainer.SplitterDistance の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.Equal(500, splitContainer.SplitterDistance); - Assert.Equal(splitContainer.Panel1.Height, splitContainer.SplitterDistance); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Horizontal; + splitContainer.SplitterWidth = 5; + splitContainer.SplitterDistance = 500; + + // setter で代入した長さと一致しているか、SplitterDistance と Panel1.Height が一致しているかをテスト + Assert.Equal(500, splitContainer.SplitterDistance); + Assert.Equal(splitContainer.Panel1.Height, splitContainer.SplitterDistance); + + // 反転した状態でも OTSplitterContainer.SplitterDistance の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.Equal(500, splitContainer.SplitterDistance); + Assert.Equal(splitContainer.Panel1.Height, splitContainer.SplitterDistance); } [Fact] public void SplitterDistanceSetter_InvertedVerticalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Vertical; - splitContainer.SplitterWidth = 5; - - splitContainer.IsPanelInverted = true; - - // 反転中に SplitterDistance を変更する - splitContainer.SplitterDistance = 500; - Assert.Equal(295, ((SplitContainer)splitContainer).SplitterDistance); - Assert.Equal(500, ((SplitContainer)splitContainer).Panel2.Width); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Vertical; + splitContainer.SplitterWidth = 5; + + splitContainer.IsPanelInverted = true; + + // 反転中に SplitterDistance を変更する + splitContainer.SplitterDistance = 500; + Assert.Equal(295, ((SplitContainer)splitContainer).SplitterDistance); + Assert.Equal(500, ((SplitContainer)splitContainer).Panel2.Width); } [Fact] public void SplitterDistanceSetter_InvertedHorizontalTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Orientation = Orientation.Horizontal; - splitContainer.SplitterWidth = 5; - - splitContainer.IsPanelInverted = true; - - // 反転中に SplitterDistance を変更する - splitContainer.SplitterDistance = 500; - Assert.Equal(95, ((SplitContainer)splitContainer).SplitterDistance); - Assert.Equal(500, ((SplitContainer)splitContainer).Panel2.Height); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Orientation = Orientation.Horizontal; + splitContainer.SplitterWidth = 5; + + splitContainer.IsPanelInverted = true; + + // 反転中に SplitterDistance を変更する + splitContainer.SplitterDistance = 500; + Assert.Equal(95, ((SplitContainer)splitContainer).SplitterDistance); + Assert.Equal(500, ((SplitContainer)splitContainer).Panel2.Height); } [Fact] public void PanelMinSizeGetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.Panel1MinSize = 200; - splitContainer.Panel2MinSize = 300; - - Assert.Equal(200, splitContainer.Panel1MinSize); - Assert.Equal(300, splitContainer.Panel2MinSize); - - // 反転した状態でも OTSplitterContainer.Panel1MinSize, Panel2MinSize の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.Equal(200, splitContainer.Panel1MinSize); - Assert.Equal(300, splitContainer.Panel2MinSize); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.Panel1MinSize = 200; + splitContainer.Panel2MinSize = 300; + + Assert.Equal(200, splitContainer.Panel1MinSize); + Assert.Equal(300, splitContainer.Panel2MinSize); + + // 反転した状態でも OTSplitterContainer.Panel1MinSize, Panel2MinSize の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.Equal(200, splitContainer.Panel1MinSize); + Assert.Equal(300, splitContainer.Panel2MinSize); } [Fact] public void PanelMinSizeSetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }) - { - splitContainer.IsPanelInverted = true; - - // 反転中に Panel1MinSize, Panel2MinSize を変更する - splitContainer.Panel1MinSize = 200; - splitContainer.Panel2MinSize = 300; - Assert.Equal(300, ((SplitContainer)splitContainer).Panel1MinSize); - Assert.Equal(200, ((SplitContainer)splitContainer).Panel2MinSize); - } + using var splitContainer = new OTSplitContainer { Width = 800, Height = 600 }; + splitContainer.IsPanelInverted = true; + + // 反転中に Panel1MinSize, Panel2MinSize を変更する + splitContainer.Panel1MinSize = 200; + splitContainer.Panel2MinSize = 300; + Assert.Equal(300, ((SplitContainer)splitContainer).Panel1MinSize); + Assert.Equal(200, ((SplitContainer)splitContainer).Panel2MinSize); } [Fact] public void Panel1CollapsedGetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.Panel1Collapsed = true; + using var splitContainer = new OTSplitContainer(); + splitContainer.Panel1Collapsed = true; - Assert.True(splitContainer.Panel1Collapsed); + Assert.True(splitContainer.Panel1Collapsed); - // 反転した状態でも OTSplitterContainer.Panel1Collapsed の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.True(splitContainer.Panel1Collapsed); - } + // 反転した状態でも OTSplitterContainer.Panel1Collapsed の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.True(splitContainer.Panel1Collapsed); } [Fact] public void Panel1CollapsedSetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.IsPanelInverted = true; - - // 反転中に Panel1Collapsed を変更する - splitContainer.Panel1Collapsed = true; - Assert.False(((SplitContainer)splitContainer).Panel1Collapsed); - Assert.True(((SplitContainer)splitContainer).Panel2Collapsed); - } + using var splitContainer = new OTSplitContainer(); + splitContainer.IsPanelInverted = true; + + // 反転中に Panel1Collapsed を変更する + splitContainer.Panel1Collapsed = true; + Assert.False(((SplitContainer)splitContainer).Panel1Collapsed); + Assert.True(((SplitContainer)splitContainer).Panel2Collapsed); } [Fact] public void Panel2CollapsedGetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.Panel2Collapsed = true; + using var splitContainer = new OTSplitContainer(); + splitContainer.Panel2Collapsed = true; - Assert.True(splitContainer.Panel2Collapsed); + Assert.True(splitContainer.Panel2Collapsed); - // 反転した状態でも OTSplitterContainer.Panel2Collapsed の値は外見上変化しない - splitContainer.IsPanelInverted = true; - Assert.True(splitContainer.Panel2Collapsed); - } + // 反転した状態でも OTSplitterContainer.Panel2Collapsed の値は外見上変化しない + splitContainer.IsPanelInverted = true; + Assert.True(splitContainer.Panel2Collapsed); } [Fact] public void Panel2CollapsedSetter_InvertedTest() { - using (var splitContainer = new OTSplitContainer()) - { - splitContainer.IsPanelInverted = true; - - // 反転中に Panel2Collapsed を変更する - splitContainer.Panel2Collapsed = true; - Assert.True(((SplitContainer)splitContainer).Panel1Collapsed); - Assert.False(((SplitContainer)splitContainer).Panel2Collapsed); - } + using var splitContainer = new OTSplitContainer(); + splitContainer.IsPanelInverted = true; + + // 反転中に Panel2Collapsed を変更する + splitContainer.Panel2Collapsed = true; + Assert.True(((SplitContainer)splitContainer).Panel1Collapsed); + Assert.False(((SplitContainer)splitContainer).Panel2Collapsed); } } } diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index 999b4327e..a61fda3a5 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -3,10 +3,13 @@ OpenTween net472 - 8.0 + 10.0 enable true + + + @@ -22,7 +25,12 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/OpenTween.Tests/ShortUrlTest.cs b/OpenTween.Tests/ShortUrlTest.cs index a5a490ad9..887222617 100644 --- a/OpenTween.Tests/ShortUrlTest.cs +++ b/OpenTween.Tests/ShortUrlTest.cs @@ -38,392 +38,362 @@ public class ShortUrlTest public async Task ExpandUrlAsync_Test() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> http://example.com/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> http://example.com/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - Assert.Equal(new Uri("http://example.com/hoge2"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); + Assert.Equal(new Uri("http://example.com/hoge2"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_IrregularUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://www.flickr.com/photo.gne?short=hoge -> /photos/foo/11111/ - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://www.flickr.com/photo.gne?short=hoge"), x.RequestUri); + // https://www.flickr.com/photo.gne?short=hoge -> /photos/foo/11111/ + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://www.flickr.com/photo.gne?short=hoge"), x.RequestUri); - return this.CreateRedirectResponse("/photos/foo/11111/", UriKind.Relative); - }); + return this.CreateRedirectResponse("/photos/foo/11111/", UriKind.Relative); + }); - Assert.Equal(new Uri("https://www.flickr.com/photos/foo/11111/"), - await shortUrl.ExpandUrlAsync(new Uri("https://www.flickr.com/photo.gne?short=hoge"))); + Assert.Equal(new Uri("https://www.flickr.com/photos/foo/11111/"), + await shortUrl.ExpandUrlAsync(new Uri("https://www.flickr.com/photo.gne?short=hoge"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_DisableExpandingTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - shortUrl.DisableExpanding = true; + shortUrl.DisableExpanding = true; - // https://t.co/hoge1 -> http://example.com/hoge2 - handler.Enqueue(x => - { - // このリクエストは実行されないはず - Assert.True(false); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + // https://t.co/hoge1 -> http://example.com/hoge2 + handler.Enqueue(x => + { + // このリクエストは実行されないはず + Assert.True(false); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - Assert.Equal(new Uri("https://t.co/hoge1"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); + Assert.Equal(new Uri("https://t.co/hoge1"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); - Assert.Equal(1, handler.QueueCount); - } + Assert.Equal(1, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_RecursiveTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> https://bit.ly/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> https://bit.ly/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("https://bit.ly/hoge2"); - }); + return this.CreateRedirectResponse("https://bit.ly/hoge2"); + }); - // https://bit.ly/hoge2 -> http://example.com/hoge3 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://bit.ly/hoge2"), x.RequestUri); + // https://bit.ly/hoge2 -> http://example.com/hoge3 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://bit.ly/hoge2"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge3"); - }); + return this.CreateRedirectResponse("http://example.com/hoge3"); + }); - Assert.Equal(new Uri("http://example.com/hoge3"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); + Assert.Equal(new Uri("http://example.com/hoge3"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_RecursiveLimitTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> https://bit.ly/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> https://bit.ly/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("https://bit.ly/hoge2"); - }); + return this.CreateRedirectResponse("https://bit.ly/hoge2"); + }); - // https://bit.ly/hoge2 -> https://tinyurl.com/hoge3 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://bit.ly/hoge2"), x.RequestUri); + // https://bit.ly/hoge2 -> https://tinyurl.com/hoge3 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://bit.ly/hoge2"), x.RequestUri); - return this.CreateRedirectResponse("https://tinyurl.com/hoge3"); - }); + return this.CreateRedirectResponse("https://tinyurl.com/hoge3"); + }); - // https://tinyurl.com/hoge3 -> http://example.com/hoge4 - handler.Enqueue(x => - { - // このリクエストは実行されないはず - Assert.True(false); - return this.CreateRedirectResponse("http://example.com/hoge4"); - }); + // https://tinyurl.com/hoge3 -> http://example.com/hoge4 + handler.Enqueue(x => + { + // このリクエストは実行されないはず + Assert.True(false); + return this.CreateRedirectResponse("http://example.com/hoge4"); + }); - Assert.Equal(new Uri("https://tinyurl.com/hoge3"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"), redirectLimit: 2)); + Assert.Equal(new Uri("https://tinyurl.com/hoge3"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"), redirectLimit: 2)); - Assert.Equal(1, handler.QueueCount); - } + Assert.Equal(1, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_UpgradeToHttpsTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // http://t.co/hoge -> http://example.com/hoge - handler.Enqueue(x => - { - // https:// に変換されてリクエストが送信される - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge"), x.RequestUri); + // http://t.co/hoge -> http://example.com/hoge + handler.Enqueue(x => + { + // https:// に変換されてリクエストが送信される + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge"); - }); + return this.CreateRedirectResponse("http://example.com/hoge"); + }); - Assert.Equal(new Uri("http://example.com/hoge"), - await shortUrl.ExpandUrlAsync(new Uri("http://t.co/hoge"))); + Assert.Equal(new Uri("http://example.com/hoge"), + await shortUrl.ExpandUrlAsync(new Uri("http://t.co/hoge"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_InsecureDomainTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // http://tinami.jp/hoge -> http://example.com/hoge - handler.Enqueue(x => - { - // HTTPS非対応のドメインは http:// のままリクエストが送信される - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("http://tinami.jp/hoge"), x.RequestUri); + // http://tinami.jp/hoge -> http://example.com/hoge + handler.Enqueue(x => + { + // HTTPS非対応のドメインは http:// のままリクエストが送信される + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("http://tinami.jp/hoge"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge"); - }); + return this.CreateRedirectResponse("http://example.com/hoge"); + }); - Assert.Equal(new Uri("http://example.com/hoge"), - await shortUrl.ExpandUrlAsync(new Uri("http://tinami.jp/hoge"))); + Assert.Equal(new Uri("http://example.com/hoge"), + await shortUrl.ExpandUrlAsync(new Uri("http://tinami.jp/hoge"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_RelativeUriTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - handler.Enqueue(x => - { - // このリクエストは実行されないはず - Assert.True(false); - return this.CreateRedirectResponse(""); - }); + handler.Enqueue(x => + { + // このリクエストは実行されないはず + Assert.True(false); + return this.CreateRedirectResponse(""); + }); - // 相対 URI に対しては何も行わない - Assert.Equal(new Uri("./foo/bar", UriKind.Relative), - await shortUrl.ExpandUrlAsync(new Uri("./foo/bar", UriKind.Relative))); + // 相対 URI に対しては何も行わない + Assert.Equal(new Uri("./foo/bar", UriKind.Relative), + await shortUrl.ExpandUrlAsync(new Uri("./foo/bar", UriKind.Relative))); - Assert.Equal(1, handler.QueueCount); - } + Assert.Equal(1, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_RelativeRedirectTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // Location に相対 URL を指定したリダイレクト (テストに使う URL は適当) - // https://t.co/hogehoge -> /tetetete - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hogehoge"), x.RequestUri); + // Location に相対 URL を指定したリダイレクト (テストに使う URL は適当) + // https://t.co/hogehoge -> /tetetete + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hogehoge"), x.RequestUri); - return this.CreateRedirectResponse("/tetetete", UriKind.Relative); - }); + return this.CreateRedirectResponse("/tetetete", UriKind.Relative); + }); - // https://t.co/tetetete -> http://example.com/tetetete - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/tetetete"), x.RequestUri); + // https://t.co/tetetete -> http://example.com/tetetete + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/tetetete"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/tetetete"); - }); + return this.CreateRedirectResponse("http://example.com/tetetete"); + }); - Assert.Equal(new Uri("http://example.com/tetetete"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hogehoge"))); + Assert.Equal(new Uri("http://example.com/tetetete"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hogehoge"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_String_Test() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> http://example.com/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> http://example.com/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - Assert.Equal("http://example.com/hoge2", - await shortUrl.ExpandUrlAsync("https://t.co/hoge1")); + Assert.Equal("http://example.com/hoge2", + await shortUrl.ExpandUrlAsync("https://t.co/hoge1")); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_String_SchemeLessUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> http://example.com/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> http://example.com/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - // スキームが省略されたURL - Assert.Equal("http://example.com/hoge2", - await shortUrl.ExpandUrlAsync("t.co/hoge1")); + // スキームが省略されたURL + Assert.Equal("http://example.com/hoge2", + await shortUrl.ExpandUrlAsync("t.co/hoge1")); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_String_InvalidUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - handler.Enqueue(x => - { - // リクエストは送信されないはず - Assert.True(false); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + handler.Enqueue(x => + { + // リクエストは送信されないはず + Assert.True(false); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - // 不正なURL - Assert.Equal("..hogehoge..", await shortUrl.ExpandUrlAsync("..hogehoge..")); + // 不正なURL + Assert.Equal("..hogehoge..", await shortUrl.ExpandUrlAsync("..hogehoge..")); - Assert.Equal(1, handler.QueueCount); - } + Assert.Equal(1, handler.QueueCount); } [Fact] public async Task ExpandUrlAsync_HttpErrorTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> 503 Service Unavailable - handler.Enqueue(x => - { - return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); - }); + // https://t.co/hoge1 -> 503 Service Unavailable + handler.Enqueue(x => + { + return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); + }); - Assert.Equal(new Uri("https://t.co/hoge1"), - await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); + Assert.Equal(new Uri("https://t.co/hoge1"), + await shortUrl.ExpandUrlAsync(new Uri("https://t.co/hoge1"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlHtmlAsync_Test() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - // https://t.co/hoge1 -> http://example.com/hoge2 - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Head, x.Method); - Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); + // https://t.co/hoge1 -> http://example.com/hoge2 + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://t.co/hoge1"), x.RequestUri); - return this.CreateRedirectResponse("http://example.com/hoge2"); - }); + return this.CreateRedirectResponse("http://example.com/hoge2"); + }); - Assert.Equal("hogehoge", - await shortUrl.ExpandUrlHtmlAsync("hogehoge")); + Assert.Equal("hogehoge", + await shortUrl.ExpandUrlHtmlAsync("hogehoge")); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ExpandUrlHtmlAsync_RelativeUriTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var shortUrl = new ShortUrl(http); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); - handler.Enqueue(x => - { - // リクエストは送信されないはず - Assert.True(false); - return this.CreateRedirectResponse("http://example.com/hoge"); - }); + handler.Enqueue(x => + { + // リクエストは送信されないはず + Assert.True(false); + return this.CreateRedirectResponse("http://example.com/hoge"); + }); - Assert.Equal("hogehoge", - await shortUrl.ExpandUrlHtmlAsync("hogehoge")); + Assert.Equal("hogehoge", + await shortUrl.ExpandUrlHtmlAsync("hogehoge")); - Assert.Equal(1, handler.QueueCount); - } + Assert.Equal(1, handler.QueueCount); } private HttpResponseMessage CreateRedirectResponse(string uriStr) @@ -440,54 +410,50 @@ private HttpResponseMessage CreateRedirectResponse(string uriStr, UriKind uriKin public async Task ShortenUrlAsync_TinyUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); + + handler.Enqueue(async x => { - var shortUrl = new ShortUrl(http); + Assert.Equal(HttpMethod.Post, x.Method); + Assert.Equal(new Uri("https://tinyurl.com/api-create.php"), x.RequestUri); + Assert.Equal("url=http%3A%2F%2Fexample.com%2Fhogehogehoge", await x.Content.ReadAsStringAsync()); - handler.Enqueue(async x => + return new HttpResponseMessage(HttpStatusCode.OK) { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal(new Uri("https://tinyurl.com/api-create.php"), x.RequestUri); - Assert.Equal("url=http%3A%2F%2Fexample.com%2Fhogehogehoge", await x.Content.ReadAsStringAsync()); + Content = new ByteArrayContent(Encoding.UTF8.GetBytes("http://tinyurl.com/hoge")), + }; + }); - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(Encoding.UTF8.GetBytes("http://tinyurl.com/hoge")), - }; - }); + Assert.Equal(new Uri("https://tinyurl.com/hoge"), + await shortUrl.ShortenUrlAsync(MyCommon.UrlConverter.TinyUrl, new Uri("http://example.com/hogehogehoge"))); - Assert.Equal(new Uri("https://tinyurl.com/hoge"), - await shortUrl.ShortenUrlAsync(MyCommon.UrlConverter.TinyUrl, new Uri("http://example.com/hogehogehoge"))); - - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task ShortenUrlAsync_UxnuUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); + + handler.Enqueue(x => { - var shortUrl = new ShortUrl(http); + Assert.Equal(HttpMethod.Get, x.Method); + Assert.Equal("https://ux.nu/api/short?format=plain&url=http:%2F%2Fexample.com%2Fhogehoge", + x.RequestUri.AbsoluteUri); - handler.Enqueue(x => + return new HttpResponseMessage(HttpStatusCode.OK) { - Assert.Equal(HttpMethod.Get, x.Method); - Assert.Equal("https://ux.nu/api/short?format=plain&url=http:%2F%2Fexample.com%2Fhogehoge", - x.RequestUri.AbsoluteUri); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(Encoding.UTF8.GetBytes("https://ux.nu/hoge")), - }; - }); + Content = new ByteArrayContent(Encoding.UTF8.GetBytes("https://ux.nu/hoge")), + }; + }); - Assert.Equal(new Uri("https://ux.nu/hoge"), - await shortUrl.ShortenUrlAsync(MyCommon.UrlConverter.Uxnu, new Uri("http://example.com/hogehoge"))); + Assert.Equal(new Uri("https://ux.nu/hoge"), + await shortUrl.ShortenUrlAsync(MyCommon.UrlConverter.Uxnu, new Uri("http://example.com/hogehoge"))); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } } } diff --git a/OpenTween.Tests/ShortcutCommandTest.cs b/OpenTween.Tests/ShortcutCommandTest.cs index 8439a2f1b..f03c565e7 100644 --- a/OpenTween.Tests/ShortcutCommandTest.cs +++ b/OpenTween.Tests/ShortcutCommandTest.cs @@ -150,7 +150,8 @@ public async Task RunCommand_AsyncTest() var invoked = false; var shortcut = ShortcutCommand.Create(Keys.F5) - .Do(async () => { + .Do(async () => + { await Task.Delay(100).ConfigureAwait(false); invoked = true; }); diff --git a/OpenTween.Tests/TabsDialogTest.cs b/OpenTween.Tests/TabsDialogTest.cs index 0b3ca539d..c3306d20b 100644 --- a/OpenTween.Tests/TabsDialogTest.cs +++ b/OpenTween.Tests/TabsDialogTest.cs @@ -47,7 +47,7 @@ public TabsDialogTest() this.tabinfo.AddTab(new FilterTabModel("MyTab1")); // 一応 TabInformation.GetInstance() でも取得できるようにする - var field = typeof(TabInformations).GetField("_instance", + var field = typeof(TabInformations).GetField("Instance", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.SetField); field.SetValue(null, this.tabinfo); } @@ -55,113 +55,103 @@ public TabsDialogTest() [Fact] public void OKButtonEnabledTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - Assert.False(dialog.OK_Button.Enabled); + using var dialog = new TabsDialog(this.tabinfo); + Assert.False(dialog.OK_Button.Enabled); - dialog.TabList.SelectedIndex = 0; + dialog.TabList.SelectedIndex = 0; - Assert.True(dialog.OK_Button.Enabled); + Assert.True(dialog.OK_Button.Enabled); - dialog.TabList.SelectedIndex = -1; + dialog.TabList.SelectedIndex = -1; - Assert.False(dialog.OK_Button.Enabled); - } + Assert.False(dialog.OK_Button.Enabled); } [Fact] public void MultiSelectTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - // MultiSelect = false (default) - var firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; - Assert.Null(firstItem.Tab); // 「(新規タブ)」 - Assert.Equal(SelectionMode.One, dialog.TabList.SelectionMode); - - dialog.MultiSelect = true; - firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; - Assert.NotNull(firstItem.Tab); - Assert.Equal(SelectionMode.MultiExtended, dialog.TabList.SelectionMode); - - dialog.MultiSelect = false; - firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; - Assert.Null(firstItem.Tab); - Assert.Equal(SelectionMode.One, dialog.TabList.SelectionMode); - } + using var dialog = new TabsDialog(this.tabinfo); + + // MultiSelect = false (default) + var firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; + Assert.Null(firstItem.Tab); // 「(新規タブ)」 + Assert.Equal(SelectionMode.One, dialog.TabList.SelectionMode); + + dialog.MultiSelect = true; + firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; + Assert.NotNull(firstItem.Tab); + Assert.Equal(SelectionMode.MultiExtended, dialog.TabList.SelectionMode); + + dialog.MultiSelect = false; + firstItem = (TabsDialog.TabListItem)dialog.TabList.Items[0]; + Assert.Null(firstItem.Tab); + Assert.Equal(SelectionMode.One, dialog.TabList.SelectionMode); } [Fact] public void DoubleClickTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - dialog.TabList.SelectedIndex = -1; - TestUtils.FireEvent(dialog.TabList, "DoubleClick"); + using var dialog = new TabsDialog(this.tabinfo); - Assert.Equal(DialogResult.None, dialog.DialogResult); - Assert.False(dialog.IsDisposed); + dialog.TabList.SelectedIndex = -1; + TestUtils.FireEvent(dialog.TabList, "DoubleClick"); - dialog.TabList.SelectedIndex = 1; - TestUtils.FireEvent(dialog.TabList, "DoubleClick"); + Assert.Equal(DialogResult.None, dialog.DialogResult); + Assert.False(dialog.IsDisposed); - Assert.Equal(DialogResult.OK, dialog.DialogResult); - Assert.True(dialog.IsDisposed); - } + dialog.TabList.SelectedIndex = 1; + TestUtils.FireEvent(dialog.TabList, "DoubleClick"); + + Assert.Equal(DialogResult.OK, dialog.DialogResult); + Assert.True(dialog.IsDisposed); } [Fact] public void SelectableTabTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - dialog.MultiSelect = false; + using var dialog = new TabsDialog(this.tabinfo); + dialog.MultiSelect = false; - var item = (TabsDialog.TabListItem)dialog.TabList.Items[0]; - Assert.Null(item.Tab); + var item = (TabsDialog.TabListItem)dialog.TabList.Items[0]; + Assert.Null(item.Tab); - item = (TabsDialog.TabListItem)dialog.TabList.Items[1]; - Assert.Equal(this.tabinfo.Tabs["Reply"], item.Tab); + item = (TabsDialog.TabListItem)dialog.TabList.Items[1]; + Assert.Equal(this.tabinfo.Tabs["Reply"], item.Tab); - item = (TabsDialog.TabListItem)dialog.TabList.Items[2]; - Assert.Equal(this.tabinfo.Tabs["MyTab1"], item.Tab); - } + item = (TabsDialog.TabListItem)dialog.TabList.Items[2]; + Assert.Equal(this.tabinfo.Tabs["MyTab1"], item.Tab); } [Fact] public void SelectedTabTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - dialog.MultiSelect = false; + using var dialog = new TabsDialog(this.tabinfo); + dialog.MultiSelect = false; - dialog.TabList.SelectedIndex = 0; - Assert.Null(dialog.SelectedTab); + dialog.TabList.SelectedIndex = 0; + Assert.Null(dialog.SelectedTab); - dialog.TabList.SelectedIndex = 1; - Assert.Equal(this.tabinfo.Tabs["Reply"], dialog.SelectedTab); - } + dialog.TabList.SelectedIndex = 1; + Assert.Equal(this.tabinfo.Tabs["Reply"], dialog.SelectedTab); } [Fact] public void SelectedTabsTest() { - using (var dialog = new TabsDialog(this.tabinfo)) - { - dialog.MultiSelect = true; - - dialog.TabList.SelectedIndices.Clear(); - var selectedTabs = dialog.SelectedTabs; - Assert.Empty(selectedTabs); - - dialog.TabList.SelectedIndices.Add(0); - selectedTabs = dialog.SelectedTabs; - Assert.Equal(new[] { this.tabinfo.Tabs["Reply"] }, selectedTabs); - - dialog.TabList.SelectedIndices.Add(1); - selectedTabs = dialog.SelectedTabs; - Assert.Equal(new[] { this.tabinfo.Tabs["Reply"], this.tabinfo.Tabs["MyTab1"] }, selectedTabs); - } + using var dialog = new TabsDialog(this.tabinfo); + dialog.MultiSelect = true; + + dialog.TabList.SelectedIndices.Clear(); + var selectedTabs = dialog.SelectedTabs; + Assert.Empty(selectedTabs); + + dialog.TabList.SelectedIndices.Add(0); + selectedTabs = dialog.SelectedTabs; + Assert.Equal(new[] { this.tabinfo.Tabs["Reply"] }, selectedTabs); + + dialog.TabList.SelectedIndices.Add(1); + selectedTabs = dialog.SelectedTabs; + Assert.Equal(new[] { this.tabinfo.Tabs["Reply"], this.tabinfo.Tabs["MyTab1"] }, selectedTabs); } } } diff --git a/OpenTween.Tests/TestUtils.cs b/OpenTween.Tests/TestUtils.cs index fd495e160..e7fe87b8a 100644 --- a/OpenTween.Tests/TestUtils.cs +++ b/OpenTween.Tests/TestUtils.cs @@ -59,12 +59,12 @@ public static async Task NotRaisesAsync(Action> attach, Actio { T? raisedEvent = null; - void handler(object s, T e) + void Handler(object s, T e) => raisedEvent = e; try { - attach(handler); + attach(Handler); await testCode().ConfigureAwait(false); if (raisedEvent != null) @@ -72,13 +72,13 @@ void handler(object s, T e) } finally { - detach(handler); + detach(Handler); } } public static void NotPropertyChanged(INotifyPropertyChanged @object, string propertyName, Action testCode) { - void handler(object s, PropertyChangedEventArgs e) + void Handler(object s, PropertyChangedEventArgs e) { if (s == @object && e.PropertyName == propertyName) throw new Xunit.Sdk.PropertyChangedException(propertyName); @@ -86,34 +86,34 @@ void handler(object s, PropertyChangedEventArgs e) try { - @object.PropertyChanged += handler; + @object.PropertyChanged += Handler; testCode(); } finally { - @object.PropertyChanged -= handler; + @object.PropertyChanged -= Handler; } } public static MemoryImage CreateDummyImage() { - using (var bitmap = new Bitmap(100, 100)) - using (var stream = new MemoryStream()) - { - bitmap.Save(stream, ImageFormat.Png); - stream.Position = 0; + using var bitmap = new Bitmap(100, 100); + using var stream = new MemoryStream(); + bitmap.Save(stream, ImageFormat.Png); + stream.Position = 0; - return MemoryImage.CopyFromStream(stream); - } + return MemoryImage.CopyFromStream(stream); } public static MemoryImageMediaItem CreateDummyMediaItem() - => new MemoryImageMediaItem(CreateDummyImage()); + => new(CreateDummyImage()); - public static void FireEvent(T control, string eventName) where T : Control + public static void FireEvent(T control, string eventName) + where T : Control => TestUtils.FireEvent(control, eventName, EventArgs.Empty); - public static void FireEvent(T control, string eventName, EventArgs e) where T : Control + public static void FireEvent(T control, string eventName, EventArgs e) + where T : Control { var methodName = "On" + eventName; var method = typeof(T).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); @@ -121,7 +121,8 @@ public static void FireEvent(T control, string eventName, EventArgs e) where method.Invoke(control, new[] { e }); } - public static void Validate(T control) where T : Control + public static void Validate(T control) + where T : Control { var cancelEventArgs = new CancelEventArgs(); TestUtils.FireEvent(control, "Validating", cancelEventArgs); @@ -148,35 +149,40 @@ private sealed class RestoreFreezedTime : IDisposable public void Dispose() => DateTimeUtc.UseFakeNow = false; } + + public static DateTimeUtc LocalTime(int year, int month, int day, int hour, int minute, int second) + => new(new DateTimeOffset(year, month, day, hour, minute, second, TimeZoneInfo.Local.BaseUtcOffset)); } } +#pragma warning disable SA1403 + namespace OpenTween.Setting { public class SettingManagerTest { public static SettingCommon Common { - get => SettingManager.Common; - set => SettingManager.Common = value; + get => SettingManager.Instance.Common; + set => SettingManager.Instance.Common = value; } public static SettingLocal Local { - get => SettingManager.Local; - set => SettingManager.Local = value; + get => SettingManager.Instance.Local; + set => SettingManager.Instance.Local = value; } public static SettingTabs Tabs { - get => SettingManager.Tabs; - set => SettingManager.Tabs = value; + get => SettingManager.Instance.Tabs; + set => SettingManager.Instance.Tabs = value; } public static SettingAtIdList AtIdList { - get => SettingManager.AtIdList; - set => SettingManager.AtIdList = value; + get => SettingManager.Instance.AtIdList; + set => SettingManager.Instance.AtIdList = value; } } } diff --git a/OpenTween.Tests/ThemeManagerTest.cs b/OpenTween.Tests/ThemeManagerTest.cs new file mode 100644 index 000000000..bcdad67c4 --- /dev/null +++ b/OpenTween.Tests/ThemeManagerTest.cs @@ -0,0 +1,74 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System.Drawing; +using Xunit; + +namespace OpenTween +{ + public class ThemeManagerTest + { + [Fact] + public void FontDefaultTest() + { + var settings = new SettingLocal(); + using var themeManager = new ThemeManager(settings); + Assert.True(themeManager.FontDetail.IsSystemFont); + Assert.Equal(nameof(SystemFonts.DefaultFont), themeManager.FontDetail.SystemFontName); + } + + [Fact] + public void FontCustomTest() + { + var settings = new SettingLocal + { + FontDetailStr = "Arial, 9pt", + }; + using var themeManager = new ThemeManager(settings); + Assert.False(themeManager.FontDetail.IsSystemFont); + Assert.Equal("Arial", themeManager.FontDetail.OriginalFontName); + Assert.Equal(9, themeManager.FontDetail.SizeInPoints); + } + + [Fact] + public void ColorDefaultTest() + { + var settings = new SettingLocal(); + using var themeManager = new ThemeManager(settings); + Assert.True(themeManager.ColorDetail.IsSystemColor); + Assert.Equal(nameof(KnownColor.ControlText), themeManager.ColorDetail.Name); + } + + [Fact] + public void ColorCustomTest() + { + var settings = new SettingLocal + { + ColorDetailStr = "0, 100, 200", + }; + using var themeManager = new ThemeManager(settings); + Assert.False(themeManager.ColorDetail.IsSystemColor); + Assert.Equal(0, themeManager.ColorDetail.R); + Assert.Equal(100, themeManager.ColorDetail.G); + Assert.Equal(200, themeManager.ColorDetail.B); + } + } +} diff --git a/OpenTween.Tests/ThrottleTimerTest.cs b/OpenTween.Tests/ThrottleTimerTest.cs index 66a51226b..280d8f633 100644 --- a/OpenTween.Tests/ThrottleTimerTest.cs +++ b/OpenTween.Tests/ThrottleTimerTest.cs @@ -33,7 +33,7 @@ public class ThrottleTimerTest { private class TestThrottleTimer : ThrottleTimer { - public MockTimer mockTimer = new MockTimer(() => Task.CompletedTask); + public MockTimer MockTimer = new(() => Task.CompletedTask); public TestThrottleTimer(Func timerCallback, TimeSpan interval) : base(timerCallback, interval) @@ -41,7 +41,7 @@ public TestThrottleTimer(Func timerCallback, TimeSpan interval) } protected override ITimer CreateTimer(Func callback) - => this.mockTimer = new MockTimer(callback); + => this.MockTimer = new MockTimer(callback); } [Fact] @@ -50,16 +50,18 @@ public async Task Callback_ThrottleTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.FromMinutes(2); - using var throttling = new TestThrottleTimer(callback, interval); - var mockTimer = throttling.mockTimer; + using var throttling = new TestThrottleTimer(Callback, interval); + var mockTimer = throttling.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -105,16 +107,18 @@ public async Task Callback_CallOnceTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.FromMinutes(2); - using var throttling = new TestThrottleTimer(callback, interval); - var mockTimer = throttling.mockTimer; + using var throttling = new TestThrottleTimer(Callback, interval); + var mockTimer = throttling.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -143,16 +147,18 @@ public async Task Callback_ResumeTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { var count = 0; - Func callback = () => + + Task Callback() { count++; TestUtils.DriftTime(TimeSpan.FromSeconds(10)); return Task.CompletedTask; - }; + } + var interval = TimeSpan.FromMinutes(2); var maxWait = TimeSpan.FromMinutes(2); - using var throttling = new TestThrottleTimer(callback, interval); - var mockTimer = throttling.mockTimer; + using var throttling = new TestThrottleTimer(Callback, interval); + var mockTimer = throttling.MockTimer; Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); diff --git a/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs index 0f41b7759..6d8bb1f82 100644 --- a/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs @@ -69,143 +69,139 @@ public void LegacyUrlPattern_Test() public async Task GetThumbnailInfoAsync_NewUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); + using var http = new HttpClient(handler); + var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Get, x.Method); - Assert.Equal("https://api.foursquare.com/v2/checkins/resolve", - x.RequestUri.GetLeftPart(UriPartial.Path)); + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Get, x.Method); + Assert.Equal("https://api.foursquare.com/v2/checkins/resolve", + x.RequestUri.GetLeftPart(UriPartial.Path)); - var query = HttpUtility.ParseQueryString(x.RequestUri.Query); + var query = HttpUtility.ParseQueryString(x.RequestUri.Query); - Assert.Equal("fake_client_id", query["client_id"]); - Assert.Equal("fake_client_secret", query["client_secret"]); - Assert.NotNull(query["v"]); - Assert.Equal("xxxxxxxx", query["shortId"]); + Assert.Equal("fake_client_id", query["client_id"]); + Assert.Equal("fake_client_secret", query["client_secret"]); + Assert.NotNull(query["v"]); + Assert.Equal("xxxxxxxx", query["shortId"]); - // リクエストに対するテストなのでレスポンスは適当に返す - return new HttpResponseMessage(HttpStatusCode.NotFound); - }); + // リクエストに対するテストなのでレスポンスは適当に返す + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); - var post = new PostClass - { - PostGeo = null, - }; + var post = new PostClass + { + PostGeo = null, + }; - await service.GetThumbnailInfoAsync( - "https://www.swarmapp.com/c/xxxxxxxx", - post, CancellationToken.None); + await service.GetThumbnailInfoAsync( + "https://www.swarmapp.com/c/xxxxxxxx", + post, + CancellationToken.None); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task GetThumbnailInfoAsync_LegacyUrlTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); + using var http = new HttpClient(handler); + var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Get, x.Method); - Assert.Equal("https://api.foursquare.com/v2/checkins/xxxxxxxx", - x.RequestUri.GetLeftPart(UriPartial.Path)); + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Get, x.Method); + Assert.Equal("https://api.foursquare.com/v2/checkins/xxxxxxxx", + x.RequestUri.GetLeftPart(UriPartial.Path)); - var query = HttpUtility.ParseQueryString(x.RequestUri.Query); + var query = HttpUtility.ParseQueryString(x.RequestUri.Query); - Assert.Equal("fake_client_id", query["client_id"]); - Assert.Equal("fake_client_secret", query["client_secret"]); - Assert.NotNull(query["v"]); - Assert.Null(query["signature"]); + Assert.Equal("fake_client_id", query["client_id"]); + Assert.Equal("fake_client_secret", query["client_secret"]); + Assert.NotNull(query["v"]); + Assert.Null(query["signature"]); - // リクエストに対するテストなのでレスポンスは適当に返す - return new HttpResponseMessage(HttpStatusCode.NotFound); - }); + // リクエストに対するテストなのでレスポンスは適当に返す + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); - var post = new PostClass - { - PostGeo = null, - }; + var post = new PostClass + { + PostGeo = null, + }; - await service.GetThumbnailInfoAsync( - "https://foursquare.com/hogehoge/checkin/xxxxxxxx", - post, CancellationToken.None); + await service.GetThumbnailInfoAsync( + "https://foursquare.com/hogehoge/checkin/xxxxxxxx", + post, + CancellationToken.None); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task GetThumbnailInfoAsync_LegacyUrlWithSignatureTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) - { - var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); + using var http = new HttpClient(handler); + var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); - handler.Enqueue(x => - { - Assert.Equal(HttpMethod.Get, x.Method); - Assert.Equal("https://api.foursquare.com/v2/checkins/xxxxxxxx", - x.RequestUri.GetLeftPart(UriPartial.Path)); + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Get, x.Method); + Assert.Equal("https://api.foursquare.com/v2/checkins/xxxxxxxx", + x.RequestUri.GetLeftPart(UriPartial.Path)); - var query = HttpUtility.ParseQueryString(x.RequestUri.Query); + var query = HttpUtility.ParseQueryString(x.RequestUri.Query); - Assert.Equal("fake_client_id", query["client_id"]); - Assert.Equal("fake_client_secret", query["client_secret"]); - Assert.NotNull(query["v"]); - Assert.Equal("aaaaaaa", query["signature"]); + Assert.Equal("fake_client_id", query["client_id"]); + Assert.Equal("fake_client_secret", query["client_secret"]); + Assert.NotNull(query["v"]); + Assert.Equal("aaaaaaa", query["signature"]); - // リクエストに対するテストなのでレスポンスは適当に返す - return new HttpResponseMessage(HttpStatusCode.NotFound); - }); + // リクエストに対するテストなのでレスポンスは適当に返す + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); - var post = new PostClass - { - PostGeo = null, - }; + var post = new PostClass + { + PostGeo = null, + }; - await service.GetThumbnailInfoAsync( - "https://foursquare.com/hogehoge/checkin/xxxxxxxx?s=aaaaaaa", - post, CancellationToken.None); + await service.GetThumbnailInfoAsync( + "https://foursquare.com/hogehoge/checkin/xxxxxxxx?s=aaaaaaa", + post, + CancellationToken.None); - Assert.Equal(0, handler.QueueCount); - } + Assert.Equal(0, handler.QueueCount); } [Fact] public async Task GetThumbnailInfoAsync_GeoLocatedTweetTest() { var handler = new HttpMessageHandlerMock(); - using (var http = new HttpClient(handler)) + using var http = new HttpClient(handler); + var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); + + handler.Enqueue(x => { - var service = new FoursquareCheckin(http, ApiKey.Create("fake_client_id"), ApiKey.Create("fake_client_secret")); - - handler.Enqueue(x => - { - // このリクエストは実行されないはず - Assert.True(false); - return new HttpResponseMessage(HttpStatusCode.NotFound); - }); - - // 既にジオタグが付いているツイートに対しては何もしない - var post = new PostClass - { - PostGeo = new PostClass.StatusGeo(134.04693603515625, 34.35067978344854), - }; - - await service.GetThumbnailInfoAsync( - "https://www.swarmapp.com/c/xxxxxxxx", - post, CancellationToken.None); - - Assert.Equal(1, handler.QueueCount); - } + // このリクエストは実行されないはず + Assert.True(false); + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + // 既にジオタグが付いているツイートに対しては何もしない + var post = new PostClass + { + PostGeo = new PostClass.StatusGeo(134.04693603515625, 34.35067978344854), + }; + + await service.GetThumbnailInfoAsync( + "https://www.swarmapp.com/c/xxxxxxxx", + post, + CancellationToken.None); + + Assert.Equal(1, handler.QueueCount); } [Fact] diff --git a/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs b/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs index 76cec5c45..6031fbed9 100644 --- a/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs @@ -37,7 +37,7 @@ namespace OpenTween.Thumbnail.Services { public class ImgAzyobuziNetTest { - class TestImgAzyobuziNet : ImgAzyobuziNet + private class TestImgAzyobuziNet : ImgAzyobuziNet { public TestImgAzyobuziNet() : this(new[] { "http://img.azyobuzi.net/api/" }) @@ -47,12 +47,12 @@ public TestImgAzyobuziNet() public TestImgAzyobuziNet(string[] apiHosts) : base(null, autoupdate: false) { - this.ApiHosts = apiHosts; + this.apiHosts = apiHosts; this.LoadRegexAsync().Wait(); } public string? GetApiBase() - => this.ApiBase; + => this.apiBase; protected override Task FetchRegexAsync(string apiBase) { diff --git a/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs b/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs index 9dec5fc5e..2e30ba675 100644 --- a/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs @@ -36,7 +36,7 @@ namespace OpenTween.Thumbnail.Services { public class MetaThumbnailServiceTest { - class TestMetaThumbnailService : MetaThumbnailService + private class TestMetaThumbnailService : MetaThumbnailService { public string FakeHtml { get; set; } = ""; diff --git a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs index a7b51e0ba..d2d1c9a46 100644 --- a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs @@ -38,7 +38,7 @@ namespace OpenTween.Thumbnail.Services { public class TinamiTest { - class TestTinami : Tinami + private class TestTinami : Tinami { public string FakeXml { get; set; } = ""; diff --git a/OpenTween.Tests/TimelineListViewCacheTest.cs b/OpenTween.Tests/TimelineListViewCacheTest.cs new file mode 100644 index 000000000..cd701866d --- /dev/null +++ b/OpenTween.Tests/TimelineListViewCacheTest.cs @@ -0,0 +1,551 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using OpenTween.Models; +using OpenTween.OpenTweenCustomControl; +using Xunit; + +namespace OpenTween +{ + public class TimelineListViewCacheTest + { + private readonly Random random = new(); + + private PostClass CreatePost() + { + return new() + { + StatusId = this.random.Next(10000), + UserId = this.random.Next(10000), + ScreenName = "test", + Nickname = "てすと", + AccessibleText = "foo", + Source = "OpenTween", + FavoritedCount = 0, + CreatedAt = TestUtils.LocalTime(2022, 1, 1, 0, 0, 0), + IsRead = true, + }; + } + + [Fact] + public void UpdateListSize_Test() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + Assert.Equal(0, listView.VirtualListSize); + Assert.False(cache.IsListSizeMismatched); + + tab.AddPostQueue(this.CreatePost()); + tab.AddSubmit(); + + Assert.True(cache.IsListSizeMismatched); + + cache.UpdateListSize(); + + Assert.Equal(1, listView.VirtualListSize); + Assert.False(cache.IsListSizeMismatched); + } + + [Fact] + public void GetItem_Test() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + + Assert.Equal("", item.SubItems[0].Text); + Assert.Equal("てすと", item.SubItems[1].Text); + Assert.Equal("foo", item.SubItems[2].Text); + Assert.Equal("2022/01/01 0:00:00", item.SubItems[3].Text); + Assert.Equal("test", item.SubItems[4].Text); + Assert.Equal("", item.SubItems[5].Text); + Assert.Equal("", item.SubItems[6].Text); + Assert.Equal("OpenTween", item.SubItems[7].Text); + } + + [Fact] + public void GetItem_UnreadTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsRead = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + Assert.Equal("★", item.SubItems[5].Text); + } + + [Fact] + public void GetItem_UnreadManageDisabledTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + // 未読管理が無効な場合は未読状態に関わらず未読マークを表示しない + tab.UnreadManage = false; + + var post = this.CreatePost(); + post.IsRead = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + Assert.Equal("", item.SubItems[5].Text); + } + + [Fact] + public void GetItem_FavoritesTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.FavoritedCount = 1; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + Assert.Equal("+1", item.SubItems[6].Text); + } + + [Fact] + public void GetItem_RetweetTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.RetweetedId = 50L; + post.RetweetedBy = "hoge"; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + Assert.Equal($"test{Environment.NewLine}(RT:hoge)", item.SubItems[4].Text); + } + + [Fact] + public void GetItem_DeletedTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsDeleted = true; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var item = cache.GetItem(0); + Assert.Equal("(DELETED)", item.SubItems[2].Text); + } + + [Fact] + public void GetItem_CachedTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + tab.AddPostQueue(post); + tab.AddSubmit(); + + cache.CreateCache(startIndex: 0, endIndex: 0); + + post.IsRead = false; + + // IsRead の状態はキャッシュに未反映なので既読状態で返るのが正しい + var item = cache.GetItem(0); + Assert.Equal("", item.SubItems[5].Text); + } + + [Fact] + public void GetStyle_Font_ReadedTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsRead = true; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemFont.Readed, style.Font); + } + + [Fact] + public void GetStyle_Font_UnreadTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsRead = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemFont.Unread, style.Font); + } + + [Fact] + public void GetStyle_Font_UnreadStyleDisabledTest() + { + var tab = new PublicSearchTabModel("tab"); + var settingCommon = new SettingCommon(); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, settingCommon); + + var post = this.CreatePost(); + post.IsRead = false; + + settingCommon.UseUnreadStyle = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemFont.Readed, style.Font); + } + + [Fact] + public void GetStyle_Font_UnreadManageDisabledTest() + { + var tab = new PublicSearchTabModel("tab"); + var settingCommon = new SettingCommon(); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, settingCommon); + + var post = this.CreatePost(); + post.IsRead = false; + + tab.UnreadManage = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemFont.Readed, style.Font); + } + + [Fact] + public void GetStyleItem_ForeColor_Test() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.None, style.ForeColor); + } + + [Fact] + public void GetStyle_ForeColor_FavoritedTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsFav = true; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.Fav, style.ForeColor); + } + + [Fact] + public void GetStyle_ForeColor_RetweetTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.RetweetedId = 100L; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.Retweet, style.ForeColor); + } + + [Fact] + public void GetStyle_ForeColor_OWLTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + post.IsOwl = true; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.OWL, style.ForeColor); + } + + [Fact] + public void GetStyle_ForeColor_OWLStyleDisabledTest() + { + var tab = new PublicSearchTabModel("tab"); + var settingCommon = new SettingCommon(); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, settingCommon); + + var post = this.CreatePost(); + post.IsOwl = true; + + settingCommon.OneWayLove = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.None, style.ForeColor); + } + + [Fact] + public void GetStyle_ForeColor_DMTest() + { + var tab = new PublicSearchTabModel("tab"); + var settingCommon = new SettingCommon(); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, settingCommon); + + var post = this.CreatePost(); + post.IsDm = true; + post.IsOwl = true; + + // DM の場合は設定に関わらず ColorOWL を使う + settingCommon.OneWayLove = false; + + tab.AddPostQueue(post); + tab.AddSubmit(); + + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.OWL, style.ForeColor); + } + + [Fact] + public void GetStyle_BackColor_AtToTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + basePost.InReplyToStatusId = targetPost.StatusId; + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.AtTo, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_SelfTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + targetPost.IsMe = true; + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.Self, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_AtSelfTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + targetPost.IsReply = true; + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.AtSelf, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_AtFromTargetTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + basePost.ReplyToList = new() { (targetPost.UserId, targetPost.ScreenName) }; + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.AtFromTarget, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_AtTargetTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var basePost = this.CreatePost(); + tab.AddPostQueue(basePost); + + var targetPost = this.CreatePost(); + targetPost.ReplyToList = new() { (basePost.UserId, basePost.ScreenName) }; + tab.AddPostQueue(targetPost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.AtTarget, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_TargetTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + basePost.UserId = targetPost.UserId; + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.Target, style.BackColor); + } + + [Fact] + public void GetStyle_BackColor_NormalTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var targetPost = this.CreatePost(); + tab.AddPostQueue(targetPost); + + var basePost = this.CreatePost(); + tab.AddPostQueue(basePost); + + tab.AddSubmit(); + tab.SelectPosts(new[] { tab.IndexOf(basePost.StatusId) }); + + var style = cache.GetStyle(tab.IndexOf(targetPost.StatusId)); + Assert.Equal(ListItemBackColor.None, style.BackColor); + } + + [Fact] + public void GetStyle_CachedTest() + { + var tab = new PublicSearchTabModel("tab"); + using var listView = new DetailsListView(); + using var cache = new TimelineListViewCache(listView, tab, new()); + + var post = this.CreatePost(); + tab.AddPostQueue(post); + tab.AddSubmit(); + + cache.CreateCache(startIndex: 0, endIndex: 0); + + post.IsFav = true; + + // IsFav の状態はキャッシュに未反映なので None が返るのが正しい + var style = cache.GetStyle(0); + Assert.Equal(ListItemForeColor.None, style.ForeColor); + } + } +} diff --git a/OpenTween.Tests/TimelineScheduerTest.cs b/OpenTween.Tests/TimelineScheduerTest.cs index 087f9ddff..6a72acbf2 100644 --- a/OpenTween.Tests/TimelineScheduerTest.cs +++ b/OpenTween.Tests/TimelineScheduerTest.cs @@ -32,7 +32,7 @@ public class TimelineScheduerTest { private class TestTimelineScheduler : TimelineScheduler { - public MockTimer mockTimer = new MockTimer(() => Task.CompletedTask); + public MockTimer MockTimer = new(() => Task.CompletedTask); public TestTimelineScheduler() : base() @@ -40,7 +40,7 @@ public TestTimelineScheduler() } protected override ITimer CreateTimer(Func callback) - => this.mockTimer = new MockTimer(callback); + => this.MockTimer = new MockTimer(callback); } [Fact] @@ -49,7 +49,7 @@ public async Task Callback_Test() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { using var scheduler = new TestTimelineScheduler(); - var mockTimer = scheduler.mockTimer; + var mockTimer = scheduler.MockTimer; Assert.False(mockTimer.IsTimerRunning); @@ -80,7 +80,7 @@ public async Task Callback_SystemResumeTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { using var scheduler = new TestTimelineScheduler(); - var mockTimer = scheduler.mockTimer; + var mockTimer = scheduler.MockTimer; Assert.False(mockTimer.IsTimerRunning); @@ -125,7 +125,7 @@ public void RefreshSchedule_Test() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { using var scheduler = new TestTimelineScheduler(); - var mockTimer = scheduler.mockTimer; + var mockTimer = scheduler.MockTimer; scheduler.Enabled = true; Assert.False(mockTimer.IsTimerRunning); @@ -145,7 +145,7 @@ public void RefreshSchedule_EmptyTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { using var scheduler = new TestTimelineScheduler(); - var mockTimer = scheduler.mockTimer; + var mockTimer = scheduler.MockTimer; scheduler.Enabled = true; Assert.False(mockTimer.IsTimerRunning); @@ -161,7 +161,7 @@ public void RefreshSchedule_MultipleTest() using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) { using var scheduler = new TestTimelineScheduler(); - var mockTimer = scheduler.mockTimer; + var mockTimer = scheduler.MockTimer; scheduler.Enabled = true; Assert.False(mockTimer.IsTimerRunning); diff --git a/OpenTween.Tests/ToolStripAPIGaugeTest.cs b/OpenTween.Tests/ToolStripAPIGaugeTest.cs index 983967a65..e9f3a3d61 100644 --- a/OpenTween.Tests/ToolStripAPIGaugeTest.cs +++ b/OpenTween.Tests/ToolStripAPIGaugeTest.cs @@ -35,130 +35,125 @@ public class ToolStripAPIGaugeTest [Fact] public void ApiEndpointTest() { - using (var toolStrip = new TestToolStripAPIGauge()) - { - var now = DateTimeUtc.Now; - toolStrip.DateTimeNow = now; + using var toolStrip = new TestToolStripAPIGauge(); + var now = DateTimeUtc.Now; + toolStrip.DateTimeNow = now; - MyCommon.TwitterApiInfo.AccessLimit["endpoint1"] = new ApiLimit(15, 15, now + TimeSpan.FromMinutes(15)); - MyCommon.TwitterApiInfo.AccessLimit["endpoint2"] = new ApiLimit(180, 18, now + TimeSpan.FromMinutes(5)); + MyCommon.TwitterApiInfo.AccessLimit["endpoint1"] = new ApiLimit(15, 15, now + TimeSpan.FromMinutes(15)); + MyCommon.TwitterApiInfo.AccessLimit["endpoint2"] = new ApiLimit(180, 18, now + TimeSpan.FromMinutes(5)); - // toolStrip.ApiEndpoint の初期値は null + // toolStrip.ApiEndpoint の初期値は null - Assert.Null(toolStrip.ApiEndpoint); - Assert.Null(toolStrip.ApiLimit); + Assert.Null(toolStrip.ApiEndpoint); + Assert.Null(toolStrip.ApiLimit); - toolStrip.ApiEndpoint = "endpoint1"; + toolStrip.ApiEndpoint = "endpoint1"; - Assert.Equal("endpoint1", toolStrip.ApiEndpoint); - Assert.Equal(new ApiLimit(15, 15, now + TimeSpan.FromMinutes(15)), toolStrip.ApiLimit); + Assert.Equal("endpoint1", toolStrip.ApiEndpoint); + Assert.Equal(new ApiLimit(15, 15, now + TimeSpan.FromMinutes(15)), toolStrip.ApiLimit); - toolStrip.ApiEndpoint = "endpoint2"; + toolStrip.ApiEndpoint = "endpoint2"; - Assert.Equal("endpoint2", toolStrip.ApiEndpoint); - Assert.Equal(new ApiLimit(180, 18, now + TimeSpan.FromMinutes(5)), toolStrip.ApiLimit); + Assert.Equal("endpoint2", toolStrip.ApiEndpoint); + Assert.Equal(new ApiLimit(180, 18, now + TimeSpan.FromMinutes(5)), toolStrip.ApiLimit); - MyCommon.TwitterApiInfo.AccessLimit["endpoint2"] = new ApiLimit(180, 17, now + TimeSpan.FromMinutes(5)); - toolStrip.ApiEndpoint = "endpoint2"; + MyCommon.TwitterApiInfo.AccessLimit["endpoint2"] = new ApiLimit(180, 17, now + TimeSpan.FromMinutes(5)); + toolStrip.ApiEndpoint = "endpoint2"; - Assert.Equal("endpoint2", toolStrip.ApiEndpoint); - Assert.Equal(new ApiLimit(180, 17, now + TimeSpan.FromMinutes(5)), toolStrip.ApiLimit); + Assert.Equal("endpoint2", toolStrip.ApiEndpoint); + Assert.Equal(new ApiLimit(180, 17, now + TimeSpan.FromMinutes(5)), toolStrip.ApiLimit); - toolStrip.ApiEndpoint = "hoge"; + toolStrip.ApiEndpoint = "hoge"; - Assert.Equal("hoge", toolStrip.ApiEndpoint); - Assert.Null(toolStrip.ApiLimit); + Assert.Equal("hoge", toolStrip.ApiEndpoint); + Assert.Null(toolStrip.ApiLimit); - toolStrip.ApiEndpoint = ""; + toolStrip.ApiEndpoint = ""; - Assert.Null(toolStrip.ApiEndpoint); - Assert.Null(toolStrip.ApiLimit); + Assert.Null(toolStrip.ApiEndpoint); + Assert.Null(toolStrip.ApiLimit); - MyCommon.TwitterApiInfo.AccessLimit.Clear(); - } + MyCommon.TwitterApiInfo.AccessLimit.Clear(); } [Fact] public void GaugeHeightTest() { - using (var toolStrip = new ToolStripAPIGauge()) - { - toolStrip.AutoSize = false; - toolStrip.Size = new Size(100, 10); + using var toolStrip = new ToolStripAPIGauge(); + toolStrip.AutoSize = false; + toolStrip.Size = new Size(100, 10); - MyCommon.TwitterApiInfo.AccessLimit["endpoint"] = new ApiLimit(15, 15, DateTimeUtc.MaxValue); - toolStrip.ApiEndpoint = "endpoint"; + MyCommon.TwitterApiInfo.AccessLimit["endpoint"] = new ApiLimit(15, 15, DateTimeUtc.MaxValue); + toolStrip.ApiEndpoint = "endpoint"; - toolStrip.GaugeHeight = 5; + toolStrip.GaugeHeight = 5; - Assert.Equal(new Rectangle(0, 0, 100, 5), toolStrip.apiGaugeBounds); - Assert.Equal(new Rectangle(0, 5, 100, 5), toolStrip.timeGaugeBounds); + Assert.Equal(new Rectangle(0, 0, 100, 5), toolStrip.ApiGaugeBounds); + Assert.Equal(new Rectangle(0, 5, 100, 5), toolStrip.TimeGaugeBounds); - toolStrip.GaugeHeight = 3; + toolStrip.GaugeHeight = 3; - Assert.Equal(new Rectangle(0, 2, 100, 3), toolStrip.apiGaugeBounds); - Assert.Equal(new Rectangle(0, 5, 100, 3), toolStrip.timeGaugeBounds); + Assert.Equal(new Rectangle(0, 2, 100, 3), toolStrip.ApiGaugeBounds); + Assert.Equal(new Rectangle(0, 5, 100, 3), toolStrip.TimeGaugeBounds); - toolStrip.GaugeHeight = 0; + toolStrip.GaugeHeight = 0; - Assert.Equal(Rectangle.Empty, toolStrip.apiGaugeBounds); - Assert.Equal(Rectangle.Empty, toolStrip.timeGaugeBounds); + Assert.Equal(Rectangle.Empty, toolStrip.ApiGaugeBounds); + Assert.Equal(Rectangle.Empty, toolStrip.TimeGaugeBounds); - MyCommon.TwitterApiInfo.AccessLimit.Clear(); - } + MyCommon.TwitterApiInfo.AccessLimit.Clear(); } [Fact] public void TextTest() { - using (var toolStrip = new ToolStripAPIGauge()) - { - MyCommon.TwitterApiInfo.AccessLimit["/statuses/home_timeline"] = new ApiLimit(15, 15, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); - MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 18, DateTimeUtc.Now + TimeSpan.FromMinutes(-2)); - MyCommon.TwitterApiInfo.AccessLimit["/search/tweets"] = new ApiLimit(180, 90, DateTimeUtc.Now + TimeSpan.FromMinutes(5)); + using var toolStrip = new ToolStripAPIGauge(); - // toolStrip.ApiEndpoint の初期値は null + MyCommon.TwitterApiInfo.AccessLimit["/statuses/home_timeline"] = new ApiLimit(15, 15, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); + MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 18, DateTimeUtc.Now + TimeSpan.FromMinutes(-2)); + MyCommon.TwitterApiInfo.AccessLimit["/search/tweets"] = new ApiLimit(180, 90, DateTimeUtc.Now + TimeSpan.FromMinutes(5)); - Assert.Equal("API ???/???", toolStrip.Text); - Assert.Equal("API rest unknown ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); + // toolStrip.ApiEndpoint の初期値は null - toolStrip.ApiEndpoint = "/search/tweets"; + Assert.Equal("API ???/???", toolStrip.Text); + Assert.Equal("API rest unknown ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); - Assert.Equal("API 90/180", toolStrip.Text); - Assert.Equal("API rest /search/tweets 90/180" + Environment.NewLine + "(reset after 5 minutes)", toolStrip.ToolTipText); + toolStrip.ApiEndpoint = "/search/tweets"; - toolStrip.ApiEndpoint = "/statuses/user_timeline"; + Assert.Equal("API 90/180", toolStrip.Text); + Assert.Equal("API rest /search/tweets 90/180" + Environment.NewLine + "(reset after 5 minutes)", toolStrip.ToolTipText); - Assert.Equal("API ???/???", toolStrip.Text); - Assert.Equal("API rest /statuses/user_timeline ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); + toolStrip.ApiEndpoint = "/statuses/user_timeline"; - MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 180, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); - toolStrip.ApiEndpoint = "/statuses/user_timeline"; + Assert.Equal("API ???/???", toolStrip.Text); + Assert.Equal("API rest /statuses/user_timeline ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); - Assert.Equal("API 180/180", toolStrip.Text); - Assert.Equal("API rest /statuses/user_timeline 180/180" + Environment.NewLine + "(reset after 15 minutes)", toolStrip.ToolTipText); + MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 180, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); + toolStrip.ApiEndpoint = "/statuses/user_timeline"; - MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 179, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); - toolStrip.ApiEndpoint = "/statuses/user_timeline"; + Assert.Equal("API 180/180", toolStrip.Text); + Assert.Equal("API rest /statuses/user_timeline 180/180" + Environment.NewLine + "(reset after 15 minutes)", toolStrip.ToolTipText); - Assert.Equal("API 179/180", toolStrip.Text); - Assert.Equal("API rest /statuses/user_timeline 179/180" + Environment.NewLine + "(reset after 15 minutes)", toolStrip.ToolTipText); + MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit(180, 179, DateTimeUtc.Now + TimeSpan.FromMinutes(15)); + toolStrip.ApiEndpoint = "/statuses/user_timeline"; - toolStrip.ApiEndpoint = "hoge"; + Assert.Equal("API 179/180", toolStrip.Text); + Assert.Equal("API rest /statuses/user_timeline 179/180" + Environment.NewLine + "(reset after 15 minutes)", toolStrip.ToolTipText); - Assert.Equal("API ???/???", toolStrip.Text); - Assert.Equal("API rest hoge ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); + toolStrip.ApiEndpoint = "hoge"; - toolStrip.ApiEndpoint = ""; + Assert.Equal("API ???/???", toolStrip.Text); + Assert.Equal("API rest hoge ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); - Assert.Equal("API ???/???", toolStrip.Text); - Assert.Equal("API rest unknown ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); + toolStrip.ApiEndpoint = ""; - MyCommon.TwitterApiInfo.AccessLimit.Clear(); - } + Assert.Equal("API ???/???", toolStrip.Text); + Assert.Equal("API rest unknown ???/???" + Environment.NewLine + "(reset after ??? minutes)", toolStrip.ToolTipText); + + MyCommon.TwitterApiInfo.AccessLimit.Clear(); } - class TestToolStripAPIGauge : ToolStripAPIGauge + private class TestToolStripAPIGauge : ToolStripAPIGauge { public DateTimeUtc DateTimeNow = DateTimeUtc.Now; @@ -175,33 +170,32 @@ protected override void UpdateRemainMinutes() [Fact] public void GaugeBoundsTest() { - using (var toolStrip = new TestToolStripAPIGauge()) - { - var now = DateTimeUtc.Now; - toolStrip.DateTimeNow = now; + using var toolStrip = new TestToolStripAPIGauge(); - toolStrip.AutoSize = false; - toolStrip.Size = new Size(100, 10); - toolStrip.GaugeHeight = 5; + var now = DateTimeUtc.Now; + toolStrip.DateTimeNow = now; - // toolStrip.ApiEndpoint の初期値は null + toolStrip.AutoSize = false; + toolStrip.Size = new Size(100, 10); + toolStrip.GaugeHeight = 5; - Assert.Equal(Rectangle.Empty, toolStrip.apiGaugeBounds); - Assert.Equal(Rectangle.Empty, toolStrip.timeGaugeBounds); + // toolStrip.ApiEndpoint の初期値は null - MyCommon.TwitterApiInfo.AccessLimit["endpoint"] = new ApiLimit(150, 60, now + TimeSpan.FromMinutes(3)); - toolStrip.ApiEndpoint = "endpoint"; + Assert.Equal(Rectangle.Empty, toolStrip.ApiGaugeBounds); + Assert.Equal(Rectangle.Empty, toolStrip.TimeGaugeBounds); - Assert.Equal(new Rectangle(0, 0, 40, 5), toolStrip.apiGaugeBounds); // 40% (60/150) - Assert.Equal(new Rectangle(0, 5, 20, 5), toolStrip.timeGaugeBounds); // 20% (3/15) + MyCommon.TwitterApiInfo.AccessLimit["endpoint"] = new ApiLimit(150, 60, now + TimeSpan.FromMinutes(3)); + toolStrip.ApiEndpoint = "endpoint"; - toolStrip.ApiEndpoint = ""; + Assert.Equal(new Rectangle(0, 0, 40, 5), toolStrip.ApiGaugeBounds); // 40% (60/150) + Assert.Equal(new Rectangle(0, 5, 20, 5), toolStrip.TimeGaugeBounds); // 20% (3/15) - Assert.Equal(Rectangle.Empty, toolStrip.apiGaugeBounds); - Assert.Equal(Rectangle.Empty, toolStrip.timeGaugeBounds); + toolStrip.ApiEndpoint = ""; - MyCommon.TwitterApiInfo.AccessLimit.Clear(); - } + Assert.Equal(Rectangle.Empty, toolStrip.ApiGaugeBounds); + Assert.Equal(Rectangle.Empty, toolStrip.TimeGaugeBounds); + + MyCommon.TwitterApiInfo.AccessLimit.Clear(); } [Fact] @@ -217,14 +211,14 @@ public void OneBillionTest() toolStrip.GaugeHeight = 5; MyCommon.TwitterApiInfo.AccessLimit["/statuses/user_timeline"] = new ApiLimit( - limitCount: 1_000_000_000, - limitRemain: 999_999_999, - resetDate: now + TimeSpan.FromMinutes(15) + AccessLimitCount: 1_000_000_000, + AccessLimitRemain: 999_999_999, + AccessLimitResetDate: now + TimeSpan.FromMinutes(15) ); toolStrip.ApiEndpoint = "/statuses/user_timeline"; - Assert.Equal(new Rectangle(0, 0, 99, 5), toolStrip.apiGaugeBounds); // 99% (999999999/1000000000) - Assert.Equal(new Rectangle(0, 5, 100, 5), toolStrip.timeGaugeBounds); // 100% (15/15) + Assert.Equal(new Rectangle(0, 0, 99, 5), toolStrip.ApiGaugeBounds); // 99% (999999999/1000000000) + Assert.Equal(new Rectangle(0, 5, 100, 5), toolStrip.TimeGaugeBounds); // 100% (15/15) Assert.Equal("API 999999999/1000000000", toolStrip.Text); Assert.Equal("API rest /statuses/user_timeline 999999999/1000000000" + Environment.NewLine + "(reset after 15 minutes)", toolStrip.ToolTipText); diff --git a/OpenTween.Tests/TweenMainTest.cs b/OpenTween.Tests/TweenMainTest.cs index de1d42699..275074c1e 100644 --- a/OpenTween.Tests/TweenMainTest.cs +++ b/OpenTween.Tests/TweenMainTest.cs @@ -37,50 +37,42 @@ public class TweenMainTest public void GetUrlFromDataObject_XMozUrlTest() { var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\nTwitter\0"); - using (var memstream = new MemoryStream(dataBytes)) - { - var data = new DataObject("text/x-moz-url", memstream); + using var memstream = new MemoryStream(dataBytes); + var data = new DataObject("text/x-moz-url", memstream); - var expected = ("https://twitter.com/", "Twitter"); - Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); - } + var expected = ("https://twitter.com/", "Twitter"); + Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); } [Fact] public void GetUrlFromDataObject_IESiteModeToUrlTest() { var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0Twitter\0"); - using (var memstream = new MemoryStream(dataBytes)) - { - var data = new DataObject("IESiteModeToUrl", memstream); + using var memstream = new MemoryStream(dataBytes); + var data = new DataObject("IESiteModeToUrl", memstream); - var expected = ("https://twitter.com/", "Twitter"); - Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); - } + var expected = ("https://twitter.com/", "Twitter"); + Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); } [Fact] public void GetUrlFromDataObject_UniformResourceLocatorWTest() { var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0"); - using (var memstream = new MemoryStream(dataBytes)) - { - var data = new DataObject("UniformResourceLocatorW", memstream); + using var memstream = new MemoryStream(dataBytes); + var data = new DataObject("UniformResourceLocatorW", memstream); - var expected = ("https://twitter.com/", (string?)null); - Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); - } + var expected = ("https://twitter.com/", (string?)null); + Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data)); } [Fact] public void GetUrlFromDataObject_UnknownFormatTest() { - using (var memstream = new MemoryStream(Array.Empty())) - { - var data = new DataObject("application/x-hogehoge", memstream); + using var memstream = new MemoryStream(Array.Empty()); + var data = new DataObject("application/x-hogehoge", memstream); - Assert.Throws(() => TweenMain.GetUrlFromDataObject(data)); - } + Assert.Throws(() => TweenMain.GetUrlFromDataObject(data)); } [Fact] diff --git a/OpenTween.Tests/TweetExtractorTest.cs b/OpenTween.Tests/TweetExtractorTest.cs index 1c03a8e91..942440d46 100644 --- a/OpenTween.Tests/TweetExtractorTest.cs +++ b/OpenTween.Tests/TweetExtractorTest.cs @@ -304,6 +304,18 @@ public void ExtractEmojiEntities_Unicode13Test() Assert.Equal("https://twemoji.maxcdn.com/2/72x72/1f977.png", entity.Url); } + [Fact] + public void ExtractEmojiEntities_Unicode14Test() + { + // Unicode 14.0 で追加された絵文字 + var origText = "🫠"; // U+1FAE0 (MELTING FACE) + var entity = TweetExtractor.ExtractEmojiEntities(origText).Single(); + + Assert.Equal(new[] { 0, 1 }, entity.Indices); + Assert.Equal("🫠", entity.Text); + Assert.Equal("https://twemoji.maxcdn.com/2/72x72/1fae0.png", entity.Url); + } + [Fact] public void ExtractEmojiEntities_EmojiModifiers_CombiningTest() { diff --git a/OpenTween.Tests/TweetThumbnailTest.cs b/OpenTween.Tests/TweetThumbnailTest.cs index 0af40799a..9f274ec8b 100644 --- a/OpenTween.Tests/TweetThumbnailTest.cs +++ b/OpenTween.Tests/TweetThumbnailTest.cs @@ -41,7 +41,7 @@ namespace OpenTween { public class TweetThumbnailTest { - class TestThumbnailService : IThumbnailService + private class TestThumbnailService : IThumbnailService { private readonly Regex regex; private readonly string replaceUrl; @@ -71,7 +71,7 @@ public TestThumbnailService(string pattern, string replaceUrl, string? replaceTo }; } - class MockThumbnailInfo : ThumbnailInfo + private class MockThumbnailInfo : ThumbnailInfo { public override Task LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken) => Task.FromResult(TestUtils.CreateDummyImage()); @@ -79,20 +79,21 @@ public override Task LoadThumbnailImageAsync(HttpClient http, Cance } public TweetThumbnailTest() - { - this.ThumbnailGeneratorSetup(); - this.MyCommonSetup(); - } + => this.MyCommonSetup(); - private void ThumbnailGeneratorSetup() + private ThumbnailGenerator CreateThumbnailGenerator() { - ThumbnailGenerator.Services.Clear(); - ThumbnailGenerator.Services.AddRange(new[] + var imgAzyobuziNet = new ImgAzyobuziNet(autoupdate: false); + var thumbGenerator = new ThumbnailGenerator(imgAzyobuziNet); + thumbGenerator.Services.Clear(); + thumbGenerator.Services.AddRange(new[] { new TestThumbnailService(@"^https?://foo.example.com/(.+)$", @"http://img.example.com/${1}.png", null), new TestThumbnailService(@"^https?://bar.example.com/(.+)$", @"http://img.example.com/${1}.png", @"${1}"), new TestThumbnailService(@"^https?://slow.example.com/(.+)$", @"http://img.example.com/${1}.png", null), }); + + return thumbGenerator; } private void MyCommonSetup() @@ -106,19 +107,18 @@ private void MyCommonSetup() [Fact] public void CreatePictureBoxTest() { - using (var thumbBox = new TweetThumbnail()) - { - var method = typeof(TweetThumbnail).GetMethod("CreatePictureBox", BindingFlags.Instance | BindingFlags.NonPublic); - var picbox = method.Invoke(thumbBox, new[] { "pictureBox1" }) as PictureBox; + using var thumbBox = new TweetThumbnail(); - Assert.NotNull(picbox); - Assert.Equal("pictureBox1", picbox!.Name); - Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); - Assert.False(picbox.WaitOnLoad); - Assert.Equal(DockStyle.Fill, picbox.Dock); + var method = typeof(TweetThumbnail).GetMethod("CreatePictureBox", BindingFlags.Instance | BindingFlags.NonPublic); + var picbox = method.Invoke(thumbBox, new[] { "pictureBox1" }) as PictureBox; - picbox.Dispose(); - } + Assert.NotNull(picbox); + Assert.Equal("pictureBox1", picbox!.Name); + Assert.Equal(PictureBoxSizeMode.Zoom, picbox.SizeMode); + Assert.False(picbox.WaitOnLoad); + Assert.Equal(DockStyle.Fill, picbox.Dock); + + picbox.Dispose(); } [Fact] @@ -133,17 +133,18 @@ public async Task CancelAsyncTest() }, }; - using (var thumbbox = new TweetThumbnail()) - using (var tokenSource = new CancellationTokenSource()) - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - var task = thumbbox.ShowThumbnailAsync(post, tokenSource.Token); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - tokenSource.Cancel(); + using var tokenSource = new CancellationTokenSource(); - await Assert.ThrowsAnyAsync(async () => await task); - Assert.True(task.IsCanceled); - } + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + var task = thumbbox.ShowThumbnailAsync(post, tokenSource.Token); + + tokenSource.Cancel(); + + await Assert.ThrowsAnyAsync(async () => await task); + Assert.True(task.IsCanceled); } [Theory] @@ -152,29 +153,29 @@ public async Task CancelAsyncTest() [InlineData(2)] public void SetThumbnailCountTest(int count) { - using (var thumbbox = new TweetThumbnail()) - { - var method = typeof(TweetThumbnail).GetMethod("SetThumbnailCount", BindingFlags.Instance | BindingFlags.NonPublic); - method.Invoke(thumbbox, new[] { (object)count }); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - Assert.Equal(count, thumbbox.pictureBox.Count); + var method = typeof(TweetThumbnail).GetMethod("SetThumbnailCount", BindingFlags.Instance | BindingFlags.NonPublic); + method.Invoke(thumbbox, new[] { (object)count }); - var num = 0; - foreach (var picbox in thumbbox.pictureBox) - { - Assert.Equal("pictureBox" + num, picbox.Name); - num++; - } + Assert.Equal(count, thumbbox.PictureBox.Count); + + var num = 0; + foreach (var picbox in thumbbox.PictureBox) + { + Assert.Equal("pictureBox" + num, picbox.Name); + num++; + } - Assert.Equal(thumbbox.pictureBox, thumbbox.panelPictureBox.Controls.Cast()); + Assert.Equal(thumbbox.PictureBox, thumbbox.panelPictureBox.Controls.Cast()); - Assert.Equal(0, thumbbox.scrollBar.Minimum); + Assert.Equal(0, thumbbox.scrollBar.Minimum); - if (count == 0) - Assert.Equal(0, thumbbox.scrollBar.Maximum); - else - Assert.Equal(count - 1, thumbbox.scrollBar.Maximum); - } + if (count == 0) + Assert.Equal(0, thumbbox.scrollBar.Maximum); + else + Assert.Equal(count - 1, thumbbox.scrollBar.Maximum); } [Fact] @@ -189,25 +190,25 @@ public async Task ShowThumbnailAsyncTest() }, }; - using (var thumbbox = new TweetThumbnail()) - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - await thumbbox.ShowThumbnailAsync(post); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - Assert.Equal(0, thumbbox.scrollBar.Maximum); - Assert.False(thumbbox.scrollBar.Enabled); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + await thumbbox.ShowThumbnailAsync(post); - Assert.Single(thumbbox.pictureBox); - Assert.NotNull(thumbbox.pictureBox[0].Image); + Assert.Equal(0, thumbbox.scrollBar.Maximum); + Assert.False(thumbbox.scrollBar.Enabled); - Assert.IsAssignableFrom(thumbbox.pictureBox[0].Tag); - var thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[0].Tag; + Assert.Single(thumbbox.PictureBox); + Assert.NotNull(thumbbox.PictureBox[0].Image); - Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl); - Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl); + Assert.IsAssignableFrom(thumbbox.PictureBox[0].Tag); + var thumbinfo = (ThumbnailInfo)thumbbox.PictureBox[0].Tag; - Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[0])); - } + Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl); + Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl); + + Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.PictureBox[0])); } [Fact] @@ -223,72 +224,72 @@ public async Task ShowThumbnailAsyncTest2() }, }; - using (var thumbbox = new TweetThumbnail()) - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - await thumbbox.ShowThumbnailAsync(post); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - Assert.Equal(1, thumbbox.scrollBar.Maximum); - Assert.True(thumbbox.scrollBar.Enabled); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + await thumbbox.ShowThumbnailAsync(post); - Assert.Equal(2, thumbbox.pictureBox.Count); - Assert.NotNull(thumbbox.pictureBox[0].Image); - Assert.NotNull(thumbbox.pictureBox[1].Image); + Assert.Equal(1, thumbbox.scrollBar.Maximum); + Assert.True(thumbbox.scrollBar.Enabled); - Assert.IsAssignableFrom(thumbbox.pictureBox[0].Tag); - var thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[0].Tag; + Assert.Equal(2, thumbbox.PictureBox.Count); + Assert.NotNull(thumbbox.PictureBox[0].Image); + Assert.NotNull(thumbbox.PictureBox[1].Image); - Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl); - Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl); + Assert.IsAssignableFrom(thumbbox.PictureBox[0].Tag); + var thumbinfo = (ThumbnailInfo)thumbbox.PictureBox[0].Tag; - Assert.IsAssignableFrom(thumbbox.pictureBox[1].Tag); - thumbinfo = (ThumbnailInfo)thumbbox.pictureBox[1].Tag; + Assert.Equal("http://foo.example.com/abcd", thumbinfo.MediaPageUrl); + Assert.Equal("http://img.example.com/abcd.png", thumbinfo.ThumbnailImageUrl); - Assert.Equal("http://bar.example.com/efgh", thumbinfo.MediaPageUrl); - Assert.Equal("http://img.example.com/efgh.png", thumbinfo.ThumbnailImageUrl); + Assert.IsAssignableFrom(thumbbox.PictureBox[1].Tag); + thumbinfo = (ThumbnailInfo)thumbbox.PictureBox[1].Tag; - Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[0])); - Assert.Equal("efgh", thumbbox.toolTip.GetToolTip(thumbbox.pictureBox[1])); - } + Assert.Equal("http://bar.example.com/efgh", thumbinfo.MediaPageUrl); + Assert.Equal("http://img.example.com/efgh.png", thumbinfo.ThumbnailImageUrl); + + Assert.Equal("", thumbbox.toolTip.GetToolTip(thumbbox.PictureBox[0])); + Assert.Equal("efgh", thumbbox.toolTip.GetToolTip(thumbbox.PictureBox[1])); } [Fact] public async Task ThumbnailLoadingEventTest() { - using (var thumbbox = new TweetThumbnail()) - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - var post = new PostClass + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + + var post = new PostClass + { + TextFromApi = "てすと", + Media = new List { - TextFromApi = "てすと", - Media = new List - { - }, - }; - await TestUtils.NotRaisesAsync( - x => thumbbox.ThumbnailLoading += x, - x => thumbbox.ThumbnailLoading -= x, - () => thumbbox.ShowThumbnailAsync(post) - ); + }, + }; + await TestUtils.NotRaisesAsync( + x => thumbbox.ThumbnailLoading += x, + x => thumbbox.ThumbnailLoading -= x, + () => thumbbox.ShowThumbnailAsync(post) + ); - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - var post2 = new PostClass - { - TextFromApi = "てすと http://foo.example.com/abcd", - Media = new List + var post2 = new PostClass + { + TextFromApi = "てすと http://foo.example.com/abcd", + Media = new List { new MediaInfo("http://foo.example.com/abcd"), }, - }; + }; - await Assert.RaisesAsync( - x => thumbbox.ThumbnailLoading += x, - x => thumbbox.ThumbnailLoading -= x, - () => thumbbox.ShowThumbnailAsync(post2) - ); - } + await Assert.RaisesAsync( + x => thumbbox.ThumbnailLoading += x, + x => thumbbox.ThumbnailLoading -= x, + () => thumbbox.ShowThumbnailAsync(post2) + ); } [Fact] @@ -304,36 +305,36 @@ public async Task ScrollTest() }, }; - using (var thumbbox = new TweetThumbnail()) - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - await thumbbox.ShowThumbnailAsync(post); + using var thumbbox = new TweetThumbnail(); + thumbbox.Initialize(this.CreateThumbnailGenerator()); - Assert.Equal(0, thumbbox.scrollBar.Minimum); - Assert.Equal(1, thumbbox.scrollBar.Maximum); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + await thumbbox.ShowThumbnailAsync(post); - thumbbox.scrollBar.Value = 0; + Assert.Equal(0, thumbbox.scrollBar.Minimum); + Assert.Equal(1, thumbbox.scrollBar.Maximum); - thumbbox.ScrollDown(); - Assert.Equal(1, thumbbox.scrollBar.Value); - Assert.False(thumbbox.pictureBox[0].Visible); - Assert.True(thumbbox.pictureBox[1].Visible); + thumbbox.scrollBar.Value = 0; - thumbbox.ScrollDown(); - Assert.Equal(1, thumbbox.scrollBar.Value); - Assert.False(thumbbox.pictureBox[0].Visible); - Assert.True(thumbbox.pictureBox[1].Visible); + thumbbox.ScrollDown(); + Assert.Equal(1, thumbbox.scrollBar.Value); + Assert.False(thumbbox.PictureBox[0].Visible); + Assert.True(thumbbox.PictureBox[1].Visible); - thumbbox.ScrollUp(); - Assert.Equal(0, thumbbox.scrollBar.Value); - Assert.True(thumbbox.pictureBox[0].Visible); - Assert.False(thumbbox.pictureBox[1].Visible); + thumbbox.ScrollDown(); + Assert.Equal(1, thumbbox.scrollBar.Value); + Assert.False(thumbbox.PictureBox[0].Visible); + Assert.True(thumbbox.PictureBox[1].Visible); - thumbbox.ScrollUp(); - Assert.Equal(0, thumbbox.scrollBar.Value); - Assert.True(thumbbox.pictureBox[0].Visible); - Assert.False(thumbbox.pictureBox[1].Visible); - } + thumbbox.ScrollUp(); + Assert.Equal(0, thumbbox.scrollBar.Value); + Assert.True(thumbbox.PictureBox[0].Visible); + Assert.False(thumbbox.PictureBox[1].Visible); + + thumbbox.ScrollUp(); + Assert.Equal(0, thumbbox.scrollBar.Value); + Assert.True(thumbbox.PictureBox[0].Visible); + Assert.False(thumbbox.PictureBox[1].Visible); } } } diff --git a/OpenTween.Tests/TwitterTest.cs b/OpenTween.Tests/TwitterTest.cs index df02eb7a1..2c03f9698 100644 --- a/OpenTween.Tests/TwitterTest.cs +++ b/OpenTween.Tests/TwitterTest.cs @@ -108,341 +108,23 @@ public void FindTopOfReplyChainTest() Assert.Equal(1210L, Twitter.FindTopOfReplyChain(posts, 1210L).StatusId); } - [Fact] - public void CreateAccessibleText_MediaAltTest() - { - var text = "https://t.co/hoge"; - var entities = new TwitterEntities - { - Media = new[] - { - new TwitterEntityMedia - { - Indices = new[] { 0, 17 }, - Url = "https://t.co/hoge", - DisplayUrl = "pic.twitter.com/hoge", - ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", - AltText = "代替テキスト", - }, - }, - }; - - var expectedText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); - - Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null)); - } - - [Fact] - public void CreateAccessibleText_MediaNoAltTest() - { - var text = "https://t.co/hoge"; - var entities = new TwitterEntities - { - Media = new[] - { - new TwitterEntityMedia - { - Indices = new[] { 0, 17 }, - Url = "https://t.co/hoge", - DisplayUrl = "pic.twitter.com/hoge", - ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", - AltText = null, - }, - }, - }; - - var expectedText = "pic.twitter.com/hoge"; - - Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null)); - } - - [Fact] - public void CreateAccessibleText_QuotedUrlTest() - { - var text = "https://t.co/hoge"; - var entities = new TwitterEntities - { - Urls = new[] - { - new TwitterEntityUrl - { - Indices = new[] { 0, 17 }, - Url = "https://t.co/hoge", - DisplayUrl = "twitter.com/hoge/status/1…", - ExpandedUrl = "https://twitter.com/hoge/status/1234567890", - }, - }, - }; - var quotedStatus = new TwitterStatus - { - Id = 1234567890L, - IdStr = "1234567890", - User = new TwitterUser - { - Id = 1111, - IdStr = "1111", - ScreenName = "foo", - }, - FullText = "test", - }; - - var expectedText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); - - Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null)); - } - - [Fact] - public void CreateAccessibleText_QuotedUrlWithPermelinkTest() - { - var text = "hoge"; - var entities = new TwitterEntities(); - var quotedStatus = new TwitterStatus - { - Id = 1234567890L, - IdStr = "1234567890", - User = new TwitterUser - { - Id = 1111, - IdStr = "1111", - ScreenName = "foo", - }, - FullText = "test", - }; - var quotedStatusLink = new TwitterQuotedStatusPermalink - { - Url = "https://t.co/hoge", - Display = "twitter.com/hoge/status/1…", - Expanded = "https://twitter.com/hoge/status/1234567890", - }; - - var expectedText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); - - Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink)); - } - - [Fact] - public void CreateAccessibleText_QuotedUrlNoReferenceTest() - { - var text = "https://t.co/hoge"; - var entities = new TwitterEntities - { - Urls = new[] - { - new TwitterEntityUrl - { - Indices = new[] { 0, 17 }, - Url = "https://t.co/hoge", - DisplayUrl = "twitter.com/hoge/status/1…", - ExpandedUrl = "https://twitter.com/hoge/status/1234567890", - }, - }, - }; - var quotedStatus = (TwitterStatus?)null; - - var expectedText = "twitter.com/hoge/status/1…"; - - Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null)); - } - - [Fact] - public void CreateHtmlAnchor_Test() - { - var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3"; - var entities = new TwitterEntities - { - UserMentions = new[] - { - new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" }, - }, - Hashtags = new[] - { - new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" }, - }, - Urls = new[] - { - new TwitterEntityUrl - { - Indices = new[] { 31, 54 }, - Url ="https://t.co/mIJcSoVSK3", - DisplayUrl = "apps-of-a-feather.com", - ExpandedUrl = "http://apps-of-a-feather.com/", - }, - }, - }; - - var expectedHtml = @"@twitterapi" - + @" #BreakingMyTwitter" - + @" apps-of-a-feather.com"; - - Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); - } - - [Fact] - public void CreateHtmlAnchor_NicovideoTest() - { - var text = "sm9"; - var entities = new TwitterEntities(); - - var expectedHtml = @"sm9"; - - Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); - } - - [Fact] - public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() - { - var text = "hoge"; - var entities = new TwitterEntities(); - var quotedStatusLink = new TwitterQuotedStatusPermalink - { - Url = "https://t.co/hoge", - Display = "twitter.com/hoge/status/1…", - Expanded = "https://twitter.com/hoge/status/1234567890", - }; - - var expectedHtml = @"hoge" - + @" twitter.com/hoge/status/1…"; - - Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink)); - } - - [Fact] - public void ParseSource_Test() - { - var sourceHtml = "Twitter Web Client"; - - var expected = ("Twitter Web Client", new Uri("http://twitter.com/")); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_PlainTextTest() - { - var sourceHtml = "web"; - - var expected = ("web", (Uri?)null); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_RelativeUriTest() - { - // 参照: https://twitter.com/kim_upsilon/status/477796052049752064 - var sourceHtml = "erased_45416"; - - var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416")); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_EmptyTest() - { - // 参照: https://twitter.com/kim_upsilon/status/595156014032244738 - var sourceHtml = ""; - - var expected = ("", (Uri?)null); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_NullTest() - { - string? sourceHtml = null; - - var expected = ("", (Uri?)null); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_UnescapeTest() - { - var sourceHtml = "<<hogehoge>>"; - - var expected = ("<>", new Uri("http://example.com/?aaa=123&bbb=456")); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void ParseSource_UnescapeNoUriTest() - { - var sourceHtml = "<<hogehoge>>"; - - var expected = ("<>", (Uri?)null); - Assert.Equal(expected, Twitter.ParseSource(sourceHtml)); - } - - [Fact] - public void GetQuoteTweetStatusIds_EntityTest() - { - var entities = new[] - { - new TwitterEntityUrl - { - Url = "https://t.co/3HXq0LrbJb", - ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640", - }, - }; - - var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink: null); - Assert.Equal(new[] { 599261132361072640L }, statusIds); - } - - [Fact] - public void GetQuoteTweetStatusIds_QuotedStatusLinkTest() - { - var entities = new TwitterEntities(); - var quotedStatusLink = new TwitterQuotedStatusPermalink - { - Url = "https://t.co/3HXq0LrbJb", - Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640", - }; - - var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink); - Assert.Equal(new[] { 599261132361072640L }, statusIds); - } - - [Fact] - public void GetQuoteTweetStatusIds_UrlStringTest() - { - var urls = new[] - { - "https://twitter.com/kim_upsilon/status/599261132361072640", - }; - - var statusIds = Twitter.GetQuoteTweetStatusIds(urls); - Assert.Equal(new[] { 599261132361072640L }, statusIds); - } - - [Fact] - public void GetQuoteTweetStatusIds_OverflowTest() - { - var urls = new[] - { - // 符号付き 64 ビット整数の範囲を超える値 - "https://twitter.com/kim_upsilon/status/9999999999999999999", - }; - - var statusIds = Twitter.GetQuoteTweetStatusIds(urls); - Assert.Empty(statusIds); - } - [Fact] public void GetApiResultCount_DefaultTest() { var oldInstance = SettingManagerTest.Common; SettingManagerTest.Common = new SettingCommon(); - var timeline = SettingManager.Common.CountApi; - var reply = SettingManager.Common.CountApiReply; - var more = SettingManager.Common.MoreCountApi; - var startup = SettingManager.Common.FirstCountApi; - var favorite = SettingManager.Common.FavoritesCountApi; - var list = SettingManager.Common.ListCountApi; - var search = SettingManager.Common.SearchCountApi; - var usertl = SettingManager.Common.UserTimelineCountApi; + var timeline = SettingManager.Instance.Common.CountApi; + var reply = SettingManager.Instance.Common.CountApiReply; + var more = SettingManager.Instance.Common.MoreCountApi; + var startup = SettingManager.Instance.Common.FirstCountApi; + var favorite = SettingManager.Instance.Common.FavoritesCountApi; + var list = SettingManager.Instance.Common.ListCountApi; + var search = SettingManager.Instance.Common.SearchCountApi; + var usertl = SettingManager.Instance.Common.UserTimelineCountApi; // デフォルト値チェック - Assert.False(SettingManager.Common.UseAdditionalCount); + Assert.False(SettingManager.Instance.Common.UseAdditionalCount); Assert.Equal(60, timeline); Assert.Equal(40, reply); Assert.Equal(200, more); @@ -471,33 +153,33 @@ public void GetApiResultCount_AdditionalCountTest() var oldInstance = SettingManagerTest.Common; SettingManagerTest.Common = new SettingCommon(); - var timeline = SettingManager.Common.CountApi; - var reply = SettingManager.Common.CountApiReply; - var more = SettingManager.Common.MoreCountApi; - var startup = SettingManager.Common.FirstCountApi; - var favorite = SettingManager.Common.FavoritesCountApi; - var list = SettingManager.Common.ListCountApi; - var search = SettingManager.Common.SearchCountApi; - var usertl = SettingManager.Common.UserTimelineCountApi; + var timeline = SettingManager.Instance.Common.CountApi; + var reply = SettingManager.Instance.Common.CountApiReply; + var more = SettingManager.Instance.Common.MoreCountApi; + var startup = SettingManager.Instance.Common.FirstCountApi; + var favorite = SettingManager.Instance.Common.FavoritesCountApi; + var list = SettingManager.Instance.Common.ListCountApi; + var search = SettingManager.Instance.Common.SearchCountApi; + var usertl = SettingManager.Instance.Common.UserTimelineCountApi; - SettingManager.Common.UseAdditionalCount = true; + SettingManager.Instance.Common.UseAdditionalCount = true; // Timeline Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, false, false)); - Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, true, false)); + Assert.Equal(100, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, true, false)); // 100 件が上限 Assert.Equal(startup, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, false, true)); // Reply Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, false)); Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, true, false)); - Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, true)); //Replyの値が使われる + Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, true)); // Replyの値が使われる // Favorites Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, false)); Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, true, false)); Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, true)); - SettingManager.Common.FavoritesCountApi = 0; + SettingManager.Instance.Common.FavoritesCountApi = 0; Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, false)); Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, true, false)); @@ -508,7 +190,7 @@ public void GetApiResultCount_AdditionalCountTest() Assert.Equal(list, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, true, false)); Assert.Equal(list, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, false, true)); - SettingManager.Common.ListCountApi = 0; + SettingManager.Instance.Common.ListCountApi = 0; Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, false, false)); Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, true, false)); @@ -519,10 +201,10 @@ public void GetApiResultCount_AdditionalCountTest() Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false)); Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, true)); - SettingManager.Common.SearchCountApi = 0; + SettingManager.Instance.Common.SearchCountApi = 0; Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, false)); - Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false)); //MoreCountApiの値がPublicSearchの最大値に制限される + Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false)); // MoreCountApiの値がPublicSearchの最大値に制限される Assert.Equal(startup, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, true)); // UserTimeline @@ -530,7 +212,7 @@ public void GetApiResultCount_AdditionalCountTest() Assert.Equal(usertl, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, true, false)); Assert.Equal(usertl, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, false, true)); - SettingManager.Common.UserTimelineCountApi = 0; + SettingManager.Instance.Common.UserTimelineCountApi = 0; Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, false, false)); Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, true, false)); @@ -645,5 +327,28 @@ public void GetTextLengthRemain_BrokenSurrogateTest() Assert.Equal(278, twitter.GetTextLengthRemain("\ud83d")); Assert.Equal(9999, twitter.GetTextLengthRemain("D twitter \ud83d")); } + + [Theory] + [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "normal", "https://pbs.twimg.com/profile_images/00000/foo_normal.jpg")] + [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "bigger", "https://pbs.twimg.com/profile_images/00000/foo_bigger.jpg")] + [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "mini", "https://pbs.twimg.com/profile_images/00000/foo_mini.jpg")] + [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "original", "https://pbs.twimg.com/profile_images/00000/foo.jpg")] + [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal_bar_normal.jpg", "original", "https://pbs.twimg.com/profile_images/00000/foo_normal_bar.jpg")] + public void CreateProfileImageUrl_Test(string normalUrl, string size, string expected) + => Assert.Equal(expected, Twitter.CreateProfileImageUrl(normalUrl, size)); + + [Fact] + public void CreateProfileImageUrl_InvalidSizeTest() + => Assert.Throws(() => Twitter.CreateProfileImageUrl("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "INVALID")); + + [Theory] + [InlineData(24, "mini")] + [InlineData(25, "normal")] + [InlineData(48, "normal")] + [InlineData(49, "bigger")] + [InlineData(73, "bigger")] + [InlineData(74, "original")] + public void DecideProfileImageSize_Test(int sizePx, string expected) + => Assert.Equal(expected, Twitter.DecideProfileImageSize(sizePx)); } } diff --git a/OpenTween.sln b/OpenTween.sln index 0f6d39232..126220171 100644 --- a/OpenTween.sln +++ b/OpenTween.sln @@ -1,12 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29215.179 -MinimumVisualStudioVersion = 16.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32319.34 +MinimumVisualStudioVersion = 17.0 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTween", "OpenTween\OpenTween.csproj", "{3D8995C7-BDF3-4273-9F9D-DDD902F6A101}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTween.Tests", "OpenTween.Tests\OpenTween.Tests.csproj", "{18A32642-A8F3-425B-978D-0C6F630EDDE8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8B1F66B9-8C63-4236-AB06-F5F19C20EE71}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + stylecop.json = stylecop.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,4 +45,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DDF8A3B3-E5D3-4853-84B5-3B3ADCB356C0} + EndGlobalSection EndGlobal diff --git a/OpenTween/Api/ApiLimit.cs b/OpenTween/Api/ApiLimit.cs index 25d605fd0..16633e115 100644 --- a/OpenTween/Api/ApiLimit.cs +++ b/OpenTween/Api/ApiLimit.cs @@ -28,49 +28,12 @@ namespace OpenTween.Api { - public class ApiLimit - { - /// - /// API 実行回数制限の値 - /// - public int AccessLimitCount { get; } - - /// - /// API 実行回数制限までの残回数 - /// - public int AccessLimitRemain { get; } - - /// - /// API 実行回数制限がリセットされる日時 - /// - public DateTimeUtc AccessLimitResetDate { get; } - - /// - /// API 実行回数制限値を取得した日時 - /// - public DateTimeUtc UpdatedAt { get; } - - public ApiLimit(int limitCount, int limitRemain, DateTimeUtc resetDate) - : this(limitCount, limitRemain, resetDate, DateTimeUtc.Now) - { - } - - public ApiLimit(int limitCount, int limitRemain, DateTimeUtc resetDate, DateTimeUtc updatedAt) - { - this.AccessLimitCount = limitCount; - this.AccessLimitRemain = limitRemain; - this.AccessLimitResetDate = resetDate; - this.UpdatedAt = updatedAt; - } - - public override bool Equals(object? obj) - => this.Equals(obj as ApiLimit); - - public bool Equals(ApiLimit? obj) - => obj != null && this.AccessLimitCount == obj.AccessLimitCount && - this.AccessLimitRemain == obj.AccessLimitRemain && this.AccessLimitResetDate == obj.AccessLimitResetDate; - - public override int GetHashCode() - => this.AccessLimitCount ^ this.AccessLimitRemain ^ this.AccessLimitResetDate.GetHashCode(); - } + /// API 実行回数制限の値 + /// API 実行回数制限までの残回数 + /// API 実行回数制限がリセットされる日時 + public record ApiLimit( + int AccessLimitCount, + int AccessLimitRemain, + DateTimeUtc AccessLimitResetDate + ); } diff --git a/OpenTween/Api/BitlyApi.cs b/OpenTween/Api/BitlyApi.cs index 8ec9dc037..922b1a13b 100644 --- a/OpenTween/Api/BitlyApi.cs +++ b/OpenTween/Api/BitlyApi.cs @@ -38,17 +38,19 @@ namespace OpenTween.Api { public class BitlyApi { - public static readonly Uri ApiBase = new Uri("https://api-ssl.bitly.com/"); + public static readonly Uri ApiBase = new("https://api-ssl.bitly.com/"); public string EndUserAccessToken { get; set; } = ""; public string EndUserLoginName { get; set; } = ""; + public string EndUserApiKey { get; set; } = ""; private readonly ApiKey clientId; private readonly ApiKey clientSecret; - private HttpClient http => this.localHttpClient ?? Networking.Http; + private HttpClient Http => this.localHttpClient ?? Networking.Http; + private readonly HttpClient? localHttpClient; public BitlyApi() @@ -90,7 +92,7 @@ public async Task GetAsync(Uri endpoint, IEnumerable GetAccessTokenAsync(string username, string password) request.Content = postContent; - using var response = await this.http.SendAsync(request) + using var response = await this.Http.SendAsync(request) .ConfigureAwait(false); var responseBytes = await response.Content.ReadAsByteArrayAsync() .ConfigureAwait(false); diff --git a/OpenTween/Api/DataModel/TwitterEntities.cs b/OpenTween/Api/DataModel/TwitterEntities.cs new file mode 100644 index 000000000..83b77d53b --- /dev/null +++ b/OpenTween/Api/DataModel/TwitterEntities.cs @@ -0,0 +1,79 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2014 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +#nullable enable annotations + +namespace OpenTween.Api.DataModel +{ + // 参照: https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object + + [DataContract] + public class TwitterEntities : IEnumerable + { + [DataMember(Name = "hashtags", IsRequired = false)] + public TwitterEntityHashtag[]? Hashtags { get; set; } + + [DataMember(Name = "media", IsRequired = false)] + public TwitterEntityMedia[]? Media { get; set; } + + [DataMember(Name = "symbols", IsRequired = false)] + public TwitterEntitySymbol[]? Symbols { get; set; } + + [DataMember(Name = "urls", IsRequired = false)] + public TwitterEntityUrl[]? Urls { get; set; } + + [DataMember(Name = "user_mentions", IsRequired = false)] + public TwitterEntityMention[]? UserMentions { get; set; } + + public IEnumerator GetEnumerator() + { + var entities = Enumerable.Empty(); + + if (this.Hashtags != null) + entities = entities.Concat(this.Hashtags); + + if (this.Media != null) + entities = entities.Concat(this.Media); + + if (this.Symbols != null) + entities = entities.Concat(this.Symbols); + + if (this.Urls != null) + entities = entities.Concat(this.Urls); + + if (this.UserMentions != null) + entities = entities.Concat(this.UserMentions); + + return entities.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + => this.GetEnumerator(); + } +} diff --git a/OpenTween/Api/DataModel/TwitterEntity.cs b/OpenTween/Api/DataModel/TwitterEntity.cs index 0220aba37..cd7e2e0cf 100644 --- a/OpenTween/Api/DataModel/TwitterEntity.cs +++ b/OpenTween/Api/DataModel/TwitterEntity.cs @@ -33,50 +33,6 @@ namespace OpenTween.Api.DataModel { // 参照: https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object - [DataContract] - public class TwitterEntities : IEnumerable - { - [DataMember(Name = "hashtags", IsRequired = false)] - public TwitterEntityHashtag[]? Hashtags { get; set; } - - [DataMember(Name = "media", IsRequired = false)] - public TwitterEntityMedia[]? Media { get; set; } - - [DataMember(Name = "symbols", IsRequired = false)] - public TwitterEntitySymbol[]? Symbols { get; set; } - - [DataMember(Name = "urls", IsRequired = false)] - public TwitterEntityUrl[]? Urls { get; set; } - - [DataMember(Name = "user_mentions", IsRequired = false)] - public TwitterEntityMention[]? UserMentions { get; set; } - - public IEnumerator GetEnumerator() - { - var entities = Enumerable.Empty(); - - if (this.Hashtags != null) - entities = entities.Concat(this.Hashtags); - - if (this.Media != null) - entities = entities.Concat(this.Media); - - if (this.Symbols != null) - entities = entities.Concat(this.Symbols); - - if (this.Urls != null) - entities = entities.Concat(this.Urls); - - if (this.UserMentions != null) - entities = entities.Concat(this.UserMentions); - - return entities.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - => this.GetEnumerator(); - } - [DataContract] public abstract class TwitterEntity { diff --git a/OpenTween/Api/DataModel/TwitterMessageEvent.cs b/OpenTween/Api/DataModel/TwitterMessageEvent.cs index c1883aa63..129041195 100644 --- a/OpenTween/Api/DataModel/TwitterMessageEvent.cs +++ b/OpenTween/Api/DataModel/TwitterMessageEvent.cs @@ -27,39 +27,6 @@ namespace OpenTween.Api.DataModel { - [DataContract] - public class TwitterMessageEventList - { - [DataMember(Name = "apps")] - public Dictionary Apps { get; set; } - - [DataContract] - public class App - { - [DataMember(Name = "id")] - public string Id { get; set; } - - [DataMember(Name = "name")] - public string Name { get; set; } - - [DataMember(Name = "url")] - public string Url { get; set; } - } - - [DataMember(Name = "events")] - public TwitterMessageEvent[] Events { get; set; } - - [DataMember(Name = "next_cursor", IsRequired = false)] - public string? NextCursor { get; set; } - } - - [DataContract] - public class TwitterMessageEventSingle - { - [DataMember(Name = "event")] - public TwitterMessageEvent Event { get; set; } - } - [DataContract] public class TwitterMessageEvent { diff --git a/OpenTween/Api/DataModel/TwitterMessageEventList.cs b/OpenTween/Api/DataModel/TwitterMessageEventList.cs new file mode 100644 index 000000000..ecae1719a --- /dev/null +++ b/OpenTween/Api/DataModel/TwitterMessageEventList.cs @@ -0,0 +1,65 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2018 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable annotations + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTween.Api.DataModel +{ + [DataContract] + public class TwitterMessageEventList + { + [DataMember(Name = "apps")] + public Dictionary Apps { get; set; } + + [DataContract] + public class App + { + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + } + + [DataMember(Name = "events")] + public TwitterMessageEvent[] Events { get; set; } + + [DataMember(Name = "next_cursor", IsRequired = false)] + public string? NextCursor { get; set; } + } + + [DataContract] + public class TwitterMessageEventSingle + { + [DataMember(Name = "event")] + public TwitterMessageEvent Event { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/TwitterStreamMessage.cs b/OpenTween/Api/DataModel/TwitterStreamMessage.cs index f2b46a95a..4a7d2a615 100644 --- a/OpenTween/Api/DataModel/TwitterStreamMessage.cs +++ b/OpenTween/Api/DataModel/TwitterStreamMessage.cs @@ -20,6 +20,7 @@ // Boston, MA 02110-1301, USA. #nullable enable annotations +#pragma warning disable SA1649 using System.Runtime.Serialization; @@ -37,7 +38,7 @@ public StreamMessageStatus(TwitterStatusCompat status) => this.Status = status; public static StreamMessageStatus ParseJson(string json) - => new StreamMessageStatus(TwitterStatusCompat.ParseJson(json)); + => new(TwitterStatusCompat.ParseJson(json)); } public class StreamMessageKeepAlive : ITwitterStreamMessage diff --git a/OpenTween/Api/DataModel/TwitterUploadMediaResult.cs b/OpenTween/Api/DataModel/TwitterUploadMediaResult.cs index 1a57a9a22..91e10fe67 100644 --- a/OpenTween/Api/DataModel/TwitterUploadMediaResult.cs +++ b/OpenTween/Api/DataModel/TwitterUploadMediaResult.cs @@ -20,6 +20,7 @@ // Boston, MA 02110-1301, USA. #nullable enable annotations +#pragma warning disable SA1649 using System; using System.Collections.Generic; diff --git a/OpenTween/Api/DataModel/TwitterV2TweetId.cs b/OpenTween/Api/DataModel/TwitterV2TweetId.cs new file mode 100644 index 000000000..cdec92859 --- /dev/null +++ b/OpenTween/Api/DataModel/TwitterV2TweetId.cs @@ -0,0 +1,34 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable annotations + +using System.Runtime.Serialization; + +namespace OpenTween.Api.DataModel +{ + [DataContract] + public class TwitterV2TweetId + { + [DataMember(Name = "id")] + public string Id { get; set; } + } +} diff --git a/OpenTween/Api/DataModel/TwitterV2TweetIds.cs b/OpenTween/Api/DataModel/TwitterV2TweetIds.cs new file mode 100644 index 000000000..6b61b9c4d --- /dev/null +++ b/OpenTween/Api/DataModel/TwitterV2TweetIds.cs @@ -0,0 +1,34 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable annotations + +using System.Runtime.Serialization; + +namespace OpenTween.Api.DataModel +{ + [DataContract] + public class TwitterV2TweetIds + { + [DataMember(Name = "data")] + public TwitterV2TweetId[] Data { get; set; } + } +} diff --git a/OpenTween/Api/ImgurApi.cs b/OpenTween/Api/ImgurApi.cs index 0b9003ed3..591bec315 100644 --- a/OpenTween/Api/ImgurApi.cs +++ b/OpenTween/Api/ImgurApi.cs @@ -40,7 +40,7 @@ public class ImgurApi : IImgurApi private readonly ApiKey clientId; private readonly HttpClient http; - public static readonly Uri UploadEndpoint = new Uri("https://api.imgur.com/3/image.xml"); + public static readonly Uri UploadEndpoint = new("https://api.imgur.com/3/image.xml"); public ImgurApi() : this(ApplicationSettings.ImgurClientId, null) diff --git a/OpenTween/Api/MicrosoftTranslatorApi.cs b/OpenTween/Api/MicrosoftTranslatorApi.cs index 7f5e16060..363946841 100644 --- a/OpenTween/Api/MicrosoftTranslatorApi.cs +++ b/OpenTween/Api/MicrosoftTranslatorApi.cs @@ -38,15 +38,17 @@ namespace OpenTween.Api { public class MicrosoftTranslatorApi { - public static readonly Uri IssueTokenEndpoint = new Uri("https://api.cognitive.microsoft.com/sts/v1.0/issueToken"); - public static readonly Uri TranslateEndpoint = new Uri("https://api.cognitive.microsofttranslator.com/translate"); + public static readonly Uri IssueTokenEndpoint = new("https://api.cognitive.microsoft.com/sts/v1.0/issueToken"); + public static readonly Uri TranslateEndpoint = new("https://api.cognitive.microsofttranslator.com/translate"); public string AccessToken { get; internal set; } = ""; + public DateTimeUtc RefreshAccessTokenAt { get; internal set; } = DateTimeUtc.MinValue; private readonly ApiKey subscriptionKey; private HttpClient Http => this.localHttpClient ?? Networking.Http; + private readonly HttpClient? localHttpClient; public MicrosoftTranslatorApi() diff --git a/OpenTween/Api/MobypictureApi.cs b/OpenTween/Api/MobypictureApi.cs index 255267e6e..d76d4347c 100644 --- a/OpenTween/Api/MobypictureApi.cs +++ b/OpenTween/Api/MobypictureApi.cs @@ -39,10 +39,10 @@ public class MobypictureApi : IMobypictureApi private readonly ApiKey apiKey; private readonly HttpClient http; - public static readonly Uri UploadEndpoint = new Uri("https://api.mobypicture.com/2.0/upload.xml"); + public static readonly Uri UploadEndpoint = new("https://api.mobypicture.com/2.0/upload.xml"); - private static readonly Uri OAuthRealm = new Uri("http://api.twitter.com/"); - private static readonly Uri AuthServiceProvider = new Uri("https://api.twitter.com/1.1/account/verify_credentials.json"); + private static readonly Uri OAuthRealm = new("http://api.twitter.com/"); + private static readonly Uri AuthServiceProvider = new("https://api.twitter.com/1.1/account/verify_credentials.json"); public MobypictureApi(TwitterApi twitterApi) : this(ApplicationSettings.MobypictureKey, twitterApi) diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 928de6f6a..1f5c7ce9f 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -36,11 +36,12 @@ namespace OpenTween.Api public sealed class TwitterApi : IDisposable { public long CurrentUserId { get; private set; } + public string CurrentScreenName { get; private set; } = ""; - public IApiConnection Connection => this.apiConnection ?? throw new InvalidOperationException(); + public IApiConnection Connection => this.ApiConnection ?? throw new InvalidOperationException(); - internal IApiConnection? apiConnection; + internal IApiConnection? ApiConnection; private readonly ApiKey consumerKey; private readonly ApiKey consumerSecret; @@ -54,7 +55,7 @@ public TwitterApi(ApiKey consumerKey, ApiKey consumerSecret) public void Initialize(string accessToken, string accessSecret, long userId, string screenName) { var newInstance = new TwitterApiConnection(this.consumerKey, this.consumerSecret, accessToken, accessSecret); - var oldInstance = Interlocked.Exchange(ref this.apiConnection, newInstance); + var oldInstance = Interlocked.Exchange(ref this.ApiConnection, newInstance); oldInstance?.Dispose(); this.CurrentUserId = userId; @@ -137,8 +138,27 @@ public Task StatusesShow(long statusId) return this.Connection.GetAsync(endpoint, param, "/statuses/show/:id"); } - public Task> StatusesUpdate(string status, long? replyToId, IReadOnlyList? mediaIds, - bool? autoPopulateReplyMetadata = null, IReadOnlyList? excludeReplyUserIds = null, string? attachmentUrl = null) + public Task StatusesLookup(IReadOnlyList statusIds) + { + var endpoint = new Uri("statuses/lookup.json", UriKind.Relative); + var param = new Dictionary + { + ["id"] = string.Join(",", statusIds), + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + + return this.Connection.GetAsync(endpoint, param, "/statuses/lookup"); + } + + public Task> StatusesUpdate( + string status, + long? replyToId, + IReadOnlyList? mediaIds, + bool? autoPopulateReplyMetadata = null, + IReadOnlyList? excludeReplyUserIds = null, + string? attachmentUrl = null) { var endpoint = new Uri("statuses/update.json", UriKind.Relative); var param = new Dictionary @@ -783,6 +803,6 @@ public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? rea => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(authServiceProvider, realm); public void Dispose() - => this.apiConnection?.Dispose(); + => this.ApiConnection?.Dispose(); } } diff --git a/OpenTween/Api/TwitterApiException.cs b/OpenTween/Api/TwitterApiException.cs index fd25a8e9b..e2b9de049 100644 --- a/OpenTween/Api/TwitterApiException.cs +++ b/OpenTween/Api/TwitterApiException.cs @@ -37,6 +37,7 @@ namespace OpenTween.Api public class TwitterApiException : WebApiException { public HttpStatusCode StatusCode { get; } + public TwitterError? ErrorResponse { get; } public TwitterErrorItem[] Errors @@ -90,13 +91,13 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } public static TwitterApiException CreateFromException(HttpRequestException ex) - => new TwitterApiException(ex.InnerException?.Message ?? ex.Message, ex); + => new(ex.InnerException?.Message ?? ex.Message, ex); public static TwitterApiException CreateFromException(OperationCanceledException ex) - => new TwitterApiException("Timeout", ex); + => new("Timeout", ex); public static TwitterApiException CreateFromException(SerializationException ex, string responseText) - => new TwitterApiException("Invalid JSON", responseText, ex); + => new("Invalid JSON", responseText, ex); private static string FormatTwitterError(TwitterError error) => string.Join(",", error.Errors.Select(x => x.ToString())); diff --git a/OpenTween/Api/TwitterApiStatus.cs b/OpenTween/Api/TwitterApiStatus.cs index 628265bd5..27a318cdc 100644 --- a/OpenTween/Api/TwitterApiStatus.cs +++ b/OpenTween/Api/TwitterApiStatus.cs @@ -22,24 +22,26 @@ #nullable enable using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections; using System.Linq; +using System.Net.Http.Headers; using System.Runtime.Serialization.Json; using System.Text; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using OpenTween.Api.DataModel; -using System.Net.Http.Headers; namespace OpenTween.Api { public class TwitterApiStatus { public TwitterApiAccessLevel AccessLevel { get; set; } + public EndpointLimits AccessLimit { get; } + public ApiLimit? MediaUploadLimit { get; set; } public class AccessLimitUpdatedEventArgs : EventArgs @@ -49,6 +51,7 @@ public class AccessLimitUpdatedEventArgs : EventArgs public AccessLimitUpdatedEventArgs(string? endpointName) => this.EndpointName = endpointName; } + public event EventHandler? AccessLimitUpdated; public TwitterApiStatus() @@ -154,7 +157,7 @@ public class EndpointLimits : IEnumerable> public TwitterApiStatus Owner { get; } private readonly ConcurrentDictionary innerDict - = new ConcurrentDictionary(); + = new(); public EndpointLimits(TwitterApiStatus owner) => this.Owner = owner; diff --git a/OpenTween/Api/TwitterStreamObservable.cs b/OpenTween/Api/TwitterStreamObservable.cs index 815f06d26..89ba38e8a 100644 --- a/OpenTween/Api/TwitterStreamObservable.cs +++ b/OpenTween/Api/TwitterStreamObservable.cs @@ -21,7 +21,6 @@ #nullable enable -using OpenTween.Api.DataModel; using System; using System.IO; using System.Linq; @@ -32,6 +31,7 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; +using OpenTween.Api.DataModel; namespace OpenTween.Api { diff --git a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs new file mode 100644 index 000000000..fcc537d6b --- /dev/null +++ b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs @@ -0,0 +1,79 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Api.DataModel; +using OpenTween.Connection; + +namespace OpenTween.Api.TwitterV2 +{ + public class GetTimelineRequest + { + public static readonly string EndpointName = "/2/users/:id/timelines/reverse_chronological"; + + public long UserId { get; set; } + + public int? MaxResults { get; set; } + + public string? UntilId { get; set; } + + public string? SinceId { get; set; } + + public GetTimelineRequest(long userId) + => this.UserId = userId; + + private Uri CreateEndpointUri() + => new($"/2/users/{this.UserId}/timelines/reverse_chronological", UriKind.Relative); + + private Dictionary CreateParameters() + { + var param = new Dictionary + { + ["tweet.fields"] = "id", + }; + + if (this.MaxResults != null) + param["max_results"] = this.MaxResults.ToString(); + + if (this.UntilId != null) + param["until_id"] = this.UntilId; + + if (this.SinceId != null) + param["since_id"] = this.SinceId; + + return param; + } + + public Task Send(IApiConnection apiConnection) + { + var uri = this.CreateEndpointUri(); + var param = this.CreateParameters(); + + return apiConnection.GetAsync(uri, param, EndpointName); + } + } +} diff --git a/OpenTween/ApiInfoDialog.cs b/OpenTween/ApiInfoDialog.cs index d722c7ed8..37192c661 100644 --- a/OpenTween/ApiInfoDialog.cs +++ b/OpenTween/ApiInfoDialog.cs @@ -1,19 +1,19 @@ // OpenTween - Client of Twitter // Copyright (c) 2015 spx (@5px) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -31,6 +31,7 @@ using System.Threading.Tasks; using System.Windows.Forms; using OpenTween.Api; +using OpenTween.Api.TwitterV2; namespace OpenTween { @@ -39,9 +40,9 @@ public partial class ApiInfoDialog : OTBaseForm public ApiInfoDialog() => this.InitializeComponent(); - private readonly List _tlEndpoints = new List + private readonly List tlEndpoints = new() { - "/statuses/home_timeline", + GetTimelineRequest.EndpointName, "/statuses/mentions_timeline", "/statuses/show/:id", "/statuses/user_timeline", @@ -57,21 +58,21 @@ private void ApiInfoDialog_Shown(object sender, EventArgs e) { // TL更新用エンドポイントの追加 var group = this.ListViewApi.Groups[0]; - foreach (var endpoint in _tlEndpoints) + foreach (var endpoint in this.tlEndpoints) { var apiLimit = MyCommon.TwitterApiInfo.AccessLimit[endpoint]; if (apiLimit == null) continue; - AddListViewItem(endpoint, apiLimit, group); + this.AddListViewItem(endpoint, apiLimit, group); } // その他 group = this.ListViewApi.Groups[1]; - var apiStatuses = MyCommon.TwitterApiInfo.AccessLimit.Where(x => !_tlEndpoints.Contains(x.Key)).OrderBy(x => x.Key); + var apiStatuses = MyCommon.TwitterApiInfo.AccessLimit.Where(x => !this.tlEndpoints.Contains(x.Key)).OrderBy(x => x.Key); foreach (var (endpoint, apiLimit) in apiStatuses) { - AddListViewItem(endpoint, apiLimit, group); + this.AddListViewItem(endpoint, apiLimit, group); } MyCommon.TwitterApiInfo.AccessLimitUpdated += this.TwitterApiStatus_AccessLimitUpdated; @@ -116,7 +117,7 @@ private async void TwitterApiStatus_AccessLimitUpdated(object sender, EventArgs { var endpoint = ((TwitterApiStatus.AccessLimitUpdatedEventArgs)e).EndpointName; if (endpoint != null) - UpdateEndpointLimit(endpoint); + this.UpdateEndpointLimit(endpoint); } } catch (ObjectDisposedException) diff --git a/OpenTween/ApiKey.cs b/OpenTween/ApiKey.cs index fb8246f39..c4e968263 100644 --- a/OpenTween/ApiKey.cs +++ b/OpenTween/ApiKey.cs @@ -65,7 +65,7 @@ private ApiKey(string password, string rawKey) /// /// 成功した場合は true、暗号化された API キーの復号に失敗した場合は false を返します /// - public bool TryGetValue([NotNullWhen(true)]out string output) + public bool TryGetValue([NotNullWhen(true)]out string? output) { try { @@ -74,7 +74,7 @@ public bool TryGetValue([NotNullWhen(true)]out string output) } catch (ApiKeyDecryptException) { - output = null!; + output = null; return false; } } @@ -89,7 +89,7 @@ public static ApiKey Create(string rawKey) /// インスタンスを作成します /// public static ApiKey Create(string password, string rawKey) - => new ApiKey(password, rawKey); + => new(password, rawKey); /// /// 指定された文字列を暗号化して返します @@ -187,7 +187,7 @@ private static byte[] GenerateSalt() return salt; } - private static (byte[], byte[], byte[]) GenerateKeyAndIV(string password, byte[] salt) + private static (byte[] EncryptionKey, byte[] IV, byte[] MacKey) GenerateKeyAndIV(string password, byte[] salt) { using var generator = new Rfc2898DeriveBytes(password, salt, IterationCount, HashAlgorithm); var encryptionKey = generator.GetBytes(KeySize); @@ -215,7 +215,9 @@ private static Aes CreateAes() public static class ApiKeyExtensions { +#pragma warning disable SA1141 public static bool TryGetValue(this ValueTuple apiKeys, out ValueTuple decryptedKeys) +#pragma warning restore SA1141 { var (apiKey1, apiKey2) = apiKeys; if (apiKey1.TryGetValue(out var decrypted1) && apiKey2.TryGetValue(out var decrypted2)) @@ -232,9 +234,23 @@ public static bool TryGetValue(this ValueTuple apiKeys, out Valu [Serializable] public class ApiKeyDecryptException : Exception { - public ApiKeyDecryptException() { } - public ApiKeyDecryptException(string message) : base(message) { } - public ApiKeyDecryptException(string message, Exception innerException) : base(message, innerException) { } - protected ApiKeyDecryptException(SerializationInfo info, StreamingContext context) : base(info, context) { } + public ApiKeyDecryptException() + { + } + + public ApiKeyDecryptException(string message) + : base(message) + { + } + + public ApiKeyDecryptException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected ApiKeyDecryptException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/OpenTween/AppendSettingDialog.cs b/OpenTween/AppendSettingDialog.cs index c6b60b59f..1c644813e 100644 --- a/OpenTween/AppendSettingDialog.cs +++ b/OpenTween/AppendSettingDialog.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -31,18 +31,17 @@ using System.ComponentModel; using System.Data; using System.Drawing; +using System.IO; using System.Linq; using System.Net.Http; +using System.Resources; using System.Text; -using System.Windows.Forms; using System.Threading; -using System.IO; -using System.Resources; -using OpenTween.Api; -using OpenTween.Connection; -using OpenTween.Thumbnail; using System.Threading.Tasks; +using System.Windows.Forms; +using OpenTween.Connection; using OpenTween.Setting.Panel; +using OpenTween.Thumbnail; namespace OpenTween { @@ -50,9 +49,6 @@ public partial class AppendSettingDialog : OTBaseForm { public event EventHandler? IntervalChanged; - internal Twitter tw = null!; - internal TwitterApi twitterApi = null!; - public AppendSettingDialog() { this.InitializeComponent(); @@ -82,12 +78,6 @@ public void LoadConfig(SettingCommon settingCommon, SettingLocal settingLocal) this.CooperatePanel.LoadConfig(settingCommon); this.ConnectionPanel.LoadConfig(settingCommon); this.NotifyPanel.LoadConfig(settingCommon); - - var activeUser = settingCommon.UserAccounts.FirstOrDefault(x => x.UserId == this.tw.UserId); - if (activeUser != null) - { - this.BasedPanel.AuthUserCombo.SelectedItem = activeUser; - } } public void SaveConfig(SettingCommon settingCommon, SettingLocal settingLocal) @@ -107,18 +97,6 @@ public void SaveConfig(SettingCommon settingCommon, SettingLocal settingLocal) this.CooperatePanel.SaveConfig(settingCommon); this.ConnectionPanel.SaveConfig(settingCommon); this.NotifyPanel.SaveConfig(settingCommon); - - var userAccountIdx = this.BasedPanel.AuthUserCombo.SelectedIndex; - if (userAccountIdx != -1) - { - var u = settingCommon.UserAccounts[userAccountIdx]; - this.tw.Initialize(u.Token, u.TokenSecret, u.Username, u.UserId); - } - else - { - this.tw.ClearAuthInfo(); - this.tw.Initialize("", "", "", 0); - } } private void TreeViewSetting_BeforeSelect(object sender, TreeViewCancelEventArgs e) @@ -141,7 +119,7 @@ private void TreeViewSetting_AfterSelect(object sender, TreeViewEventArgs e) private void Setting_FormClosing(object sender, FormClosingEventArgs e) { - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; if (this.BasedPanel.AuthUserCombo.SelectedIndex == -1 && e.CloseReason == CloseReason.None) { @@ -150,9 +128,9 @@ private void Setting_FormClosing(object sender, FormClosingEventArgs e) e.Cancel = true; } } - if (e.Cancel == false && TreeViewSetting.SelectedNode != null) + if (e.Cancel == false && this.TreeViewSetting.SelectedNode != null) { - var curPanel = (SettingPanelBase)TreeViewSetting.SelectedNode.Tag; + var curPanel = (SettingPanelBase)this.TreeViewSetting.SelectedNode.Tag; curPanel.Visible = false; curPanel.Enabled = false; } @@ -160,26 +138,26 @@ private void Setting_FormClosing(object sender, FormClosingEventArgs e) private void Setting_Load(object sender, EventArgs e) { - this.TreeViewSetting.Nodes["BasedNode"].Tag = BasedPanel; - this.TreeViewSetting.Nodes["BasedNode"].Nodes["PeriodNode"].Tag = GetPeriodPanel; - this.TreeViewSetting.Nodes["BasedNode"].Nodes["StartUpNode"].Tag = StartupPanel; - this.TreeViewSetting.Nodes["BasedNode"].Nodes["GetCountNode"].Tag = GetCountPanel; - this.TreeViewSetting.Nodes["ActionNode"].Tag = ActionPanel; - this.TreeViewSetting.Nodes["ActionNode"].Nodes["TweetActNode"].Tag = TweetActPanel; - this.TreeViewSetting.Nodes["PreviewNode"].Tag = PreviewPanel; - this.TreeViewSetting.Nodes["PreviewNode"].Nodes["TweetPrvNode"].Tag = TweetPrvPanel; - this.TreeViewSetting.Nodes["PreviewNode"].Nodes["NotifyNode"].Tag = NotifyPanel; - this.TreeViewSetting.Nodes["FontNode"].Tag = FontPanel; - this.TreeViewSetting.Nodes["FontNode"].Nodes["FontNode2"].Tag = FontPanel2; - this.TreeViewSetting.Nodes["ConnectionNode"].Tag = ConnectionPanel; - this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["ProxyNode"].Tag = ProxyPanel; - this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["CooperateNode"].Tag = CooperatePanel; - this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["ShortUrlNode"].Tag = ShortUrlPanel; + this.TreeViewSetting.Nodes["BasedNode"].Tag = this.BasedPanel; + this.TreeViewSetting.Nodes["BasedNode"].Nodes["PeriodNode"].Tag = this.GetPeriodPanel; + this.TreeViewSetting.Nodes["BasedNode"].Nodes["StartUpNode"].Tag = this.StartupPanel; + this.TreeViewSetting.Nodes["BasedNode"].Nodes["GetCountNode"].Tag = this.GetCountPanel; + this.TreeViewSetting.Nodes["ActionNode"].Tag = this.ActionPanel; + this.TreeViewSetting.Nodes["ActionNode"].Nodes["TweetActNode"].Tag = this.TweetActPanel; + this.TreeViewSetting.Nodes["PreviewNode"].Tag = this.PreviewPanel; + this.TreeViewSetting.Nodes["PreviewNode"].Nodes["TweetPrvNode"].Tag = this.TweetPrvPanel; + this.TreeViewSetting.Nodes["PreviewNode"].Nodes["NotifyNode"].Tag = this.NotifyPanel; + this.TreeViewSetting.Nodes["FontNode"].Tag = this.FontPanel; + this.TreeViewSetting.Nodes["FontNode"].Nodes["FontNode2"].Tag = this.FontPanel2; + this.TreeViewSetting.Nodes["ConnectionNode"].Tag = this.ConnectionPanel; + this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["ProxyNode"].Tag = this.ProxyPanel; + this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["CooperateNode"].Tag = this.CooperatePanel; + this.TreeViewSetting.Nodes["ConnectionNode"].Nodes["ShortUrlNode"].Tag = this.ShortUrlPanel; this.TreeViewSetting.SelectedNode = this.TreeViewSetting.Nodes[0]; this.TreeViewSetting.ExpandAll(); - ActiveControl = BasedPanel.StartAuthButton; + this.ActiveControl = this.BasedPanel.StartAuthButton; } private void UReadMng_CheckedChanged(object sender, EventArgs e) @@ -224,8 +202,11 @@ private async void StartAuthButton_Click(object sender, EventArgs e) authUserCombo.SelectedIndex = idx; - MessageBox.Show(this, Properties.Resources.AuthorizeButton_Click1, - "Authenticate", MessageBoxButtons.OK); + MessageBox.Show( + this, + Properties.Resources.AuthorizeButton_Click1, + "Authenticate", + MessageBoxButtons.OK); } catch (WebApiException ex) { @@ -296,7 +277,8 @@ private void Setting_Shown(object sender, EventArgs e) { Thread.Sleep(10); if (this.Disposing || this.IsDisposed) return; - } while (!this.IsHandleCreated); + } + while (!this.IsHandleCreated); this.TopMost = this.PreviewPanel.CheckAlwaysTop.Checked; this.GetPeriodPanel.LabelPostAndGet.Visible = this.GetPeriodPanel.CheckPostAndGet.Checked; @@ -324,7 +306,7 @@ public class IntervalChangedEventArgs : EventArgs public bool Lists; public bool UserTimeline; - public static IntervalChangedEventArgs ResetAll => new IntervalChangedEventArgs + public static IntervalChangedEventArgs ResetAll => new() { Timeline = true, Reply = true, diff --git a/OpenTween/ApplicationContainer.cs b/OpenTween/ApplicationContainer.cs new file mode 100644 index 000000000..9a101c25d --- /dev/null +++ b/OpenTween/ApplicationContainer.cs @@ -0,0 +1,127 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using OpenTween.Api; +using OpenTween.Models; +using OpenTween.Setting; +using OpenTween.Thumbnail; +using OpenTween.Thumbnail.Services; + +namespace OpenTween +{ + public sealed class ApplicationContainer : IDisposable + { + public bool IsDisposed { get; private set; } = false; + + public SettingManager Settings { get; } + + public TabInformations TabInfo { get; } = TabInformations.GetInstance(); + + public CultureService CultureService + => this.cultureServiceLazy.Value; + + public TwitterApi TwitterApi + => this.twitterApiLazy.Value; + + public Twitter Twitter + => this.twitterLazy.Value; + + public ImageCache ImageCache + => this.imageCacheLazy.Value; + + public IconAssetsManager IconAssetsManager + => this.iconAssetsManagerLazy.Value; + + public ImgAzyobuziNet ImgAzyobuziNet + => this.imgAzyobuziNetLazy.Value; + + public ThumbnailGenerator ThumbnailGenerator + => this.thumbnailGeneratorLazy.Value; + + public TweenMain MainForm + => this.mainFormLazy.Value; + + private readonly Lazy cultureServiceLazy; + private readonly DisposableLazy twitterApiLazy; + private readonly DisposableLazy twitterLazy; + private readonly DisposableLazy imageCacheLazy; + private readonly DisposableLazy iconAssetsManagerLazy; + private readonly DisposableLazy imgAzyobuziNetLazy; + private readonly Lazy thumbnailGeneratorLazy; + private readonly DisposableLazy mainFormLazy; + + public ApplicationContainer(SettingManager settings) + { + this.Settings = settings; + SettingManager.Instance = settings; + + this.cultureServiceLazy = new(this.CreateCultureService); + this.twitterApiLazy = new(this.CreateTwitterApi); + this.twitterLazy = new(this.CreateTwitter); + this.imageCacheLazy = new(this.CreateImageCache); + this.iconAssetsManagerLazy = new(this.CreateIconAssetsManager); + this.imgAzyobuziNetLazy = new(this.CreateImgAzyobuziNet); + this.thumbnailGeneratorLazy = new(this.CreateThumbnailGenerator); + this.mainFormLazy = new(this.CreateTweenMain); + } + + private CultureService CreateCultureService() + => new(this.Settings.Common); + + private TwitterApi CreateTwitterApi() + => new(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret); + + private Twitter CreateTwitter() + => new(this.TwitterApi); + + private ImageCache CreateImageCache() + => new(); + + private IconAssetsManager CreateIconAssetsManager() + => new(); + + private ImgAzyobuziNet CreateImgAzyobuziNet() + => new(autoupdate: false); + + private ThumbnailGenerator CreateThumbnailGenerator() + => new(this.ImgAzyobuziNet); + + private TweenMain CreateTweenMain() + => new(this.Settings, this.TabInfo, this.Twitter, this.ImageCache, this.IconAssetsManager, this.ThumbnailGenerator); + + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + this.mainFormLazy.Dispose(); + this.imgAzyobuziNetLazy.Dispose(); + this.twitterLazy.Dispose(); + this.twitterApiLazy.Dispose(); + this.iconAssetsManagerLazy.Dispose(); + this.imageCacheLazy.Dispose(); + } + } +} diff --git a/OpenTween/ApplicationEvents.cs b/OpenTween/ApplicationEvents.cs index 3d6d619c7..39e75494d 100644 --- a/OpenTween/ApplicationEvents.cs +++ b/OpenTween/ApplicationEvents.cs @@ -7,19 +7,19 @@ // (c) 2012 Egtra (@egtra) // (c) 2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License // for more details. -// +// // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -28,358 +28,112 @@ #nullable enable using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Diagnostics; using System.Windows.Forms; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Globalization; -using System.Reflection; -using Microsoft.Win32; +using OpenTween.Connection; using OpenTween.Setting; -using System.Security.Principal; namespace OpenTween { - internal class MyApplication + internal class ApplicationEvents { - public static readonly CultureInfo[] SupportedUICulture = new[] - { - new CultureInfo("en"), // 先頭のカルチャはフォールバック先として使用される - new CultureInfo("ja"), - }; - /// /// 起動時に指定されたオプションを取得します /// - public static IDictionary StartupOptions { get; private set; } = null!; + public static CommandLineArgs StartupOptions { get; private set; } = null!; /// /// アプリケーションのメイン エントリ ポイントです。 /// [STAThread] - static int Main(string[] args) + public static int Main(string[] args) { - WarnIfApiKeyError(); - WarnIfRunAsAdministrator(); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); - if (!CheckRuntimeVersion()) - { - var message = string.Format(Properties.Resources.CheckRuntimeVersion_Error, ".NET Framework 4.7.2"); - MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error); - return 1; - } + using var errorReportHandler = new ErrorReportHandler(); - StartupOptions = ParseArguments(args); + StartupOptions = new(args); + InitializeTraceFrag(); - if (!SetConfigDirectoryPath()) + if (!ApplicationPreconditions.CheckAll()) return 1; - SettingManager.LoadAll(); - - InitCulture(); - - { - // 同じ設定ファイルを使用する OpenTween プロセスの二重起動を防止する - var pt = MyCommon.settingPath.Replace("\\", "/") + "/" + ApplicationSettings.AssemblyName; - using var mt = new Mutex(false, pt); + var settingsPath = SettingManager.DetermineSettingsPath(StartupOptions); + if (MyCommon.IsNullOrEmpty(settingsPath)) + return 1; - if (!mt.WaitOne(0, false)) - { - var text = string.Format(MyCommon.ReplaceAppName(Properties.Resources.StartupText1), ApplicationSettings.AssemblyName); - MessageBox.Show(text, MyCommon.ReplaceAppName(Properties.Resources.StartupText2), MessageBoxButtons.OK, MessageBoxIcon.Information); + var settings = new SettingManager(settingsPath); + settings.LoadAll(); - TryActivatePreviousWindow(); - return 1; - } + using var container = new ApplicationContainer(settings); - TaskScheduler.UnobservedTaskException += (s, e) => - { - e.SetObserved(); - OnUnhandledException(e.Exception.Flatten()); - }; - Application.ThreadException += (s, e) => OnUnhandledException(e.Exception); - AppDomain.CurrentDomain.UnhandledException += (s, e) => OnUnhandledException((Exception)e.ExceptionObject); - AsyncTimer.UnhandledException += (s, e) => OnUnhandledException(e.Exception); + settings.Common.Validate(); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new TweenMain()); + ThemeManager.ApplyGlobalUIFont(settings.Local); + container.CultureService.Initialize(); - mt.ReleaseMutex(); - } + Networking.Initialize(); + settings.ApplySettings(); - return 0; - } + // 同じ設定ファイルを使用する OpenTween プロセスの二重起動を防止する + using var mutex = new ApplicationInstanceMutex(ApplicationSettings.AssemblyName, settings.SettingsPath); - private static void WarnIfApiKeyError() - { - var canDecrypt = ApplicationSettings.TwitterConsumerKey.TryGetValue(out _); - if (!canDecrypt) + if (mutex.InstanceExists) { - var message = Properties.Resources.WarnIfApiKeyError_Message; - MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); - Environment.Exit(-1); - } - } - - /// - /// OpenTween が管理者権限で実行されている場合に警告を表示します - /// - private static void WarnIfRunAsAdministrator() - { - // UAC が無効なシステムでは警告を表示しない - using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); - using var systemKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\"); - - var enableLUA = (int?)systemKey?.GetValue("EnableLUA"); - if (enableLUA != 1) - return; + var text = string.Format(MyCommon.ReplaceAppName(Properties.Resources.StartupText1), ApplicationSettings.AssemblyName); + MessageBox.Show(text, MyCommon.ReplaceAppName(Properties.Resources.StartupText2), MessageBoxButtons.OK, MessageBoxIcon.Information); - using var currentIdentity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(currentIdentity); - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - var message = string.Format(Properties.Resources.WarnIfRunAsAdministrator_Message, ApplicationSettings.ApplicationName); - MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); + mutex.TryActivatePreviousInstance(); + return 1; } - } - - /// - /// 動作中の .NET Framework のバージョンが適切かチェックします - /// - private static bool CheckRuntimeVersion() - { - // Mono 上で動作している場合はバージョンチェックを無視します - if (Type.GetType("Mono.Runtime", false) != null) - return true; - - // .NET Framework 4.7.2 以降で動作しているかチェックする - // 参照: https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed - - using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); - using var ndpKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"); - - var releaseKey = (int)ndpKey.GetValue("Release"); - return releaseKey >= 461808; - } - - /// - /// “/key:value”形式の起動オプションを解釈し IDictionary に変換する - /// - /// - /// 不正な形式のオプションは除外されます。 - /// また、重複したキーのオプションが入力された場合は末尾に書かれたオプションが採用されます。 - /// - internal static IDictionary ParseArguments(IEnumerable arguments) - { - var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$"); - return arguments.Select(x => optionPattern.Match(x)) - .Where(x => x.Success) - .GroupBy(x => x.Groups[1].Value) - .ToDictionary(x => x.Key, x => x.Last().Groups[2].Value); - } - - private static void TryActivatePreviousWindow() - { - // 実行中の同じアプリケーションのウィンドウ・ハンドルの取得 - var prevProcess = GetPreviousProcess(); - if (prevProcess == null) + if (settings.IsIncomplete) { - return; + var completed = ShowSettingsDialog(settings, container.IconAssetsManager); + if (!completed) + return 1; // 設定が完了しなかったため終了 } - var windowHandle = NativeMethods.GetWindowHandle((uint)prevProcess.Id, ApplicationSettings.ApplicationName); - if (windowHandle != IntPtr.Zero) - { - NativeMethods.SetActiveWindow(windowHandle); - } - } + Application.Run(container.MainForm); - private static Process? GetPreviousProcess() - { - var currentProcess = Process.GetCurrentProcess(); - try - { - return Process.GetProcessesByName(currentProcess.ProcessName) - .Where(p => p.Id != currentProcess.Id) - .FirstOrDefault(p => p.MainModule.FileName.Equals(currentProcess.MainModule.FileName, StringComparison.OrdinalIgnoreCase)); - } - catch - { - return null; - } + return 0; } - private static void OnUnhandledException(Exception ex) + private static void InitializeTraceFrag() { - if (CheckIgnorableError(ex)) - return; - - if (MyCommon.ExceptionOut(ex)) - { - Application.Exit(); - } - } + var traceFlag = false; - /// - /// 無視しても問題のない既知の例外であれば true を返す - /// - private static bool CheckIgnorableError(Exception ex) - { #if DEBUG - return false; -#else - if (ex is AggregateException aggregated) - { - if (aggregated.InnerExceptions.Count != 1) - return false; - - ex = aggregated.InnerExceptions.Single(); - } - - switch (ex) - { - case System.Net.WebException webEx: - // SSL/TLS のネゴシエーションに失敗した場合に発生する。なぜかキャッチできない例外 - // https://osdn.net/ticket/browse.php?group_id=6526&tid=37432 - if (webEx.Status == System.Net.WebExceptionStatus.SecureChannelFailure) - return true; - break; - case System.Threading.Tasks.TaskCanceledException cancelEx: - // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない - // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433 - var stackTrace = new System.Diagnostics.StackTrace(cancelEx); - var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod(); - if (lastFrameMethod.ReflectedType == typeof(Connection.TwitterApiConnection) && - lastFrameMethod.Name == nameof(Connection.TwitterApiConnection.GetStreamAsync)) - return true; - break; - } - - return false; + traceFlag = true; #endif - } - - public static void InitCulture() - { - var currentCulture = CultureInfo.CurrentUICulture; - - var settingCultureStr = SettingManager.Common.Language; - if (settingCultureStr != "OS") - { - try - { - currentCulture = new CultureInfo(settingCultureStr); - } - catch (CultureNotFoundException) { } - } - var preferredCulture = GetPreferredCulture(currentCulture); - CultureInfo.DefaultThreadCurrentUICulture = preferredCulture; - Thread.CurrentThread.CurrentUICulture = preferredCulture; - } + if (StartupOptions.ContainsKey("d")) + traceFlag = true; - /// - /// サポートしているカルチャの中から、指定されたカルチャに対して適切なカルチャを選択して返します - /// - public static CultureInfo GetPreferredCulture(CultureInfo culture) - { - if (SupportedUICulture.Any(x => x.Contains(culture))) - return culture; + var version = Version.Parse(MyCommon.FileVersion); + if (version.Build != 0) + traceFlag = true; - return SupportedUICulture[0]; + MyCommon.TraceFlag = traceFlag; } - private static bool SetConfigDirectoryPath() + private static bool ShowSettingsDialog(SettingManager settings, IconAssetsManager iconAssets) { - if (StartupOptions.TryGetValue("configDir", out var configDir) && !MyCommon.IsNullOrEmpty(configDir)) - { - // 起動オプション /configDir で設定ファイルの参照先を変更できます - if (!Directory.Exists(configDir)) - { - var text = string.Format(Properties.Resources.ConfigDirectoryNotExist, configDir); - MessageBox.Show(text, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error); - return false; - } - - MyCommon.settingPath = Path.GetFullPath(configDir); - } - else - { - // OpenTween.exe と同じディレクトリに設定ファイルを配置する - MyCommon.settingPath = Application.StartupPath; + using var settingDialog = new AppendSettingDialog(); + settingDialog.Icon = iconAssets.IconMain; + settingDialog.ShowInTaskbar = true; // この時点では TweenMain が表示されていないため代わりに表示する + settingDialog.LoadConfig(settings.Common, settings.Local); - SettingManager.LoadAll(); + var ret = settingDialog.ShowDialog(); + if (ret != DialogResult.OK) + return false; - try - { - // 設定ファイルが書き込み可能な状態であるかテストする - SettingManager.SaveAll(); - } - catch (UnauthorizedAccessException) - { - // 書き込みに失敗した場合 (Program Files 以下に配置されている場合など) + settingDialog.SaveConfig(settings.Common, settings.Local); - // 通常は C:\Users\ユーザー名\AppData\Roaming\OpenTween\ となる - var roamingDir = Path.Combine(new[] - { - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - ApplicationSettings.ApplicationName, - }); - Directory.CreateDirectory(roamingDir); - - MyCommon.settingPath = roamingDir; - - /* - * 書き込みが制限されたディレクトリ内で起動された場合の設定ファイルの扱い - * - * (A) StartupPath に存在する設定ファイル - * (B) Roaming に存在する設定ファイル - * - * 1. A も B も存在しない場合 - * => B を新規に作成する - * - * 2. A が存在し、B が存在しない場合 - * => A の内容を B にコピーする (警告を表示) - * - * 3. A が存在せず、B が存在する場合 - * => B を使用する - * - * 4. A も B も存在するが、A の方が更新日時が新しい場合 - * => A の内容を B にコピーする (警告を表示) - * - * 5. A も B も存在するが、B の方が更新日時が新しい場合 - * => B を使用する - */ - var startupDirFile = new FileInfo(Path.Combine(Application.StartupPath, "SettingCommon.xml")); - var roamingDirFile = new FileInfo(Path.Combine(roamingDir, "SettingCommon.xml")); - - if (roamingDirFile.Exists && (!startupDirFile.Exists || startupDirFile.LastWriteTime <= roamingDirFile.LastWriteTime)) - { - // 既に Roaming に設定ファイルが存在し、Roaming 内のファイルの方が新しい場合は - // StartupPath に設定ファイルが存在しても無視する - SettingManager.LoadAll(); - } - else - { - if (startupDirFile.Exists) - { - // StartupPath に設定ファイルが存在し、Roaming 内のファイルよりも新しい場合のみ警告を表示する - var message = string.Format(Properties.Resources.SettingPath_Relocation, Application.StartupPath, MyCommon.settingPath); - MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - // Roaming に設定ファイルを作成 (StartupPath に読み込みに成功した設定ファイルがあれば内容がコピーされる) - SettingManager.SaveAll(); - } - } - } + if (settings.IsIncomplete) + return false; + settings.SaveAll(); return true; } } diff --git a/OpenTween/ApplicationInstanceMutex.cs b/OpenTween/ApplicationInstanceMutex.cs new file mode 100644 index 000000000..3a5bf655b --- /dev/null +++ b/OpenTween/ApplicationInstanceMutex.cs @@ -0,0 +1,100 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2007-2012 kiri_feather (@kiri_feather) +// (c) 2008-2012 Moz (@syo68k) +// (c) 2008-2012 takeshik (@takeshik) +// (c) 2010-2012 anis774 (@anis774) +// (c) 2010-2012 fantasticswallow (@f_swallow) +// (c) 2012 Egtra (@egtra) +// (c) 2012 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace OpenTween +{ + /// + /// アプリケーションの多重起動を抑制するためのクラス + /// + public sealed class ApplicationInstanceMutex : IDisposable + { + public bool IsDisposed { get; private set; } = false; + + private readonly Mutex mutex; + private readonly bool createdNew; + + public ApplicationInstanceMutex(string appName, string settingPath) + { + var name = $"{settingPath.Replace(@"\", "/")}/{appName}"; + this.mutex = new Mutex(initiallyOwned: true, name, out this.createdNew); + } + + /// + /// 他のインスタンスが既に起動している場合は true、それ以外は false + /// + public bool InstanceExists + => !this.createdNew; + + public void TryActivatePreviousInstance() + { + // 実行中の同じアプリケーションのウィンドウ・ハンドルの取得 + var prevProcess = this.GetPreviousProcess(); + if (prevProcess == null) + return; + + var windowHandle = NativeMethods.GetWindowHandle((uint)prevProcess.Id, ApplicationSettings.ApplicationName); + if (windowHandle != IntPtr.Zero) + NativeMethods.SetActiveWindow(windowHandle); + } + + private Process? GetPreviousProcess() + { + try + { + var currentProcess = Process.GetCurrentProcess(); + + return Process.GetProcessesByName(currentProcess.ProcessName) + .Where(p => p.Id != currentProcess.Id) + .FirstOrDefault(p => p.MainModule.FileName.Equals(currentProcess.MainModule.FileName, StringComparison.OrdinalIgnoreCase)); + } + catch + { + return null; + } + } + + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + + if (this.createdNew) + this.mutex.ReleaseMutex(); + + this.mutex.Dispose(); + } + } +} diff --git a/OpenTween/ApplicationPreconditions.cs b/OpenTween/ApplicationPreconditions.cs new file mode 100644 index 000000000..9e36bd838 --- /dev/null +++ b/OpenTween/ApplicationPreconditions.cs @@ -0,0 +1,138 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Security.Principal; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace OpenTween +{ + /// + /// アプリケーションの起動要件を満たしているか確認するクラス + /// + public sealed class ApplicationPreconditions + { + // .NET Framework ランタイムの最小要件 + // 参照: https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed + private const string RuntimeMinimumVersionName = ".NET Framework 4.7.2"; + private const int RuntimeMinimumVersion = 461808; + + /// + /// 全ての起動要件を満たしているか確認する + /// + /// + /// 起動に必須な要件を全て満たしている場合は true、それ以外は false。 + /// + public static bool CheckAll() + { + var conditions = new ApplicationPreconditions(); + + if (!conditions.CheckApiKey()) + { + var message = Properties.Resources.WarnIfApiKeyError_Message; + ShowMessageBox(message, MessageBoxIcon.Error); + return false; + } + + if (!conditions.CheckRuntimeVersion()) + { + var message = string.Format(Properties.Resources.CheckRuntimeVersion_Error, RuntimeMinimumVersionName); + ShowMessageBox(message, MessageBoxIcon.Error); + return false; + } + + if (!conditions.CheckRunAsNormalUser()) + { + var message = string.Format(Properties.Resources.WarnIfRunAsAdministrator_Message, ApplicationSettings.ApplicationName); + ShowMessageBox(message, MessageBoxIcon.Warning); + } + + return true; + } + + private static void ShowMessageBox(string message, MessageBoxIcon icon) + => MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, icon); + + /// + /// API キーが復号できる状態であるか確認する + /// + public bool CheckApiKey() + => this.CanDecryptApiKey(ApplicationSettings.TwitterConsumerKey); + + /// + /// 動作中の .NET Framework のバージョンが適切か確認する + /// + public bool CheckRuntimeVersion() + { + // Mono 上で動作している場合は無視する + if (this.IsRunOnMono()) + return true; + + return this.GetFrameworkReleaseKey() >= RuntimeMinimumVersion; + } + + /// + /// プロセスが管理者権限で実行されていないか確認する + /// + public bool CheckRunAsNormalUser() + { + // UAC が無効なシステムでは警告を表示しない + if (!this.GetEnableLUA()) + return true; + + return !this.IsRunAsAdministrator(); + } + + private bool CanDecryptApiKey(ApiKey apiKey) + => apiKey.TryGetValue(out _); + + private bool IsRunOnMono() + => Type.GetType("Mono.Runtime", false) != null; + + private int GetFrameworkReleaseKey() + { + using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + using var ndpKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"); + + return (int)ndpKey.GetValue("Release"); + } + + private bool GetEnableLUA() + { + using var lmKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + using var systemKey = lmKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\"); + + var enableLUA = (int?)systemKey?.GetValue("EnableLUA"); + return enableLUA == 1; + } + + private bool IsRunAsAdministrator() + { + using var currentIdentity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(currentIdentity); + + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/OpenTween/ApplicationSettings.cs b/OpenTween/ApplicationSettings.cs index 898d25cc9..56943c374 100644 --- a/OpenTween/ApplicationSettings.cs +++ b/OpenTween/ApplicationSettings.cs @@ -1,19 +1,19 @@ // OpenTween - Client of Twitter // Copyright (c) 2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License // for more details. -// +// // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -38,7 +38,7 @@ namespace OpenTween /// internal static class ApplicationSettings { - //===================================================================== + // ===================================================================== // アプリケーション情報 /// @@ -55,10 +55,10 @@ internal static class ApplicationSettings /// public static string AssemblyName => MyCommon.GetAssemblyName(); - //===================================================================== + // ===================================================================== // フィードバック送信先 // 異常終了時などにエラーログ等とともに表示されます。 - + /// /// フィードバック送信先 (メール) /// @@ -77,7 +77,7 @@ internal static class ApplicationSettings /// public static readonly bool AllowSendErrorReportByDM = true; - //===================================================================== + // ===================================================================== // Web サイト /// @@ -93,7 +93,7 @@ internal static class ApplicationSettings /// public const string ShortcutKeyUrl = "https://ja.osdn.net/projects/tween/wiki/%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E3%82%AD%E3%83%BC"; - //===================================================================== + // ===================================================================== // アップデートチェック関連 /// @@ -105,7 +105,7 @@ internal static class ApplicationSettings /// public static readonly string VersionInfoUrl = "https://www.opentween.org/status/version.txt"; - //===================================================================== + // ===================================================================== // 暗号化キー /// @@ -113,7 +113,7 @@ internal static class ApplicationSettings /// public static readonly string EncryptionPassword = ApplicationName; - //===================================================================== + // ===================================================================== // Twitter // https://developer.twitter.com/ から取得できます。 @@ -127,7 +127,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey TwitterConsumerSecret = ApiKey.Create("%e%ocBkKu4aZI5PsbboE7+Ajg==%4IlCFjqusmFPQHbcDutms/1bTOSwgxd3ah1NPCj23WUu66nGjUb5oAEMiNSv41Pb6n5utbFQrgqwJRdl3jVnfQ==%jpx+NlcN1w8UQ8kda1LgyHEmiVwl/PLGCVPotrp6pd8="); - //===================================================================== + // ===================================================================== // Foursquare // https://developer.foursquare.com/ から取得できます。 @@ -141,7 +141,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey FoursquareClientSecret = ApiKey.Create("%e%YZeT/G9cZ0Lub68LFU15bw==%x9LQxogt6ejhWOAV1toXn0zeDeBpV0lMEmJGRCpsIrizJNl3kDcDKWGu1CYSOXgk2hAoqm5IOBq4RAExE0Z2Qw==%yMdCK6WJo7WmvAYBIJ+qZrxAKVIPOR3nftfoXzyzlgY="); - //===================================================================== + // ===================================================================== // bit.ly // https://bitly.com/a/oauth_apps から取得できます。 @@ -155,7 +155,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey BitlyClientSecret = ApiKey.Create("%e%NOCzRjTNRC64Hx0d6e4spA==%XJjn/yAsTsmtgLMdFpjptss66DFh15nvV+Ff2omHxYk0tKyc5Wn5qFxVquAQ4Yg3%yJnMWwcs/FfTYJZ1Wg3r7m0TMogAPj85ViUXImom890="); - //===================================================================== + // ===================================================================== // TINAMI // http://www.tinami.com/api/ から取得できます。 @@ -164,7 +164,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey TINAMIApiKey = ApiKey.Create("%e%5wz/IYAfWvY9y731F3yCIQ==%7y8i0qD9AF4DqFWjY1zn1w==%eVU155W/1sr3ZPDcuRMGTpSQyGXF4egWFto/HzBdGJ4="); - //===================================================================== + // ===================================================================== // Microsoft Translator API (Cognitive Service) // https://www.microsoft.com/ja-jp/translator/getstarted.aspx から取得できます。 @@ -173,7 +173,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey TranslatorSubscriptionKey = ApiKey.Create("%e%N0EPwqCbM0qiNX4h7VsrXQ==%uOf/IdH2RO6fTgrhrvXuJJ7IT+R44aS7ROY3aQFCqqrLHru4fZh2hJAEoAI239BY%p26g6G/ANsAf+1Xq/iLE2zuTwA4ok/zZ61SQkvqqTZ8="); - //===================================================================== + // ===================================================================== // Imgur // https://api.imgur.com/oauth2/addclient から取得できます @@ -187,7 +187,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey ImgurClientSecret = ApiKey.Create("%e%nLVIw/raU3ozrGmkfIk3Ig==%2iKGe1reB5p6VHkvrMkH1w==%lwwvuejEuy0eZZ9nS4BT1Jw7S7pkLktGPKzsfQErttw="); - //===================================================================== + // ===================================================================== // Mobypicture // http://www.mobypicture.com/apps/my から取得できます @@ -196,7 +196,7 @@ internal static class ApplicationSettings /// public static readonly ApiKey MobypictureKey = ApiKey.Create("%e%G9elTyjHy18MCbUvVqHKIw==%TDUSyoO4HS5SX+t50cUlRQ5tIFDib0xjsnCKX+K/+DI=%s2qPqrxXrmi8oeQWoeigqNDbecUAqcYuv2LPRFDLwJk="); - //===================================================================== + // ===================================================================== // Tumblr // https://www.tumblr.com/oauth/apps から取得できます diff --git a/OpenTween/AsyncTimer.cs b/OpenTween/AsyncTimer.cs index 2b9ddb665..dbda858bc 100644 --- a/OpenTween/AsyncTimer.cs +++ b/OpenTween/AsyncTimer.cs @@ -30,6 +30,7 @@ namespace OpenTween public sealed class AsyncTimer : ITimer { public TimeSpan DueTime { get; private set; } = Timeout.InfiniteTimeSpan; + public TimeSpan Period { get; private set; } = Timeout.InfiniteTimeSpan; private readonly Func callback; @@ -43,7 +44,7 @@ public AsyncTimer(Func callback) this.timer = new Timer(this.TimerCallback); } - private async void TimerCallback(object _) + private async void TimerCallback(object state) { try { diff --git a/OpenTween/AtIdSupplement.cs b/OpenTween/AtIdSupplement.cs index 032f35bbc..97f1b5654 100644 --- a/OpenTween/AtIdSupplement.cs +++ b/OpenTween/AtIdSupplement.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 Egtra (@egtra) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -41,8 +41,9 @@ public partial class AtIdSupplement : OTBaseForm { public string StartsWith { get; set; } = ""; - public string inputText = ""; - public bool isBack = false; + public string InputText { get; set; } = ""; + + private bool isBack = false; private readonly string startChar = ""; public void AddItem(string id) @@ -76,28 +77,28 @@ public int ItemCount private void ButtonOK_Click(object sender, EventArgs e) { - inputText = this.TextId.Text; - isBack = false; + this.InputText = this.TextId.Text; + this.isBack = false; } private void ButtonCancel_Click(object sender, EventArgs e) { - inputText = ""; - isBack = false; + this.InputText = ""; + this.isBack = false; } private void TextId_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Back && MyCommon.IsNullOrEmpty(this.TextId.Text)) { - inputText = ""; - isBack = true; + this.InputText = ""; + this.isBack = true; this.Close(); } else if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Tab) { - inputText = this.TextId.Text + " "; - isBack = false; + this.InputText = this.TextId.Text + " "; + this.isBack = false; this.Close(); } else if (e.Control && e.KeyCode == Keys.Delete) @@ -116,44 +117,44 @@ private void TextId_KeyDown(object sender, KeyEventArgs e) private void AtIdSupplement_Load(object sender, EventArgs e) { - if (startChar == "#") + if (this.startChar == "#") { - this.ClientSize = new Size(this.TextId.Width, this.TextId.Height); //プロパティで切り替えできるように + this.ClientSize = new Size(this.TextId.Width, this.TextId.Height); // プロパティで切り替えできるように this.TextId.ImeMode = ImeMode.Inherit; } } private void AtIdSupplement_Shown(object sender, EventArgs e) { - TextId.Text = startChar; + this.TextId.Text = this.startChar; if (!MyCommon.IsNullOrEmpty(this.StartsWith)) { - TextId.Text += this.StartsWith.Substring(0, this.StartsWith.Length); + this.TextId.Text += this.StartsWith.Substring(0, this.StartsWith.Length); } - TextId.SelectionStart = TextId.Text.Length; - TextId.Focus(); + this.TextId.SelectionStart = this.TextId.Text.Length; + this.TextId.Focus(); } public AtIdSupplement() => this.InitializeComponent(); - public AtIdSupplement(List ItemList, string startCharacter) + public AtIdSupplement(List itemList, string startCharacter) { - InitializeComponent(); + this.InitializeComponent(); - for (var i = 0; i < ItemList.Count; ++i) + for (var i = 0; i < itemList.Count; ++i) { - this.TextId.AutoCompleteCustomSource.Add(ItemList[i]); + this.TextId.AutoCompleteCustomSource.Add(itemList[i]); } - startChar = startCharacter; + this.startChar = startCharacter; } private void TextId_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { if (e.KeyCode == Keys.Tab) { - inputText = this.TextId.Text + " "; - isBack = false; + this.InputText = this.TextId.Text + " "; + this.isBack = false; this.Close(); } } @@ -161,7 +162,7 @@ private void TextId_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) private void AtIdSupplement_FormClosed(object sender, FormClosedEventArgs e) { this.StartsWith = ""; - if (isBack) + if (this.isBack) { this.DialogResult = DialogResult.Cancel; } diff --git a/OpenTween/AuthDialog.cs b/OpenTween/AuthDialog.cs index 7cf1c7171..292656f94 100644 --- a/OpenTween/AuthDialog.cs +++ b/OpenTween/AuthDialog.cs @@ -1,19 +1,19 @@ // OpenTween - Client of Twitter // Copyright (c) 2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -40,7 +40,7 @@ public partial class AuthDialog : OTBaseForm { public AuthDialog() { - InitializeComponent(); + this.InitializeComponent(); // PinTextBox のフォントを OTBaseForm.GlobalFont に変更 this.PinTextBox.Font = this.ReplaceToGlobalFont(this.PinTextBox.Font); @@ -48,14 +48,14 @@ public AuthDialog() public string AuthUrl { - get => AuthLinkLabel.Text; - set => AuthLinkLabel.Text = value; + get => this.AuthLinkLabel.Text; + set => this.AuthLinkLabel.Text = value; } public string Pin { - get => PinTextBox.Text.Trim(); - set => PinTextBox.Text = value; + get => this.PinTextBox.Text.Trim(); + set => this.PinTextBox.Text = value; } public string? BrowserPath { get; set; } @@ -66,7 +66,7 @@ private async void AuthLinkLabel_LinkClicked(object sender, LinkLabelLinkClicked if (e.Button == MouseButtons.Right) return; - AuthLinkLabel.LinkVisited = true; + this.AuthLinkLabel.LinkVisited = true; await MyCommon.OpenInBrowserAsync(this, this.BrowserPath, this.AuthUrl); } @@ -76,7 +76,9 @@ private void MenuItemCopyURL_Click(object sender, EventArgs e) { Clipboard.SetText(this.AuthUrl); } - catch (ExternalException) { } + catch (ExternalException) + { + } } /// diff --git a/OpenTween/Bing.cs b/OpenTween/Bing.cs index 4aac36d61..9ae3da1cf 100644 --- a/OpenTween/Bing.cs +++ b/OpenTween/Bing.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -39,7 +39,7 @@ namespace OpenTween { public class Bing { - private static readonly List LanguageTable = new List + private static readonly List LanguageTable = new() { "af", "sq", diff --git a/OpenTween/CommandLineArgs.cs b/OpenTween/CommandLineArgs.cs new file mode 100644 index 000000000..1d0708c0b --- /dev/null +++ b/OpenTween/CommandLineArgs.cs @@ -0,0 +1,82 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; + +namespace OpenTween +{ + public class CommandLineArgs : IReadOnlyDictionary + { + private readonly Dictionary parsedArgs; + + public CommandLineArgs(string[] args) + => this.parsedArgs = this.ParseArguments(args); + + public string this[string key] + => this.parsedArgs[key]; + + public IEnumerable Keys + => this.parsedArgs.Keys; + + public IEnumerable Values + => this.parsedArgs.Values; + + public int Count + => this.parsedArgs.Count; + + public bool ContainsKey(string key) + => this.parsedArgs.ContainsKey(key); + +#pragma warning disable CS8767 + public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + => this.parsedArgs.TryGetValue(key, out value); +#pragma warning restore CS8767 + + public IEnumerator> GetEnumerator() + => this.parsedArgs.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => this.GetEnumerator(); + + /// + /// “/key:value”形式の起動オプションを解釈し Dictionary に変換する + /// + /// + /// 不正な形式のオプションは除外されます。 + /// また、重複したキーのオプションが入力された場合は末尾に書かれたオプションが採用されます。 + /// + private Dictionary ParseArguments(string[] arguments) + { + var optionPattern = new Regex(@"^/(.+?)(?::(.*))?$"); + + return arguments.Select(x => optionPattern.Match(x)) + .Where(x => x.Success) + .GroupBy(x => x.Groups[1].Value) + .ToDictionary(x => x.Key, x => x.Last().Groups[2].Value); + } + } +} diff --git a/OpenTween/Connection/LazyJson.cs b/OpenTween/Connection/LazyJson.cs index 6bcca4523..2f0bc80e0 100644 --- a/OpenTween/Connection/LazyJson.cs +++ b/OpenTween/Connection/LazyJson.cs @@ -83,7 +83,7 @@ public void Dispose() public static class LazyJson { public static LazyJson Create(T instance) - => new LazyJson(instance); + => new(instance); } public static class LazyJsonTaskExtension diff --git a/OpenTween/Connection/Networking.cs b/OpenTween/Connection/Networking.cs index a22d04bc3..4dca4fa1c 100644 --- a/OpenTween/Connection/Networking.cs +++ b/OpenTween/Connection/Networking.cs @@ -37,6 +37,7 @@ namespace OpenTween.Connection public static class Networking { public static TimeSpan DefaultTimeout { get; set; } + public static TimeSpan UploadImageTimeout { get; set; } /// @@ -113,8 +114,12 @@ public static void Initialize() ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; } - public static void SetWebProxy(ProxyType proxyType, string proxyAddress, int proxyPort, - string proxyUser, string proxyPassword) + public static void SetWebProxy( + ProxyType proxyType, + string proxyAddress, + int proxyPort, + string proxyUser, + string proxyPassword) { IWebProxy? proxy; switch (proxyType) @@ -222,8 +227,10 @@ public ForceIPv4Handler(HttpMessageHandler innerHandler) : base(innerHandler) { foreach (var address in Dns.GetHostAddresses("pbs.twimg.com")) + { if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) this.ipv4Address = address; + } } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/OpenTween/Connection/OAuthEchoHandler.cs b/OpenTween/Connection/OAuthEchoHandler.cs index 6bf4c7d3b..9e5e07e3d 100644 --- a/OpenTween/Connection/OAuthEchoHandler.cs +++ b/OpenTween/Connection/OAuthEchoHandler.cs @@ -35,6 +35,7 @@ namespace OpenTween.Connection public class OAuthEchoHandler : DelegatingHandler { public Uri AuthServiceProvider { get; } + public string VerifyCredentialsAuthorization { get; } public OAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, string authorizationValue) @@ -52,11 +53,24 @@ protected override Task SendAsync(HttpRequestMessage reques return base.SendAsync(request, cancellationToken); } - public static OAuthEchoHandler CreateHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, - ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret, Uri? realm = null) + public static OAuthEchoHandler CreateHandler( + HttpMessageHandler innerHandler, + Uri authServiceProvider, + ApiKey consumerKey, + ApiKey consumerSecret, + string accessToken, + string accessSecret, + Uri? realm = null) { - var credential = OAuthUtility.CreateAuthorization("GET", authServiceProvider, null, - consumerKey, consumerSecret, accessToken, accessSecret, realm?.AbsoluteUri); + var credential = OAuthUtility.CreateAuthorization( + "GET", + authServiceProvider, + null, + consumerKey, + consumerSecret, + accessToken, + accessSecret, + realm?.AbsoluteUri); return new OAuthEchoHandler(innerHandler, authServiceProvider, credential); } diff --git a/OpenTween/Connection/OAuthHandler.cs b/OpenTween/Connection/OAuthHandler.cs index b302a3585..9e0489ff3 100644 --- a/OpenTween/Connection/OAuthHandler.cs +++ b/OpenTween/Connection/OAuthHandler.cs @@ -39,8 +39,11 @@ namespace OpenTween.Connection public class OAuthHandler : DelegatingHandler { public ApiKey ConsumerKey { get; } + public ApiKey ConsumerSecret { get; } + public string AccessToken { get; } + public string AccessSecret { get; } public OAuthHandler(HttpMessageHandler innerHandler, ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret) @@ -57,8 +60,14 @@ protected override async Task SendAsync(HttpRequestMessage var query = await GetParameters(request.RequestUri, request.Content) .ConfigureAwait(false); - var credential = OAuthUtility.CreateAuthorization(request.Method.ToString().ToUpperInvariant(), request.RequestUri, query, - this.ConsumerKey, this.ConsumerSecret, this.AccessToken, this.AccessSecret); + var credential = OAuthUtility.CreateAuthorization( + request.Method.ToString().ToUpperInvariant(), + request.RequestUri, + query, + this.ConsumerKey, + this.ConsumerSecret, + this.AccessToken, + this.AccessSecret); request.Headers.TryAddWithoutValidation("Authorization", credential); if (request.Content is FormUrlEncodedContent postContent) diff --git a/OpenTween/Connection/OAuthUtility.cs b/OpenTween/Connection/OAuthUtility.cs index 22aa27810..dba7ada7f 100644 --- a/OpenTween/Connection/OAuthUtility.cs +++ b/OpenTween/Connection/OAuthUtility.cs @@ -42,7 +42,7 @@ public static class OAuthUtility /// /// OAuth署名のoauth_nonce算出用乱数クラス /// - private static readonly Random NonceRandom = new Random(); + private static readonly Random NonceRandom = new(); /// /// HTTPリクエストに追加するAuthorizationヘッダの値を生成します @@ -55,16 +55,24 @@ public static class OAuthUtility /// アクセストークン、もしくはリクエストトークン。未取得なら空文字列 /// アクセストークンシークレット。認証処理では空文字列 /// realm (必要な場合のみ) - public static string CreateAuthorization(string httpMethod, Uri requestUri, IEnumerable>? query, - ApiKey consumerKey, ApiKey consumerSecret, string token, string tokenSecret, + public static string CreateAuthorization( + string httpMethod, + Uri requestUri, + IEnumerable>? query, + ApiKey consumerKey, + ApiKey consumerSecret, + string token, + string tokenSecret, string? realm = null) { // OAuth共通情報取得 var parameter = GetOAuthParameter(consumerKey, token); // OAuth共通情報にquery情報を追加 if (query != null) + { foreach (var (key, value) in query) parameter.Add(key, value); + } // 署名の作成・追加 parameter.Add("oauth_signature", CreateSignature(consumerSecret, tokenSecret, httpMethod, requestUri, parameter)); // HTTPリクエストのヘッダに追加 diff --git a/OpenTween/Connection/TwitterApiConnection.cs b/OpenTween/Connection/TwitterApiConnection.cs index f42d93df0..faa54ae38 100644 --- a/OpenTween/Connection/TwitterApiConnection.cs +++ b/OpenTween/Connection/TwitterApiConnection.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -40,7 +41,7 @@ namespace OpenTween.Connection { public class TwitterApiConnection : IApiConnection, IDisposable { - public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/"); + public static Uri RestApiBase { get; set; } = new("https://api.twitter.com/1.1/"); // SettingCommon.xml の TwitterUrl との互換性のために用意 public static string RestApiHost @@ -52,11 +53,12 @@ public static string RestApiHost public bool IsDisposed { get; private set; } = false; public string AccessToken { get; } + public string AccessSecret { get; } - internal HttpClient http = null!; - internal HttpClient httpUpload = null!; - internal HttpClient httpStreaming = null!; + internal HttpClient Http; + internal HttpClient HttpUpload; + internal HttpClient HttpStreaming; private readonly ApiKey consumerKey; private readonly ApiKey consumerSecret; @@ -72,15 +74,16 @@ public TwitterApiConnection(ApiKey consumerKey, ApiKey consumerSecret, string ac Networking.WebProxyChanged += this.Networking_WebProxyChanged; } + [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))] private void InitializeHttpClients() { - this.http = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret); + this.Http = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret); - this.httpUpload = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret); - this.httpUpload.Timeout = Networking.UploadImageTimeout; + this.HttpUpload = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret); + this.HttpUpload.Timeout = Networking.UploadImageTimeout; - this.httpStreaming = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret, disableGzip: true); - this.httpStreaming.Timeout = Timeout.InfiniteTimeSpan; + this.HttpStreaming = InitializeHttpClient(this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret, disableGzip: true); + this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan; } public async Task GetAsync(Uri uri, IDictionary? param, string? endpointName) @@ -98,7 +101,7 @@ public async Task GetAsync(Uri uri, IDictionary? param, st try { - using var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); if (endpointName != null) @@ -161,7 +164,7 @@ public async Task GetStreamAsync(Uri uri, IDictionary? p try { - return await this.http.GetStreamAsync(requestUri) + return await this.Http.GetStreamAsync(requestUri) .ConfigureAwait(false); } catch (HttpRequestException ex) @@ -184,7 +187,7 @@ public async Task GetStreamingStreamAsync(Uri uri, IDictionary> PostLazyAsync(Uri uri, IDictionary> PostLazyAsync(Uri uri, IDictionary? param, IDictio try { - using var response = await this.httpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); await this.CheckStatusCode(response) @@ -339,7 +342,7 @@ public async Task> PostJsonAsync(Uri uri, string json) HttpResponseMessage? response = null; try { - response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); await this.CheckStatusCode(response) @@ -371,7 +374,7 @@ public async Task DeleteAsync(Uri uri) try { - using var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false); await this.CheckStatusCode(response) @@ -437,8 +440,14 @@ public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? rea { var uri = new Uri(RestApiBase, authServiceProvider); - return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri, - this.consumerKey, this.consumerSecret, this.AccessToken, this.AccessSecret, realm); + return OAuthEchoHandler.CreateHandler( + Networking.CreateHttpClientHandler(), + uri, + this.consumerKey, + this.consumerSecret, + this.AccessToken, + this.AccessSecret, + realm); } public void Dispose() @@ -457,9 +466,9 @@ protected virtual void Dispose(bool disposing) if (disposing) { Networking.WebProxyChanged -= this.Networking_WebProxyChanged; - this.http.Dispose(); - this.httpUpload.Dispose(); - this.httpStreaming.Dispose(); + this.Http.Dispose(); + this.HttpUpload.Dispose(); + this.HttpStreaming.Dispose(); } } @@ -512,8 +521,12 @@ public static async Task> GetAccessTokenAsync(ApiKey return response; } - private static async Task> GetOAuthTokenAsync(Uri uri, IDictionary param, - ApiKey consumerKey, ApiKey consumerSecret, (string Token, string TokenSecret)? oauthToken) + private static async Task> GetOAuthTokenAsync( + Uri uri, + IDictionary param, + ApiKey consumerKey, + ApiKey consumerSecret, + (string Token, string TokenSecret)? oauthToken) { HttpClient authorizeClient; if (oauthToken != null) diff --git a/OpenTween/ControlTransaction.cs b/OpenTween/ControlTransaction.cs index 20311592b..a8e03b0a4 100644 --- a/OpenTween/ControlTransaction.cs +++ b/OpenTween/ControlTransaction.cs @@ -23,10 +23,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Windows.Forms; -using System.ComponentModel; namespace OpenTween { @@ -53,7 +53,11 @@ public static IDisposable Update(Control control) // WM_SETREDRAW メッセージを直接コントロールに送信する。 return new Transaction(control, x => NativeMethods.SetRedrawState(x, false), - x => { NativeMethods.SetRedrawState(x, true); x.Invalidate(true); }); + x => + { + NativeMethods.SetRedrawState(x, true); + x.Invalidate(true); + }); } public static IDisposable Layout(Control control) diff --git a/OpenTween/CultureService.cs b/OpenTween/CultureService.cs new file mode 100644 index 000000000..50eee2b12 --- /dev/null +++ b/OpenTween/CultureService.cs @@ -0,0 +1,79 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2007-2012 kiri_feather (@kiri_feather) +// (c) 2008-2012 Moz (@syo68k) +// (c) 2008-2012 takeshik (@takeshik) +// (c) 2010-2012 anis774 (@anis774) +// (c) 2010-2012 fantasticswallow (@f_swallow) +// (c) 2012 Egtra (@egtra) +// (c) 2012 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System.Globalization; +using System.Linq; +using System.Threading; + +namespace OpenTween +{ + public class CultureService + { + public static readonly CultureInfo[] SupportedUICulture = new[] + { + new CultureInfo("en"), // 先頭のカルチャはフォールバック先として使用される + new CultureInfo("ja"), + }; + + private readonly SettingCommon settingCommon; + + public CultureService(SettingCommon settingCommon) + => this.settingCommon = settingCommon; + + public void Initialize() + { + var settingCultureStr = this.settingCommon.Language; + if (settingCultureStr == "OS") + return; + + try + { + var selectedCulture = new CultureInfo(settingCultureStr); + + var preferredCulture = GetPreferredCulture(selectedCulture); + CultureInfo.DefaultThreadCurrentUICulture = preferredCulture; + Thread.CurrentThread.CurrentUICulture = preferredCulture; + } + catch (CultureNotFoundException) + { + } + } + + /// + /// サポートしているカルチャの中から、指定されたカルチャに対して適切なカルチャを選択して返します + /// + public static CultureInfo GetPreferredCulture(CultureInfo culture) + { + if (SupportedUICulture.Any(x => x.Contains(culture))) + return culture; + + return SupportedUICulture[0]; + } + } +} diff --git a/OpenTween/DateTimeUtc.cs b/OpenTween/DateTimeUtc.cs index 0a324c52b..b89abd391 100644 --- a/OpenTween/DateTimeUtc.cs +++ b/OpenTween/DateTimeUtc.cs @@ -87,10 +87,10 @@ public long UtcTicks => this.datetime.Ticks; public long ToUnixTime() - => (long)((this - UnixEpoch).TotalSeconds); + => (long)(this - UnixEpoch).TotalSeconds; public DateTimeOffset ToDateTimeOffset() - => new DateTimeOffset(this.datetime); + => new(this.datetime); public DateTimeOffset ToLocalTime() => this.ToDateTimeOffset().ToLocalTime(); @@ -123,10 +123,10 @@ public string ToLocalTimeString(string format) => this.ToLocalTime().ToString(format); public static DateTimeUtc operator +(DateTimeUtc a, TimeSpan b) - => new DateTimeUtc(a.datetime + b); + => new(a.datetime + b); public static DateTimeUtc operator -(DateTimeUtc a, TimeSpan b) - => new DateTimeUtc(a.datetime - b); + => new(a.datetime - b); public static TimeSpan operator -(DateTimeUtc a, DateTimeUtc b) => a.datetime - b.datetime; @@ -153,7 +153,7 @@ public static DateTimeUtc FromUnixTime(long unixTime) => UnixEpoch + TimeSpan.FromTicks(unixTime * TimeSpan.TicksPerSecond); public static DateTimeUtc Parse(string input, IFormatProvider formatProvider) - => new DateTimeUtc(DateTimeOffset.Parse(input, formatProvider, DateTimeStyles.AssumeUniversal)); + => new(DateTimeOffset.Parse(input, formatProvider, DateTimeStyles.AssumeUniversal)); public static bool TryParse(string input, IFormatProvider formatProvider, out DateTimeUtc result) { diff --git a/OpenTween/DebounceTimer.cs b/OpenTween/DebounceTimer.cs index 531853ac5..cc728cbb0 100644 --- a/OpenTween/DebounceTimer.cs +++ b/OpenTween/DebounceTimer.cs @@ -37,14 +37,16 @@ public class DebounceTimer : IDisposable { private readonly ITimer debouncingTimer; private readonly Func timerCallback; - private readonly object lockObject = new object(); + private readonly object lockObject = new(); private DateTimeUtc lastCall; private bool calledSinceLastInvoke; private bool refreshTimerEnabled; public TimeSpan Interval { get; } + public bool InvokeLeading { get; } + public bool InvokeTrailing { get; } public DebounceTimer(Func timerCallback, TimeSpan interval, bool leading, bool trailing) @@ -143,6 +145,6 @@ public void Dispose() => this.debouncingTimer.Dispose(); public static DebounceTimer Create(Func callback, TimeSpan wait, bool leading = false, bool trailing = true) - => new DebounceTimer(callback, wait, leading, trailing); + => new(callback, wait, leading, trailing); } } diff --git a/OpenTween/DetailsListView.cs b/OpenTween/DetailsListView.cs index 23ad4cb99..a1df119ea 100644 --- a/OpenTween/DetailsListView.cs +++ b/OpenTween/DetailsListView.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -28,31 +28,33 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; -using System.Drawing; -using System.ComponentModel; -using System.Runtime.InteropServices; -using System.Diagnostics; namespace OpenTween.OpenTweenCustomControl { public sealed class DetailsListView : ListView { - private Rectangle changeBounds; + private (int Start, int End)? redrawRange = null; + [DefaultValue(null)] public ContextMenuStrip? ColumnHeaderContextMenuStrip { get; set; } public event EventHandler? VScrolled; + public event EventHandler? HScrolled; public DetailsListView() { - View = View.Details; - FullRowSelect = true; - HideSelection = false; - DoubleBuffered = true; + this.View = View.Details; + this.FullRowSelect = true; + this.HideSelection = false; + this.DoubleBuffered = true; } /// @@ -62,6 +64,8 @@ public DetailsListView() /// Items[idx].Selected の設定では mark が設定されるが、SelectedIndices.Add(idx) では設定されないため、 /// 主に後者と合わせて使用する /// + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int SelectionMark { get => NativeMethods.ListView_GetSelectionMark(this.Handle); @@ -112,67 +116,62 @@ public void SelectAllItems() this.OnSelectedIndexChanged(EventArgs.Empty); } - public void ChangeItemBackColor(ListViewItem item, Color backColor) + public void RefreshItem(int index) { - if (item.BackColor == backColor) - return; - - item.BackColor = backColor; - this.RefreshItemBounds(item); + this.ValidateAll(); + this.RefreshItemsRange(index, index); } - public void ChangeItemForeColor(ListViewItem item, Color foreColor) + public void RefreshItems(IEnumerable indices) { - if (item.ForeColor == foreColor) - return; + var chunks = MyCommon.ToRangeChunk(indices); + this.ValidateAll(); - item.ForeColor = foreColor; - this.RefreshItemBounds(item); + foreach (var (start, end) in chunks) + this.RefreshItemsRange(start, end); } - public void ChangeItemFontAndColor(ListViewItem item, Color foreColor, Font fnt) + private void RefreshItemsRange(int start, int end) { - if (item.ForeColor == foreColor && item.Font.Equals(fnt)) - return; - - item.ForeColor = foreColor; - item.Font = fnt; - this.RefreshItemBounds(item); + try + { + this.redrawRange = (start, end); + this.RedrawItems(start, end, invalidateOnly: false); + } + finally + { + this.redrawRange = null; + } } - private void RefreshItemBounds(ListViewItem item) + /// 領域を全て有効化する(再描画が必要な領域から除外する) + private void ValidateAll() + => NativeMethods.ValidateRect(this.Handle, IntPtr.Zero); + + protected override void OnDrawItem(DrawListViewItemEventArgs e) { - try + if (this.redrawRange is (int start, int end)) { - var itemBounds = item.Bounds; - var drawBounds = Rectangle.Intersect(this.ClientRectangle, itemBounds); - if (drawBounds == Rectangle.Empty) + var index = e.ItemIndex; + if (index < start || index > end) return; - - this.changeBounds = drawBounds; - this.Update(); - this.changeBounds = Rectangle.Empty; - } - catch (ArgumentException) - { - //タイミングによりBoundsプロパティが取れない? - this.changeBounds = Rectangle.Empty; } + + base.OnDrawItem(e); } [StructLayout(LayoutKind.Sequential)] private struct NMHDR { - public IntPtr hwndFrom; - public IntPtr idFrom; - public int code; + public IntPtr HwndFrom; + public IntPtr IdFrom; + public int Code; } [DebuggerStepThrough] protected override void WndProc(ref Message m) { const int WM_ERASEBKGND = 0x14; - const int WM_PAINT = 0xF; const int WM_MOUSEWHEEL = 0x20A; const int WM_MOUSEHWHEEL = 0x20E; const int WM_HSCROLL = 0x114; @@ -183,7 +182,7 @@ protected override void WndProc(ref Message m) const int WM_NOTIFY = 0x004E; const int WM_CONTEXTMENU = 0x7B; const int LVM_SETITEMCOUNT = 0x102F; - const int LVN_ODSTATECHANGED = ((0 - 100) - 15); + const int LVN_ODSTATECHANGED = 0 - 100 - 15; const long LVSICF_NOSCROLL = 0x2; const long LVSICF_NOINVALIDATEALL = 0x1; @@ -193,22 +192,14 @@ protected override void WndProc(ref Message m) switch (m.Msg) { case WM_ERASEBKGND: - if (this.changeBounds != Rectangle.Empty) + if (this.redrawRange != null) m.Msg = 0; break; - case WM_PAINT: - if (this.changeBounds != Rectangle.Empty) - { - NativeMethods.ValidateRect(this.Handle, IntPtr.Zero); - this.Invalidate(this.changeBounds); - this.changeBounds = Rectangle.Empty; - } - break; case WM_HSCROLL: - HScrolled?.Invoke(this, EventArgs.Empty); + this.HScrolled?.Invoke(this, EventArgs.Empty); break; case WM_VSCROLL: - VScrolled?.Invoke(this, EventArgs.Empty); + this.VScrolled?.Invoke(this, EventArgs.Empty); break; case WM_MOUSEWHEEL: case WM_MOUSEHWHEEL: @@ -219,7 +210,7 @@ protected override void WndProc(ref Message m) case WM_CONTEXTMENU: if (m.WParam != this.Handle) { - //カラムヘッダメニューを表示 + // カラムヘッダメニューを表示 this.ColumnHeaderContextMenuStrip?.Show(new Point(m.LParam.ToInt32())); return; } @@ -231,7 +222,7 @@ protected override void WndProc(ref Message m) var nmhdr = Marshal.PtrToStructure(m.LParam); // Ctrl+クリックで選択状態を変更した場合にイベントが発生しない問題への対処 - if (nmhdr.code == LVN_ODSTATECHANGED) + if (nmhdr.Code == LVN_ODSTATECHANGED) this.OnSelectedIndexChanged(EventArgs.Empty); break; } @@ -242,20 +233,25 @@ protected override void WndProc(ref Message m) } catch (ArgumentOutOfRangeException) { - //Substringでlengthが0以下。アイコンサイズが影響? + // Substringでlengthが0以下。アイコンサイズが影響? } catch (AccessViolationException) { - //WndProcのさらに先で発生する。 + // WndProcのさらに先で発生する。 } if (this.IsDisposed) return; if (vPos != -1) + { if (vPos != NativeMethods.GetScrollPosition(this, NativeMethods.ScrollBarDirection.SB_VERT)) - VScrolled?.Invoke(this, EventArgs.Empty); + this.VScrolled?.Invoke(this, EventArgs.Empty); + } + if (hPos != -1) + { if (hPos != NativeMethods.GetScrollPosition(this, NativeMethods.ScrollBarDirection.SB_HORZ)) - HScrolled?.Invoke(this, EventArgs.Empty); + this.HScrolled?.Invoke(this, EventArgs.Empty); + } } } } diff --git a/OpenTween/DisposableLazy.cs b/OpenTween/DisposableLazy.cs new file mode 100644 index 000000000..9687c12bf --- /dev/null +++ b/OpenTween/DisposableLazy.cs @@ -0,0 +1,55 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; + +namespace OpenTween +{ + public sealed class DisposableLazy : IDisposable + where T : IDisposable + { + public bool IsDisposed { get; private set; } = false; + + public T Value + => !this.IsDisposed ? this.lazy.Value : throw new ObjectDisposedException(nameof(this.lazy)); + + public bool IsValueCreated + => this.lazy.IsValueCreated; + + private readonly Lazy lazy; + + public DisposableLazy(Func factory) + => this.lazy = new(factory); + + public void Dispose() + { + if (this.IsDisposed) + return; + + if (this.IsValueCreated) + this.Value.Dispose(); + + this.IsDisposed = true; + } + } +} diff --git a/OpenTween/ErrorReportHandler.cs b/OpenTween/ErrorReportHandler.cs new file mode 100644 index 000000000..af9d572f6 --- /dev/null +++ b/OpenTween/ErrorReportHandler.cs @@ -0,0 +1,124 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using OpenTween.Connection; + +namespace OpenTween +{ + public sealed class ErrorReportHandler : IDisposable + { + public bool IsDisposed { get; private set; } = false; + + public ErrorReportHandler() + => this.RegisterHandlers(); + + private void RegisterHandlers() + { + TaskScheduler.UnobservedTaskException += this.TaskScheduler_UnobservedTaskException; + AsyncTimer.UnhandledException += this.AsyncTimer_UnhandledException; + Application.ThreadException += this.Application_ThreadException; + AppDomain.CurrentDomain.UnhandledException += this.AppDomain_UnhandledException; + } + + private void UnregisterHandlers() + { + TaskScheduler.UnobservedTaskException -= this.TaskScheduler_UnobservedTaskException; + AsyncTimer.UnhandledException -= this.AsyncTimer_UnhandledException; + Application.ThreadException -= this.Application_ThreadException; + AppDomain.CurrentDomain.UnhandledException -= this.AppDomain_UnhandledException; + } + + private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + e.SetObserved(); + this.OnUnhandledException(e.Exception.Flatten()); + } + + private void AsyncTimer_UnhandledException(object sender, ThreadExceptionEventArgs e) + => this.OnUnhandledException(e.Exception); + + private void Application_ThreadException(object sender, ThreadExceptionEventArgs e) + => this.OnUnhandledException(e.Exception); + + private void AppDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + => this.OnUnhandledException((Exception)e.ExceptionObject); + + private void OnUnhandledException(Exception ex) + { +#if !DEBUG + if (ErrorReportHandler.IsExceptionIgnorable(ex)) + return; +#endif + + if (MyCommon.ExceptionOut(ex)) + Application.Exit(); + } + + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + this.UnregisterHandlers(); + } + + /// + /// 無視しても問題のない既知の例外であれば true を返す + /// + public static bool IsExceptionIgnorable(Exception ex) + { + if (ex is AggregateException aggregated) + return aggregated.InnerExceptions.All(x => IsExceptionIgnorable(x)); + + if (ex is WebException webEx) + { + // SSL/TLS のネゴシエーションに失敗した場合に発生する。なぜかキャッチできない例外 + // https://osdn.net/ticket/browse.php?group_id=6526&tid=37432 + if (webEx.Status == WebExceptionStatus.SecureChannelFailure) + return true; + } + + if (ex is TaskCanceledException cancelEx) + { + // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない + // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433 + var stackTrace = new StackTrace(cancelEx); + var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod(); + var matchClass = lastFrameMethod.ReflectedType == typeof(TwitterApiConnection); + var matchMethod = lastFrameMethod.Name == nameof(TwitterApiConnection.GetStreamAsync); + if (matchClass && matchMethod) + return true; + } + + return false; + } + } +} diff --git a/OpenTween/Extensions.cs b/OpenTween/Extensions.cs index 069a9e399..294a9e03e 100644 --- a/OpenTween/Extensions.cs +++ b/OpenTween/Extensions.cs @@ -46,14 +46,51 @@ public static string GetSelectedText(this WebBrowser webBrowser) return selectedText; } + public static Task InvokeAsync(this Control control, Action x) + { + return control.InvokeAsync(new Func(() => + { + x(); + return 0; + })); + } + + public static Task InvokeAsync(this Control control, Func x) + => control.InvokeAsync(x).Unwrap(); + + public static Task InvokeAsync(this Control control, Func> x) + => control.InvokeAsync>(x).Unwrap(); + + /// + /// メソッドのTask版みたいなやつ + /// + public static Task InvokeAsync(this Control control, Func x) + { + var tcs = new TaskCompletionSource(); + control.BeginInvoke(() => + { + try + { + var ret = x(); + tcs.SetResult(ret); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; + } + public static ReadLockTransaction BeginReadTransaction(this ReaderWriterLockSlim lockObj) - => new ReadLockTransaction(lockObj); + => new(lockObj); public static WriteLockTransaction BeginWriteTransaction(this ReaderWriterLockSlim lockObj) - => new WriteLockTransaction(lockObj); + => new(lockObj); public static UpgradeableReadLockTransaction BeginUpgradeableReadTransaction(this ReaderWriterLockSlim lockObj) - => new UpgradeableReadLockTransaction(lockObj); + => new(lockObj); /// /// 一方のカルチャがもう一方のカルチャを内包するかを判断します @@ -159,13 +196,29 @@ public static int GetCodepointCount(this string s, int start, int end) } public static Task ForEachAsync(this IObservable observable, Action subscriber) - => ForEachAsync(observable, value => { subscriber(value); return Task.CompletedTask; }); + { + return ForEachAsync(observable, value => + { + subscriber(value); + return Task.CompletedTask; + }); + } public static Task ForEachAsync(this IObservable observable, Func subscriber) => ForEachAsync(observable, subscriber, CancellationToken.None); public static Task ForEachAsync(this IObservable observable, Action subscriber, CancellationToken cancellationToken) - => ForEachAsync(observable, value => { subscriber(value); return Task.CompletedTask; }, cancellationToken); + { + return ForEachAsync( + observable, + value => + { + subscriber(value); + return Task.CompletedTask; + }, + cancellationToken + ); + } public static async Task ForEachAsync(this IObservable observable, Func subscriber, CancellationToken cancellationToken) { @@ -179,7 +232,7 @@ public static async Task ForEachAsync(this IObservable observable, Func : IObserver { private readonly Func subscriber; - private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + private readonly TaskCompletionSource tcs = new(); public Task Task => this.tcs.Task; diff --git a/OpenTween/FilterDialog.Designer.cs b/OpenTween/FilterDialog.Designer.cs index 4bf426c0f..43b54d598 100644 --- a/OpenTween/FilterDialog.Designer.cs +++ b/OpenTween/FilterDialog.Designer.cs @@ -382,7 +382,7 @@ private void InitializeComponent() resources.ApplyResources(this.buttonRuleToggleEnabled, "buttonRuleToggleEnabled"); this.buttonRuleToggleEnabled.Name = "buttonRuleToggleEnabled"; this.buttonRuleToggleEnabled.UseVisualStyleBackColor = true; - this.buttonRuleToggleEnabled.Click += new System.EventHandler(this.buttonRuleToggleEnabled_Click); + this.buttonRuleToggleEnabled.Click += new System.EventHandler(this.ButtonRuleToggleEnabled_Click); // // ListFilters // diff --git a/OpenTween/FilterDialog.cs b/OpenTween/FilterDialog.cs index a32303d65..14ca329b9 100644 --- a/OpenTween/FilterDialog.cs +++ b/OpenTween/FilterDialog.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011-2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -28,31 +28,31 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Data; using System.Drawing; +using System.IO; using System.Linq; -using System.Text; -using System.Windows.Forms; using System.Linq.Expressions; +using System.Text; using System.Text.RegularExpressions; -using System.IO; -using System.Collections.Specialized; +using System.Windows.Forms; using OpenTween.Models; namespace OpenTween { public partial class FilterDialog : OTBaseForm { - private EDITMODE _mode; - private bool _directAdd; - private MultiSelectionState _multiSelState = MultiSelectionState.None; - private readonly TabInformations _sts; + private EDITMODE mode; + private bool directAdd; + private MultiSelectionState multiSelState = MultiSelectionState.None; + private readonly TabInformations sts; - private List tabs = new List(); + private List tabs = new(); private int selectedTabIndex = -1; - private readonly List idlist = new List(); + private readonly List idlist = new(); private enum EDITMODE { @@ -78,10 +78,10 @@ private enum MultiSelectionState private EnableButtonMode RuleEnableButtonMode { - get => this._ruleEnableButtonMode; + get => this.ruleEnableButtonMode; set { - this._ruleEnableButtonMode = value; + this.ruleEnableButtonMode = value; this.buttonRuleToggleEnabled.Text = value == FilterDialog.EnableButtonMode.Enable ? Properties.Resources.EnableButtonCaption @@ -89,7 +89,8 @@ private EnableButtonMode RuleEnableButtonMode this.buttonRuleToggleEnabled.Enabled = value != EnableButtonMode.NotSelected; } } - private EnableButtonMode _ruleEnableButtonMode = FilterDialog.EnableButtonMode.NotSelected; + + private EnableButtonMode ruleEnableButtonMode = FilterDialog.EnableButtonMode.NotSelected; public TabModel? SelectedTab => this.selectedTabIndex != -1 ? this.tabs[this.selectedTabIndex] : null; @@ -98,13 +99,13 @@ public FilterDialog() { this.InitializeComponent(); - this._sts = TabInformations.GetInstance(); + this.sts = TabInformations.GetInstance(); this.RefreshListTabs(); } private void RefreshListTabs() { - this.tabs = this._sts.Tabs.Append(this._sts.MuteTab).ToList(); + this.tabs = this.sts.Tabs.Append(this.sts.MuteTab).ToList(); using (ControlTransaction.Update(this.ListTabs)) { @@ -117,42 +118,42 @@ private void RefreshListTabs() private void SetFilters(TabModel tab) { - if (ListTabs.Items.Count == 0) return; + if (this.ListTabs.Items.Count == 0) return; - ListFilters.Items.Clear(); + this.ListFilters.Items.Clear(); if (tab is FilterTabModel filterTab) - ListFilters.Items.AddRange(filterTab.GetFilters()); + this.ListFilters.Items.AddRange(filterTab.GetFilters()); - if (ListFilters.Items.Count > 0) - ListFilters.SelectedIndex = 0; + if (this.ListFilters.Items.Count > 0) + this.ListFilters.SelectedIndex = 0; else - ShowDetail(); + this.ShowDetail(); if (tab.IsDefaultTabType) { - CheckProtected.Checked = true; - CheckProtected.Enabled = false; + this.CheckProtected.Checked = true; + this.CheckProtected.Enabled = false; } else { - CheckProtected.Checked = tab.Protected; - CheckProtected.Enabled = true; + this.CheckProtected.Checked = tab.Protected; + this.CheckProtected.Enabled = true; } - CheckManageRead.CheckedChanged -= this.CheckManageRead_CheckedChanged; - CheckManageRead.Checked = tab.UnreadManage; - CheckManageRead.CheckedChanged += this.CheckManageRead_CheckedChanged; + this.CheckManageRead.CheckedChanged -= this.CheckManageRead_CheckedChanged; + this.CheckManageRead.Checked = tab.UnreadManage; + this.CheckManageRead.CheckedChanged += this.CheckManageRead_CheckedChanged; - CheckNotifyNew.CheckedChanged -= this.CheckNotifyNew_CheckedChanged; - CheckNotifyNew.Checked = tab.Notify; - CheckNotifyNew.CheckedChanged += this.CheckNotifyNew_CheckedChanged; + this.CheckNotifyNew.CheckedChanged -= this.CheckNotifyNew_CheckedChanged; + this.CheckNotifyNew.Checked = tab.Notify; + this.CheckNotifyNew.CheckedChanged += this.CheckNotifyNew_CheckedChanged; - var idx = ComboSound.Items.IndexOf(tab.SoundFile); + var idx = this.ComboSound.Items.IndexOf(tab.SoundFile); if (idx == -1) idx = 0; - ComboSound.SelectedIndex = idx; + this.ComboSound.SelectedIndex = idx; - if (_directAdd) return; + if (this.directAdd) return; if (tab.TabType == MyCommon.TabUsageType.Mute) { @@ -175,45 +176,45 @@ private void SetFilters(TabModel tab) this.labelMuteTab.Visible = false; } - ListTabs.Enabled = true; - GroupTab.Enabled = true; - ListFilters.Enabled = true; - EditFilterGroup.Enabled = false; + this.ListTabs.Enabled = true; + this.GroupTab.Enabled = true; + this.ListFilters.Enabled = true; + this.EditFilterGroup.Enabled = false; if (tab.IsDistributableTabType) { - ButtonNew.Enabled = true; - if (ListFilters.SelectedIndex > -1) + this.ButtonNew.Enabled = true; + if (this.ListFilters.SelectedIndex > -1) { - ButtonEdit.Enabled = true; - ButtonDelete.Enabled = true; - ButtonRuleUp.Enabled = true; - ButtonRuleDown.Enabled = true; - ButtonRuleCopy.Enabled = true; - ButtonRuleMove.Enabled = true; - buttonRuleToggleEnabled.Enabled = true; + this.ButtonEdit.Enabled = true; + this.ButtonDelete.Enabled = true; + this.ButtonRuleUp.Enabled = true; + this.ButtonRuleDown.Enabled = true; + this.ButtonRuleCopy.Enabled = true; + this.ButtonRuleMove.Enabled = true; + this.buttonRuleToggleEnabled.Enabled = true; } else { - ButtonEdit.Enabled = false; - ButtonDelete.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; } } else { - ButtonNew.Enabled = false; - ButtonEdit.Enabled = false; - ButtonDelete.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; + this.ButtonNew.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; } this.LabelTabType.Text = tab.TabType switch @@ -234,13 +235,13 @@ private void SetFilters(TabModel tab) if (tab.IsDefaultTabType || tab.Protected) { - ButtonDeleteTab.Enabled = false; + this.ButtonDeleteTab.Enabled = false; } else { - ButtonDeleteTab.Enabled = true; + this.ButtonDeleteTab.Enabled = true; } - ButtonClose.Enabled = true; + this.ButtonClose.Enabled = true; } public void SetCurrent(string tabName) @@ -254,154 +255,154 @@ public void SetCurrent(string tabName) public void AddNewFilter(string id, string msg) { - //元フォームから直接呼ばれる - ButtonNew.Enabled = false; - ButtonEdit.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; - ButtonDelete.Enabled = false; - ButtonClose.Enabled = false; - EditFilterGroup.Enabled = true; - ListTabs.Enabled = false; - GroupTab.Enabled = false; - ListFilters.Enabled = false; - - RadioAND.Checked = true; - RadioPLUS.Checked = false; - UID.Text = id; - UID.SelectAll(); - MSG1.Text = msg; - MSG1.SelectAll(); - MSG2.Text = id + msg; - MSG2.SelectAll(); - TextSource.Text = ""; - UID.Enabled = true; - MSG1.Enabled = true; - MSG2.Enabled = false; - CheckRegex.Checked = false; - CheckURL.Checked = false; - CheckCaseSensitive.Checked = false; - CheckRetweet.Checked = false; - CheckLambda.Checked = false; - - RadioExAnd.Checked = true; - RadioExPLUS.Checked = false; - ExUID.Text = ""; - ExUID.SelectAll(); - ExMSG1.Text = ""; - ExMSG1.SelectAll(); - ExMSG2.Text = ""; - ExMSG2.SelectAll(); - TextExSource.Text = ""; - ExUID.Enabled = true; - ExMSG1.Enabled = true; - ExMSG2.Enabled = false; - CheckExRegex.Checked = false; - CheckExURL.Checked = false; - CheckExCaseSensitive.Checked = false; - CheckExRetweet.Checked = false; - CheckExLambDa.Checked = false; - - OptCopy.Checked = true; - CheckMark.Checked = true; - UID.Focus(); - _mode = EDITMODE.AddNew; - _directAdd = true; + // 元フォームから直接呼ばれる + this.ButtonNew.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonClose.Enabled = false; + this.EditFilterGroup.Enabled = true; + this.ListTabs.Enabled = false; + this.GroupTab.Enabled = false; + this.ListFilters.Enabled = false; + + this.RadioAND.Checked = true; + this.RadioPLUS.Checked = false; + this.UID.Text = id; + this.UID.SelectAll(); + this.MSG1.Text = msg; + this.MSG1.SelectAll(); + this.MSG2.Text = id + msg; + this.MSG2.SelectAll(); + this.TextSource.Text = ""; + this.UID.Enabled = true; + this.MSG1.Enabled = true; + this.MSG2.Enabled = false; + this.CheckRegex.Checked = false; + this.CheckURL.Checked = false; + this.CheckCaseSensitive.Checked = false; + this.CheckRetweet.Checked = false; + this.CheckLambda.Checked = false; + + this.RadioExAnd.Checked = true; + this.RadioExPLUS.Checked = false; + this.ExUID.Text = ""; + this.ExUID.SelectAll(); + this.ExMSG1.Text = ""; + this.ExMSG1.SelectAll(); + this.ExMSG2.Text = ""; + this.ExMSG2.SelectAll(); + this.TextExSource.Text = ""; + this.ExUID.Enabled = true; + this.ExMSG1.Enabled = true; + this.ExMSG2.Enabled = false; + this.CheckExRegex.Checked = false; + this.CheckExURL.Checked = false; + this.CheckExCaseSensitive.Checked = false; + this.CheckExRetweet.Checked = false; + this.CheckExLambDa.Checked = false; + + this.OptCopy.Checked = true; + this.CheckMark.Checked = true; + this.UID.Focus(); + this.mode = EDITMODE.AddNew; + this.directAdd = true; } private void ButtonNew_Click(object sender, EventArgs e) { - ButtonNew.Enabled = false; - ButtonEdit.Enabled = false; - ButtonClose.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; - ButtonDelete.Enabled = false; - ButtonClose.Enabled = false; - EditFilterGroup.Enabled = true; - ListTabs.Enabled = false; - GroupTab.Enabled = false; - ListFilters.Enabled = false; - - RadioAND.Checked = true; - RadioPLUS.Checked = false; - UID.Text = ""; - MSG1.Text = ""; - MSG2.Text = ""; - TextSource.Text = ""; - UID.Enabled = true; - MSG1.Enabled = true; - MSG2.Enabled = false; - CheckRegex.Checked = false; - CheckURL.Checked = false; - CheckCaseSensitive.Checked = false; - CheckRetweet.Checked = false; - CheckLambda.Checked = false; - - RadioExAnd.Checked = true; - RadioExPLUS.Checked = false; - ExUID.Text = ""; - ExMSG1.Text = ""; - ExMSG2.Text = ""; - TextExSource.Text = ""; - ExUID.Enabled = true; - ExMSG1.Enabled = true; - ExMSG2.Enabled = false; - CheckExRegex.Checked = false; - CheckExURL.Checked = false; - CheckExCaseSensitive.Checked = false; - CheckExRetweet.Checked = false; - CheckExLambDa.Checked = false; - - OptCopy.Checked = true; - CheckMark.Checked = true; - UID.Focus(); - _mode = EDITMODE.AddNew; + this.ButtonNew.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonClose.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonClose.Enabled = false; + this.EditFilterGroup.Enabled = true; + this.ListTabs.Enabled = false; + this.GroupTab.Enabled = false; + this.ListFilters.Enabled = false; + + this.RadioAND.Checked = true; + this.RadioPLUS.Checked = false; + this.UID.Text = ""; + this.MSG1.Text = ""; + this.MSG2.Text = ""; + this.TextSource.Text = ""; + this.UID.Enabled = true; + this.MSG1.Enabled = true; + this.MSG2.Enabled = false; + this.CheckRegex.Checked = false; + this.CheckURL.Checked = false; + this.CheckCaseSensitive.Checked = false; + this.CheckRetweet.Checked = false; + this.CheckLambda.Checked = false; + + this.RadioExAnd.Checked = true; + this.RadioExPLUS.Checked = false; + this.ExUID.Text = ""; + this.ExMSG1.Text = ""; + this.ExMSG2.Text = ""; + this.TextExSource.Text = ""; + this.ExUID.Enabled = true; + this.ExMSG1.Enabled = true; + this.ExMSG2.Enabled = false; + this.CheckExRegex.Checked = false; + this.CheckExURL.Checked = false; + this.CheckExCaseSensitive.Checked = false; + this.CheckExRetweet.Checked = false; + this.CheckExLambDa.Checked = false; + + this.OptCopy.Checked = true; + this.CheckMark.Checked = true; + this.UID.Focus(); + this.mode = EDITMODE.AddNew; } private void ButtonEdit_Click(object sender, EventArgs e) { - if (ListFilters.SelectedIndex == -1) return; - - ShowDetail(); - - var idx = ListFilters.SelectedIndex; - ListFilters.SelectedIndex = -1; - ListFilters.SelectedIndex = idx; - ListFilters.Enabled = false; - - ButtonNew.Enabled = false; - ButtonEdit.Enabled = false; - ButtonDelete.Enabled = false; - ButtonClose.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; - EditFilterGroup.Enabled = true; - ListTabs.Enabled = false; - GroupTab.Enabled = false; - - _mode = EDITMODE.Edit; + if (this.ListFilters.SelectedIndex == -1) return; + + this.ShowDetail(); + + var idx = this.ListFilters.SelectedIndex; + this.ListFilters.SelectedIndex = -1; + this.ListFilters.SelectedIndex = idx; + this.ListFilters.Enabled = false; + + this.ButtonNew.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonClose.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; + this.EditFilterGroup.Enabled = true; + this.ListTabs.Enabled = false; + this.GroupTab.Enabled = false; + + this.mode = EDITMODE.Edit; } private void ButtonDelete_Click(object sender, EventArgs e) { - var selectedCount = ListFilters.SelectedIndices.Count; + var selectedCount = this.ListFilters.SelectedIndices.Count; if (selectedCount == 0) return; string tmp; if (selectedCount == 1) { - tmp = string.Format(Properties.Resources.ButtonDelete_ClickText1, Environment.NewLine, ListFilters.SelectedItem); + tmp = string.Format(Properties.Resources.ButtonDelete_ClickText1, Environment.NewLine, this.ListFilters.SelectedItem); } else { @@ -411,53 +412,53 @@ private void ButtonDelete_Click(object sender, EventArgs e) var rslt = MessageBox.Show(tmp, Properties.Resources.ButtonDelete_ClickText2, MessageBoxButtons.OKCancel, MessageBoxIcon.Question); if (rslt == DialogResult.Cancel) return; - var indices = ListFilters.SelectedIndices.Cast().Reverse().ToArray(); // 後ろの要素から削除 + var indices = this.ListFilters.SelectedIndices.Cast().Reverse().ToArray(); // 後ろの要素から削除 var tab = (FilterTabModel)this.SelectedTab!; - using (ControlTransaction.Update(ListFilters)) + using (ControlTransaction.Update(this.ListFilters)) { foreach (var idx in indices) { - tab.RemoveFilter((PostFilterRule)ListFilters.Items[idx]); - ListFilters.Items.RemoveAt(idx); + tab.RemoveFilter((PostFilterRule)this.ListFilters.Items[idx]); + this.ListFilters.Items.RemoveAt(idx); } } } private void ButtonCancel_Click(object sender, EventArgs e) { - ListTabs.Enabled = true; - GroupTab.Enabled = true; - ListFilters.Enabled = true; - ListFilters.Focus(); - if (ListFilters.SelectedIndex != -1) - { - ShowDetail(); - } - EditFilterGroup.Enabled = false; - ButtonNew.Enabled = true; - if (ListFilters.SelectedIndex > -1) - { - ButtonEdit.Enabled = true; - ButtonDelete.Enabled = true; - ButtonRuleUp.Enabled = true; - ButtonRuleDown.Enabled = true; - ButtonRuleCopy.Enabled = true; - ButtonRuleMove.Enabled = true; - buttonRuleToggleEnabled.Enabled = true; + this.ListTabs.Enabled = true; + this.GroupTab.Enabled = true; + this.ListFilters.Enabled = true; + this.ListFilters.Focus(); + if (this.ListFilters.SelectedIndex != -1) + { + this.ShowDetail(); + } + this.EditFilterGroup.Enabled = false; + this.ButtonNew.Enabled = true; + if (this.ListFilters.SelectedIndex > -1) + { + this.ButtonEdit.Enabled = true; + this.ButtonDelete.Enabled = true; + this.ButtonRuleUp.Enabled = true; + this.ButtonRuleDown.Enabled = true; + this.ButtonRuleCopy.Enabled = true; + this.ButtonRuleMove.Enabled = true; + this.buttonRuleToggleEnabled.Enabled = true; } else { - ButtonEdit.Enabled = false; - ButtonDelete.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; + this.ButtonEdit.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; } - ButtonClose.Enabled = true; - if (_directAdd) + this.ButtonClose.Enabled = true; + if (this.directAdd) { this.Close(); } @@ -465,150 +466,150 @@ private void ButtonCancel_Click(object sender, EventArgs e) private void ShowDetail() { - if (_directAdd) return; + if (this.directAdd) return; - if (ListFilters.SelectedIndex > -1) + if (this.ListFilters.SelectedIndex > -1) { - var fc = (PostFilterRule)ListFilters.SelectedItem; + var fc = (PostFilterRule)this.ListFilters.SelectedItem; if (fc.UseNameField) { - RadioAND.Checked = true; - RadioPLUS.Checked = false; - UID.Enabled = true; - MSG1.Enabled = true; - MSG2.Enabled = false; - UID.Text = fc.FilterName; - UID.SelectAll(); - MSG1.Text = string.Join(" ", fc.FilterBody); - MSG1.SelectAll(); - MSG2.Text = ""; + this.RadioAND.Checked = true; + this.RadioPLUS.Checked = false; + this.UID.Enabled = true; + this.MSG1.Enabled = true; + this.MSG2.Enabled = false; + this.UID.Text = fc.FilterName; + this.UID.SelectAll(); + this.MSG1.Text = string.Join(" ", fc.FilterBody); + this.MSG1.SelectAll(); + this.MSG2.Text = ""; } else { - RadioPLUS.Checked = true; - RadioAND.Checked = false; - UID.Enabled = false; - MSG1.Enabled = false; - MSG2.Enabled = true; - UID.Text = ""; - MSG1.Text = ""; - MSG2.Text = string.Join(" ", fc.FilterBody); - MSG2.SelectAll(); + this.RadioPLUS.Checked = true; + this.RadioAND.Checked = false; + this.UID.Enabled = false; + this.MSG1.Enabled = false; + this.MSG2.Enabled = true; + this.UID.Text = ""; + this.MSG1.Text = ""; + this.MSG2.Text = string.Join(" ", fc.FilterBody); + this.MSG2.SelectAll(); } - TextSource.Text = fc.FilterSource; - CheckRegex.Checked = fc.UseRegex; - CheckURL.Checked = fc.FilterByUrl; - CheckCaseSensitive.Checked = fc.CaseSensitive; - CheckRetweet.Checked = fc.FilterRt; - CheckLambda.Checked = fc.UseLambda; + this.TextSource.Text = fc.FilterSource; + this.CheckRegex.Checked = fc.UseRegex; + this.CheckURL.Checked = fc.FilterByUrl; + this.CheckCaseSensitive.Checked = fc.CaseSensitive; + this.CheckRetweet.Checked = fc.FilterRt; + this.CheckLambda.Checked = fc.UseLambda; if (fc.ExUseNameField) { - RadioExAnd.Checked = true; - RadioExPLUS.Checked = false; - ExUID.Enabled = true; - ExMSG1.Enabled = true; - ExMSG2.Enabled = false; - ExUID.Text = fc.ExFilterName; - ExUID.SelectAll(); - ExMSG1.Text = string.Join(" ", fc.ExFilterBody); - ExMSG1.SelectAll(); - ExMSG2.Text = ""; + this.RadioExAnd.Checked = true; + this.RadioExPLUS.Checked = false; + this.ExUID.Enabled = true; + this.ExMSG1.Enabled = true; + this.ExMSG2.Enabled = false; + this.ExUID.Text = fc.ExFilterName; + this.ExUID.SelectAll(); + this.ExMSG1.Text = string.Join(" ", fc.ExFilterBody); + this.ExMSG1.SelectAll(); + this.ExMSG2.Text = ""; } else { - RadioExPLUS.Checked = true; - RadioExAnd.Checked = false; - ExUID.Enabled = false; - ExMSG1.Enabled = false; - ExMSG2.Enabled = true; - ExUID.Text = ""; - ExMSG1.Text = ""; - ExMSG2.Text = string.Join(" ", fc.ExFilterBody); - ExMSG2.SelectAll(); + this.RadioExPLUS.Checked = true; + this.RadioExAnd.Checked = false; + this.ExUID.Enabled = false; + this.ExMSG1.Enabled = false; + this.ExMSG2.Enabled = true; + this.ExUID.Text = ""; + this.ExMSG1.Text = ""; + this.ExMSG2.Text = string.Join(" ", fc.ExFilterBody); + this.ExMSG2.SelectAll(); } - TextExSource.Text = fc.ExFilterSource; - CheckExRegex.Checked = fc.ExUseRegex; - CheckExURL.Checked = fc.ExFilterByUrl; - CheckExCaseSensitive.Checked = fc.ExCaseSensitive; - CheckExRetweet.Checked = fc.ExFilterRt; - CheckExLambDa.Checked = fc.ExUseLambda; + this.TextExSource.Text = fc.ExFilterSource; + this.CheckExRegex.Checked = fc.ExUseRegex; + this.CheckExURL.Checked = fc.ExFilterByUrl; + this.CheckExCaseSensitive.Checked = fc.ExCaseSensitive; + this.CheckExRetweet.Checked = fc.ExFilterRt; + this.CheckExLambDa.Checked = fc.ExUseLambda; if (fc.MoveMatches) { - OptMove.Checked = true; + this.OptMove.Checked = true; } else { - OptCopy.Checked = true; + this.OptCopy.Checked = true; } - CheckMark.Checked = fc.MarkMatches; + this.CheckMark.Checked = fc.MarkMatches; - ButtonEdit.Enabled = true; - ButtonDelete.Enabled = true; - ButtonRuleUp.Enabled = true; - ButtonRuleDown.Enabled = true; - ButtonRuleCopy.Enabled = true; - ButtonRuleMove.Enabled = true; - buttonRuleToggleEnabled.Enabled = true; + this.ButtonEdit.Enabled = true; + this.ButtonDelete.Enabled = true; + this.ButtonRuleUp.Enabled = true; + this.ButtonRuleDown.Enabled = true; + this.ButtonRuleCopy.Enabled = true; + this.ButtonRuleMove.Enabled = true; + this.buttonRuleToggleEnabled.Enabled = true; } else { - RadioAND.Checked = true; - RadioPLUS.Checked = false; - UID.Enabled = true; - MSG1.Enabled = true; - MSG2.Enabled = false; - UID.Text = ""; - MSG1.Text = ""; - MSG2.Text = ""; - TextSource.Text = ""; - CheckRegex.Checked = false; - CheckURL.Checked = false; - CheckCaseSensitive.Checked = false; - CheckRetweet.Checked = false; - CheckLambda.Checked = false; - - RadioExAnd.Checked = true; - RadioExPLUS.Checked = false; - ExUID.Enabled = true; - ExMSG1.Enabled = true; - ExMSG2.Enabled = false; - ExUID.Text = ""; - ExMSG1.Text = ""; - ExMSG2.Text = ""; - TextExSource.Text = ""; - CheckExRegex.Checked = false; - CheckExURL.Checked = false; - CheckExCaseSensitive.Checked = false; - CheckExRetweet.Checked = false; - CheckExLambDa.Checked = false; - - OptCopy.Checked = true; - CheckMark.Checked = true; - - ButtonEdit.Enabled = false; - ButtonDelete.Enabled = false; - ButtonRuleUp.Enabled = false; - ButtonRuleDown.Enabled = false; - ButtonRuleCopy.Enabled = false; - ButtonRuleMove.Enabled = false; - buttonRuleToggleEnabled.Enabled = false; + this.RadioAND.Checked = true; + this.RadioPLUS.Checked = false; + this.UID.Enabled = true; + this.MSG1.Enabled = true; + this.MSG2.Enabled = false; + this.UID.Text = ""; + this.MSG1.Text = ""; + this.MSG2.Text = ""; + this.TextSource.Text = ""; + this.CheckRegex.Checked = false; + this.CheckURL.Checked = false; + this.CheckCaseSensitive.Checked = false; + this.CheckRetweet.Checked = false; + this.CheckLambda.Checked = false; + + this.RadioExAnd.Checked = true; + this.RadioExPLUS.Checked = false; + this.ExUID.Enabled = true; + this.ExMSG1.Enabled = true; + this.ExMSG2.Enabled = false; + this.ExUID.Text = ""; + this.ExMSG1.Text = ""; + this.ExMSG2.Text = ""; + this.TextExSource.Text = ""; + this.CheckExRegex.Checked = false; + this.CheckExURL.Checked = false; + this.CheckExCaseSensitive.Checked = false; + this.CheckExRetweet.Checked = false; + this.CheckExLambDa.Checked = false; + + this.OptCopy.Checked = true; + this.CheckMark.Checked = true; + + this.ButtonEdit.Enabled = false; + this.ButtonDelete.Enabled = false; + this.ButtonRuleUp.Enabled = false; + this.ButtonRuleDown.Enabled = false; + this.ButtonRuleCopy.Enabled = false; + this.ButtonRuleMove.Enabled = false; + this.buttonRuleToggleEnabled.Enabled = false; } } private void RadioAND_CheckedChanged(object sender, EventArgs e) { - var flg = RadioAND.Checked; - UID.Enabled = flg; - MSG1.Enabled = flg; - MSG2.Enabled = !flg; + var flg = this.RadioAND.Checked; + this.UID.Enabled = flg; + this.MSG1.Enabled = flg; + this.MSG2.Enabled = !flg; } private void ButtonOK_Click(object sender, EventArgs e) { - //入力チェック - if (!CheckMatchRule(out var isBlankMatch) || !CheckExcludeRule(out var isBlankExclude)) + // 入力チェック + if (!this.CheckMatchRule(out var isBlankMatch) || !this.CheckExcludeRule(out var isBlankExclude)) { return; } @@ -619,18 +620,18 @@ private void ButtonOK_Click(object sender, EventArgs e) } var tab = (FilterTabModel)this.SelectedTab!; - var i = ListFilters.SelectedIndex; + var i = this.ListFilters.SelectedIndex; PostFilterRule ft; - if (_mode == EDITMODE.AddNew) + if (this.mode == EDITMODE.AddNew) ft = new PostFilterRule(); else ft = (PostFilterRule)this.ListFilters.SelectedItem; if (tab.TabType != MyCommon.TabUsageType.Mute) { - ft.MoveMatches = OptMove.Checked; - ft.MarkMatches = CheckMark.Checked; + ft.MoveMatches = this.OptMove.Checked; + ft.MarkMatches = this.CheckMark.Checked; } else { @@ -639,9 +640,9 @@ private void ButtonOK_Click(object sender, EventArgs e) } var bdy = ""; - if (RadioAND.Checked) + if (this.RadioAND.Checked) { - ft.FilterName = UID.Text; + ft.FilterName = this.UID.Text; var owner = (TweenMain)this.Owner; var cnt = owner.AtIdSupl.ItemCount; owner.AtIdSupl.AddItem("@" + ft.FilterName); @@ -650,17 +651,17 @@ private void ButtonOK_Click(object sender, EventArgs e) owner.MarkSettingAtIdModified(); } ft.UseNameField = true; - bdy = MSG1.Text; + bdy = this.MSG1.Text; } else { ft.FilterName = ""; ft.UseNameField = false; - bdy = MSG2.Text; + bdy = this.MSG2.Text; } - ft.FilterSource = TextSource.Text; + ft.FilterSource = this.TextSource.Text; - if (CheckRegex.Checked || CheckLambda.Checked) + if (this.CheckRegex.Checked || this.CheckLambda.Checked) { ft.FilterBody = new[] { bdy }; } @@ -671,28 +672,28 @@ private void ButtonOK_Click(object sender, EventArgs e) .ToArray(); } - ft.UseRegex = CheckRegex.Checked; - ft.FilterByUrl = CheckURL.Checked; - ft.CaseSensitive = CheckCaseSensitive.Checked; - ft.FilterRt = CheckRetweet.Checked; - ft.UseLambda = CheckLambda.Checked; + ft.UseRegex = this.CheckRegex.Checked; + ft.FilterByUrl = this.CheckURL.Checked; + ft.CaseSensitive = this.CheckCaseSensitive.Checked; + ft.FilterRt = this.CheckRetweet.Checked; + ft.UseLambda = this.CheckLambda.Checked; bdy = ""; - if (RadioExAnd.Checked) + if (this.RadioExAnd.Checked) { - ft.ExFilterName = ExUID.Text; + ft.ExFilterName = this.ExUID.Text; ft.ExUseNameField = true; - bdy = ExMSG1.Text; + bdy = this.ExMSG1.Text; } else { ft.ExFilterName = ""; ft.ExUseNameField = false; - bdy = ExMSG2.Text; + bdy = this.ExMSG2.Text; } - ft.ExFilterSource = TextExSource.Text; + ft.ExFilterSource = this.TextExSource.Text; - if (CheckExRegex.Checked || CheckExLambDa.Checked) + if (this.CheckExRegex.Checked || this.CheckExLambDa.Checked) { ft.ExFilterBody = new[] { bdy }; } @@ -703,31 +704,31 @@ private void ButtonOK_Click(object sender, EventArgs e) .ToArray(); } - ft.ExUseRegex = CheckExRegex.Checked; - ft.ExFilterByUrl = CheckExURL.Checked; - ft.ExCaseSensitive = CheckExCaseSensitive.Checked; - ft.ExFilterRt = CheckExRetweet.Checked; - ft.ExUseLambda = CheckExLambDa.Checked; + ft.ExUseRegex = this.CheckExRegex.Checked; + ft.ExFilterByUrl = this.CheckExURL.Checked; + ft.ExCaseSensitive = this.CheckExCaseSensitive.Checked; + ft.ExFilterRt = this.CheckExRetweet.Checked; + ft.ExUseLambda = this.CheckExLambDa.Checked; - if (_mode == EDITMODE.AddNew) + if (this.mode == EDITMODE.AddNew) { if (!tab.AddFilter(ft)) MessageBox.Show(Properties.Resources.ButtonOK_ClickText4, Properties.Resources.ButtonOK_ClickText2, MessageBoxButtons.OK, MessageBoxIcon.Error); } - SetFilters(tab); - ListFilters.SelectedIndex = -1; - if (_mode == EDITMODE.AddNew) + this.SetFilters(tab); + this.ListFilters.SelectedIndex = -1; + if (this.mode == EDITMODE.AddNew) { - ListFilters.SelectedIndex = ListFilters.Items.Count - 1; + this.ListFilters.SelectedIndex = this.ListFilters.Items.Count - 1; } else { - ListFilters.SelectedIndex = i; + this.ListFilters.SelectedIndex = i; } - _mode = EDITMODE.None; + this.mode = EDITMODE.None; - if (_directAdd) + if (this.directAdd) { this.Close(); } @@ -753,31 +754,31 @@ private bool IsValidRegexp(string text) private bool CheckMatchRule(out bool isBlank) { isBlank = false; - if (RadioAND.Checked) + if (this.RadioAND.Checked) { - if (MyCommon.IsNullOrEmpty(UID.Text) && MyCommon.IsNullOrEmpty(MSG1.Text) && MyCommon.IsNullOrEmpty(TextSource.Text) && CheckRetweet.Checked == false) + if (MyCommon.IsNullOrEmpty(this.UID.Text) && MyCommon.IsNullOrEmpty(this.MSG1.Text) && MyCommon.IsNullOrEmpty(this.TextSource.Text) && this.CheckRetweet.Checked == false) { isBlank = true; return true; } - if (CheckLambda.Checked) + if (this.CheckLambda.Checked) { - if (!IsValidLambdaExp(UID.Text)) + if (!this.IsValidLambdaExp(this.UID.Text)) { return false; } - if (!IsValidLambdaExp(MSG1.Text)) + if (!this.IsValidLambdaExp(this.MSG1.Text)) { return false; } } - else if (CheckRegex.Checked) + else if (this.CheckRegex.Checked) { - if (!IsValidRegexp(UID.Text)) + if (!this.IsValidRegexp(this.UID.Text)) { return false; } - if (!IsValidRegexp(MSG1.Text)) + if (!this.IsValidRegexp(this.MSG1.Text)) { return false; } @@ -785,22 +786,22 @@ private bool CheckMatchRule(out bool isBlank) } else { - if (MyCommon.IsNullOrEmpty(MSG2.Text) && MyCommon.IsNullOrEmpty(TextSource.Text) && CheckRetweet.Checked == false) + if (MyCommon.IsNullOrEmpty(this.MSG2.Text) && MyCommon.IsNullOrEmpty(this.TextSource.Text) && this.CheckRetweet.Checked == false) { isBlank = true; return true; } - if (CheckLambda.Checked && !IsValidLambdaExp(MSG2.Text)) + if (this.CheckLambda.Checked && !this.IsValidLambdaExp(this.MSG2.Text)) { return false; } - else if (CheckRegex.Checked && !IsValidRegexp(MSG2.Text)) + else if (this.CheckRegex.Checked && !this.IsValidRegexp(this.MSG2.Text)) { return false; } } - if (CheckRegex.Checked && !IsValidRegexp(TextSource.Text)) + if (this.CheckRegex.Checked && !this.IsValidRegexp(this.TextSource.Text)) { return false; } @@ -810,31 +811,31 @@ private bool CheckMatchRule(out bool isBlank) private bool CheckExcludeRule(out bool isBlank) { isBlank = false; - if (RadioExAnd.Checked) + if (this.RadioExAnd.Checked) { - if (MyCommon.IsNullOrEmpty(ExUID.Text) && MyCommon.IsNullOrEmpty(ExMSG1.Text) && MyCommon.IsNullOrEmpty(TextExSource.Text) && CheckExRetweet.Checked == false) + if (MyCommon.IsNullOrEmpty(this.ExUID.Text) && MyCommon.IsNullOrEmpty(this.ExMSG1.Text) && MyCommon.IsNullOrEmpty(this.TextExSource.Text) && this.CheckExRetweet.Checked == false) { isBlank = true; return true; } - if (CheckExLambDa.Checked) + if (this.CheckExLambDa.Checked) { - if (!IsValidLambdaExp(ExUID.Text)) + if (!this.IsValidLambdaExp(this.ExUID.Text)) { return false; } - if (!IsValidLambdaExp(ExMSG1.Text)) + if (!this.IsValidLambdaExp(this.ExMSG1.Text)) { return false; } } - else if (CheckExRegex.Checked) + else if (this.CheckExRegex.Checked) { - if (!IsValidRegexp(ExUID.Text)) + if (!this.IsValidRegexp(this.ExUID.Text)) { return false; } - if (!IsValidRegexp(ExMSG1.Text)) + if (!this.IsValidRegexp(this.ExMSG1.Text)) { return false; } @@ -842,22 +843,22 @@ private bool CheckExcludeRule(out bool isBlank) } else { - if (MyCommon.IsNullOrEmpty(ExMSG2.Text) && MyCommon.IsNullOrEmpty(TextExSource.Text) && CheckExRetweet.Checked == false) + if (MyCommon.IsNullOrEmpty(this.ExMSG2.Text) && MyCommon.IsNullOrEmpty(this.TextExSource.Text) && this.CheckExRetweet.Checked == false) { isBlank = true; return true; } - if (CheckExLambDa.Checked && !IsValidLambdaExp(ExMSG2.Text)) + if (this.CheckExLambDa.Checked && !this.IsValidLambdaExp(this.ExMSG2.Text)) { return false; } - else if (CheckExRegex.Checked && !IsValidRegexp(ExMSG2.Text)) + else if (this.CheckExRegex.Checked && !this.IsValidRegexp(this.ExMSG2.Text)) { return false; } } - if (CheckExRegex.Checked && !IsValidRegexp(TextExSource.Text)) + if (this.CheckExRegex.Checked && !this.IsValidRegexp(this.TextExSource.Text)) { return false; } @@ -867,10 +868,10 @@ private bool CheckExcludeRule(out bool isBlank) private void ListFilters_SelectedIndexChanged(object sender, EventArgs e) { - if (_multiSelState != MultiSelectionState.None) //複数選択処理中は無視する + if (this.multiSelState != MultiSelectionState.None) // 複数選択処理中は無視する return; - ShowDetail(); + this.ShowDetail(); var selectedCount = this.ListFilters.SelectedIndices.Count; if (selectedCount == 0) @@ -892,36 +893,36 @@ private void ButtonClose_Click(object sender, EventArgs e) => this.Close(); private void FilterDialog_FormClosed(object sender, FormClosedEventArgs e) - => this._directAdd = false; + => this.directAdd = false; private void FilterDialog_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Escape) { - if (EditFilterGroup.Enabled) - ButtonCancel_Click(this.ButtonCancel, EventArgs.Empty); + if (this.EditFilterGroup.Enabled) + this.ButtonCancel_Click(this.ButtonCancel, EventArgs.Empty); else - ButtonClose_Click(this.ButtonClose, EventArgs.Empty); + this.ButtonClose_Click(this.ButtonClose, EventArgs.Empty); } } private void ListFilters_DoubleClick(object sender, EventArgs e) { - var idx = ListFilters.SelectedIndex; + var idx = this.ListFilters.SelectedIndex; if (idx == -1) return; - var midx = ListFilters.IndexFromPoint(ListFilters.PointToClient(Control.MousePosition)); + var midx = this.ListFilters.IndexFromPoint(this.ListFilters.PointToClient(Control.MousePosition)); if (midx == ListBox.NoMatches || idx != midx) return; - ButtonEdit_Click(sender, e); + this.ButtonEdit_Click(sender, e); } private void FilterDialog_Shown(object sender, EventArgs e) { - ListTabs.DisplayMember = nameof(TabModel.TabName); + this.ListTabs.DisplayMember = nameof(TabModel.TabName); - ComboSound.Items.Clear(); - ComboSound.Items.Add(""); + this.ComboSound.Items.Clear(); + this.ComboSound.Items.Add(""); var oDir = new DirectoryInfo(Application.StartupPath + Path.DirectorySeparatorChar); if (Directory.Exists(Path.Combine(Application.StartupPath, "Sounds"))) { @@ -929,21 +930,21 @@ private void FilterDialog_Shown(object sender, EventArgs e) } foreach (var oFile in oDir.GetFiles("*.wav")) { - ComboSound.Items.Add(oFile.Name); + this.ComboSound.Items.Add(oFile.Name); } - idlist.Clear(); + this.idlist.Clear(); foreach (var tmp in ((TweenMain)this.Owner).AtIdSupl.GetItemList()) { - idlist.Add(tmp.Remove(0, 1)); // @文字削除 + this.idlist.Add(tmp.Remove(0, 1)); // @文字削除 } - UID.AutoCompleteCustomSource.Clear(); - UID.AutoCompleteCustomSource.AddRange(idlist.ToArray()); + this.UID.AutoCompleteCustomSource.Clear(); + this.UID.AutoCompleteCustomSource.AddRange(this.idlist.ToArray()); - ExUID.AutoCompleteCustomSource.Clear(); - ExUID.AutoCompleteCustomSource.AddRange(idlist.ToArray()); + this.ExUID.AutoCompleteCustomSource.Clear(); + this.ExUID.AutoCompleteCustomSource.AddRange(this.idlist.ToArray()); - //選択タブ変更 + // 選択タブ変更 this.ListTabs.SelectedIndex = this.selectedTabIndex; } @@ -953,9 +954,9 @@ private void ListTabs_SelectedIndexChanged(object sender, EventArgs e) var selectedTab = this.SelectedTab; if (selectedTab != null) - SetFilters(selectedTab); + this.SetFilters(selectedTab); else - ListFilters.Items.Clear(); + this.ListFilters.Items.Clear(); } private async void ButtonAddTab_Click(object sender, EventArgs e) @@ -964,7 +965,7 @@ private async void ButtonAddTab_Click(object sender, EventArgs e) MyCommon.TabUsageType tabType; using (var inputName = new InputTabName()) { - inputName.TabName = _sts.MakeTabName("MyTab"); + inputName.TabName = this.sts.MakeTabName("MyTab"); inputName.IsShowUsage = true; inputName.ShowDialog(); if (inputName.DialogResult == DialogResult.Cancel) return; @@ -973,7 +974,7 @@ private async void ButtonAddTab_Click(object sender, EventArgs e) } if (!MyCommon.IsNullOrEmpty(tabName)) { - //List対応 + // List対応 ListElement? list = null; if (tabType == MyCommon.TabUsageType.Lists) { @@ -987,7 +988,10 @@ private async void ButtonAddTab_Click(object sender, EventArgs e) cancellationToken.ThrowIfCancellationRequested(); } - catch (OperationCanceledException) { return; } + catch (OperationCanceledException) + { + return; + } catch (WebApiException ex) { MessageBox.Show("Failed to get lists. (" + ex.Message + ")"); @@ -1018,7 +1022,7 @@ private async void ButtonAddTab_Click(object sender, EventArgs e) return; } - if (!_sts.AddTab(tab) || !((TweenMain)this.Owner).AddNewTab(tab, startup: false)) + if (!this.sts.AddTab(tab) || !((TweenMain)this.Owner).AddNewTab(tab, startup: false)) { var tmp = string.Format(Properties.Resources.AddTabMenuItem_ClickText1, tabName); MessageBox.Show(tmp, Properties.Resources.AddTabMenuItem_ClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); @@ -1038,13 +1042,13 @@ private void ButtonDeleteTab_Click(object sender, EventArgs e) if (selectedTab != null) { var tb = selectedTab.TabName; - var idx = ListTabs.SelectedIndex; + var idx = this.ListTabs.SelectedIndex; if (((TweenMain)this.Owner).RemoveSpecifiedTab(tb, true)) { this.RefreshListTabs(); idx -= 1; if (idx < 0) idx = 0; - ListTabs.SelectedIndex = idx; + this.ListTabs.SelectedIndex = idx; } } } @@ -1067,7 +1071,7 @@ private void CheckManageRead_CheckedChanged(object sender, EventArgs e) { ((TweenMain)this.Owner).ChangeTabUnreadManage( selectedTab.TabName, - CheckManageRead.Checked); + this.CheckManageRead.Checked); } } @@ -1122,8 +1126,8 @@ private void CheckLocked_CheckedChanged(object sender, EventArgs e) var selectedTab = this.SelectedTab; if (selectedTab != null) { - selectedTab.Protected = CheckProtected.Checked; - ButtonDeleteTab.Enabled = !CheckProtected.Checked; + selectedTab.Protected = this.CheckProtected.Checked; + this.ButtonDeleteTab.Enabled = !this.CheckProtected.Checked; } } @@ -1132,7 +1136,7 @@ private void CheckNotifyNew_CheckedChanged(object sender, EventArgs e) var selectedTab = this.SelectedTab; if (selectedTab != null) { - selectedTab.Notify = CheckNotifyNew.Checked; + selectedTab.Notify = this.CheckNotifyNew.Checked; } } @@ -1142,21 +1146,21 @@ private void ComboSound_SelectedIndexChanged(object sender, EventArgs e) if (selectedTab != null) { var filename = ""; - if (ComboSound.SelectedIndex > -1) filename = ComboSound.SelectedItem.ToString(); + if (this.ComboSound.SelectedIndex > -1) filename = this.ComboSound.SelectedItem.ToString(); selectedTab.SoundFile = filename; } } private void RadioExAnd_CheckedChanged(object sender, EventArgs e) { - var flg = RadioExAnd.Checked; - ExUID.Enabled = flg; - ExMSG1.Enabled = flg; - ExMSG2.Enabled = !flg; + var flg = this.RadioExAnd.Checked; + this.ExUID.Enabled = flg; + this.ExMSG1.Enabled = flg; + this.ExMSG2.Enabled = !flg; } private void OptMove_CheckedChanged(object sender, EventArgs e) - => this.CheckMark.Enabled = !OptMove.Checked; + => this.CheckMark.Enabled = !this.OptMove.Checked; private void ButtonRuleUp_Click(object sender, EventArgs e) => this.MoveSelectedRules(up: true); @@ -1167,10 +1171,10 @@ private void ButtonRuleDown_Click(object sender, EventArgs e) private void MoveSelectedRules(bool up) { var selectedTab = this.SelectedTab; - if (selectedTab == null || ListFilters.SelectedIndices.Count == 0) + if (selectedTab == null || this.ListFilters.SelectedIndices.Count == 0) return; - var indices = ListFilters.SelectedIndices.Cast().ToArray(); + var indices = this.ListFilters.SelectedIndices.Cast().ToArray(); int diff; if (up) @@ -1180,7 +1184,7 @@ private void MoveSelectedRules(bool up) } else { - if (indices[indices.Length - 1] >= ListFilters.Items.Count - 1) return; + if (indices[indices.Length - 1] >= this.ListFilters.Items.Count - 1) return; diff = +1; Array.Reverse(indices); // 逆順にして、下にある要素から処理する } @@ -1190,40 +1194,40 @@ private void MoveSelectedRules(bool up) try { - _multiSelState |= MultiSelectionState.MoveSelected; + this.multiSelState |= MultiSelectionState.MoveSelected; - using (ControlTransaction.Update(ListFilters)) + using (ControlTransaction.Update(this.ListFilters)) { - ListFilters.SelectedIndices.Clear(); + this.ListFilters.SelectedIndices.Clear(); foreach (var idx in indices) { var tidx = idx + diff; - var target = (PostFilterRule)ListFilters.Items[tidx]; + var target = (PostFilterRule)this.ListFilters.Items[tidx]; // 移動先にある要素と位置を入れ替える - ListFilters.Items.RemoveAt(tidx); - ListFilters.Items.Insert(idx, target); + this.ListFilters.Items.RemoveAt(tidx); + this.ListFilters.Items.Insert(idx, target); // 移動方向の先頭要素以外なら選択する if (tidx != lastSelIdx) - ListFilters.SelectedIndex = tidx; + this.ListFilters.SelectedIndex = tidx; } - tab.FilterArray = ListFilters.Items.Cast().ToArray(); + tab.FilterArray = this.ListFilters.Items.Cast().ToArray(); // 移動方向の先頭要素は最後に選択する // ※移動方向への自動スクロール目的 - ListFilters.SelectedIndex = lastSelIdx; + this.ListFilters.SelectedIndex = lastSelIdx; } } finally { - _multiSelState &= ~MultiSelectionState.MoveSelected; + this.multiSelState &= ~MultiSelectionState.MoveSelected; } } - private void buttonRuleToggleEnabled_Click(object sender, EventArgs e) + private void ButtonRuleToggleEnabled_Click(object sender, EventArgs e) { if (this.RuleEnableButtonMode == EnableButtonMode.NotSelected) return; @@ -1248,10 +1252,10 @@ private void buttonRuleToggleEnabled_Click(object sender, EventArgs e) private void ButtonRuleCopy_Click(object sender, EventArgs e) { var selectedTab = this.SelectedTab; - if (selectedTab != null && ListFilters.SelectedItem != null) + if (selectedTab != null && this.ListFilters.SelectedItem != null) { TabModel[] destinationTabs; - using (var dialog = new TabsDialog(_sts)) + using (var dialog = new TabsDialog(this.sts)) { dialog.MultiSelect = true; dialog.Text = Properties.Resources.ButtonRuleCopy_ClickText1; @@ -1264,7 +1268,7 @@ private void ButtonRuleCopy_Click(object sender, EventArgs e) var currentTab = (FilterTabModel)selectedTab; var filters = new List(); - foreach (int idx in ListFilters.SelectedIndices) + foreach (int idx in this.ListFilters.SelectedIndices) { filters.Add(currentTab.FilterArray[idx].Clone()); } @@ -1278,17 +1282,17 @@ private void ButtonRuleCopy_Click(object sender, EventArgs e) tb.AddFilter(flt.Clone()); } } - SetFilters(selectedTab); + this.SetFilters(selectedTab); } } private void ButtonRuleMove_Click(object sender, EventArgs e) { var selectedTab = this.SelectedTab; - if (selectedTab != null && ListFilters.SelectedItem != null) + if (selectedTab != null && this.ListFilters.SelectedItem != null) { TabModel[] destinationTabs; - using (var dialog = new TabsDialog(_sts)) + using (var dialog = new TabsDialog(this.sts)) { dialog.MultiSelect = true; dialog.Text = Properties.Resources.ButtonRuleMove_ClickText1; @@ -1300,7 +1304,7 @@ private void ButtonRuleMove_Click(object sender, EventArgs e) var currentTab = (FilterTabModel)selectedTab; var filters = new List(); - foreach (int idx in ListFilters.SelectedIndices) + foreach (int idx in this.ListFilters.SelectedIndices) { filters.Add(currentTab.FilterArray[idx].Clone()); } @@ -1315,15 +1319,15 @@ private void ButtonRuleMove_Click(object sender, EventArgs e) tb.AddFilter(flt.Clone()); } } - for (var idx = ListFilters.Items.Count - 1; idx >= 0; idx--) + for (var idx = this.ListFilters.Items.Count - 1; idx >= 0; idx--) { - if (ListFilters.GetSelected(idx)) + if (this.ListFilters.GetSelected(idx)) { - currentTab.RemoveFilter((PostFilterRule)ListFilters.Items[idx]); - ListFilters.Items.RemoveAt(idx); + currentTab.RemoveFilter((PostFilterRule)this.ListFilters.Items[idx]); + this.ListFilters.Items.RemoveAt(idx); } } - SetFilters(selectedTab); + this.SetFilters(selectedTab); } } @@ -1370,7 +1374,7 @@ private void FilterTextBox_KeyPress(object sender, KeyPressEventArgs e) var tbox = (TextBox)sender; if (e.KeyChar == '@') { - //@マーク + // @マーク main.ShowSuplDialog(tbox, main.AtIdSupl); e.Handled = true; } @@ -1417,7 +1421,7 @@ private void ListFilters_KeyDown(object sender, KeyEventArgs e) { try { - _multiSelState |= MultiSelectionState.SelectAll; + this.multiSelState |= MultiSelectionState.SelectAll; for (var i = 1; i < itemCount; i++) { @@ -1426,7 +1430,7 @@ private void ListFilters_KeyDown(object sender, KeyEventArgs e) } finally { - _multiSelState &= ~MultiSelectionState.SelectAll; + this.multiSelState &= ~MultiSelectionState.SelectAll; } } diff --git a/OpenTween/Growl.cs b/OpenTween/GrowlHelper.cs similarity index 60% rename from OpenTween/Growl.cs rename to OpenTween/GrowlHelper.cs index f989ec912..0773c7617 100644 --- a/OpenTween/Growl.cs +++ b/OpenTween/GrowlHelper.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -27,37 +27,39 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Reflection; -using System.IO; +using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; -using System.Windows.Forms; -using System.ComponentModel; -using System.Collections; using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Forms; namespace OpenTween { public class GrowlHelper { - private Assembly? _connector = null; - private Assembly? _core = null; + private Assembly? connector = null; + private Assembly? core = null; - private object? _growlNTreply; - private object? _growlNTdm; - private object? _growlNTnew; - private object? _growlApp; + private object? growlNTreply; + private object? growlNTdm; + private object? growlNTnew; + private object? growlApp; - private object? _targetConnector; - bool _initialized = false; + private object? targetConnector; + private bool initialized = false; public class NotifyCallbackEventArgs : EventArgs { public long StatusId { get; set; } + public NotifyType NotifyType { get; set; } + public NotifyCallbackEventArgs(NotifyType notifyType, string statusId) { if (statusId.Length > 1) @@ -86,7 +88,7 @@ public bool IsAvailable { get { - if (_connector == null || _core == null || !_initialized) + if (this.connector == null || this.core == null || !this.initialized) return false; else return true; @@ -96,7 +98,7 @@ public bool IsAvailable private byte[] IconToByteArray(string filename) { using var ic = new Icon(filename); - return IconToByteArray(ic); + return this.IconToByteArray(ic); } private byte[] IconToByteArray(Icon icondata) @@ -123,7 +125,7 @@ public static bool IsDllExists public bool RegisterGrowl() { - _initialized = false; + this.initialized = false; var dir = Application.StartupPath; var connectorPath = Path.Combine(dir, "Growl.Connector.dll"); var corePath = Path.Combine(dir, "Growl.CoreLibrary.dll"); @@ -131,8 +133,8 @@ public bool RegisterGrowl() try { if (!IsDllExists) return false; - _connector = Assembly.LoadFile(connectorPath); - _core = Assembly.LoadFile(corePath); + this.connector = Assembly.LoadFile(connectorPath); + this.core = Assembly.LoadFile(corePath); } catch (Exception) { @@ -141,107 +143,124 @@ public bool RegisterGrowl() try { - _targetConnector = _connector.CreateInstance("Growl.Connector.GrowlConnector"); - var _t = _connector.GetType("Growl.Connector.NotificationType"); - - _growlNTreply = _t.InvokeMember(null, - BindingFlags.CreateInstance, null, null, new object[] { "REPLY", "Reply" }, CultureInfo.InvariantCulture); - - _growlNTdm = _t.InvokeMember(null, - BindingFlags.CreateInstance, null, null, new object[] { "DIRECT_MESSAGE", "DirectMessage" }, CultureInfo.InvariantCulture); - - _growlNTnew = _t.InvokeMember(null, - BindingFlags.CreateInstance, null, null, new object[] { "NOTIFY", "新着通知" }, CultureInfo.InvariantCulture); + this.targetConnector = this.connector.CreateInstance("Growl.Connector.GrowlConnector"); + var t = this.connector.GetType("Growl.Connector.NotificationType"); + + this.growlNTreply = t.InvokeMember( + null, + BindingFlags.CreateInstance, + null, + null, + new object[] { "REPLY", "Reply" }, + CultureInfo.InvariantCulture); + + this.growlNTdm = t.InvokeMember(null, + BindingFlags.CreateInstance, + null, + null, + new object[] { "DIRECT_MESSAGE", "DirectMessage" }, + CultureInfo.InvariantCulture); + + this.growlNTnew = t.InvokeMember( + null, + BindingFlags.CreateInstance, + null, + null, + new object[] { "NOTIFY", "新着通知" }, + CultureInfo.InvariantCulture); var encryptType = - _connector.GetType("Growl.Connector.Cryptography+SymmetricAlgorithmType").InvokeMember( + this.connector.GetType("Growl.Connector.Cryptography+SymmetricAlgorithmType").InvokeMember( "PlainText", BindingFlags.GetField, null, null, null, CultureInfo.InvariantCulture); - _targetConnector.GetType().InvokeMember("EncryptionAlgorithm", BindingFlags.SetProperty, null, _targetConnector, new object[] { encryptType }, CultureInfo.InvariantCulture); - - _growlApp = _connector.CreateInstance( - "Growl.Connector.Application", false, BindingFlags.Default, null, new object[] { AppName }, null, null); + this.targetConnector.GetType().InvokeMember("EncryptionAlgorithm", BindingFlags.SetProperty, null, this.targetConnector, new object[] { encryptType }, CultureInfo.InvariantCulture); + this.growlApp = this.connector.CreateInstance( + "Growl.Connector.Application", false, BindingFlags.Default, null, new object[] { this.AppName }, null, null); if (File.Exists(Path.Combine(Application.StartupPath, "Icons\\Tween.png"))) { // Icons\Tween.pngを使用 - var ci = _core.GetType( - "Growl.CoreLibrary.Resource").GetConstructor( + var ci = this.core.GetType("Growl.CoreLibrary.Resource").GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, - null, new Type[] { typeof(string) }, null); + null, + new Type[] { typeof(string) }, + null); var data = ci.Invoke(new object[] { Path.Combine(Application.StartupPath, "Icons\\Tween.png") }); - var pi = _growlApp.GetType().GetProperty("Icon"); - pi.SetValue(_growlApp, data, null); - + var pi = this.growlApp.GetType().GetProperty("Icon"); + pi.SetValue(this.growlApp, data, null); } else if (File.Exists(Path.Combine(Application.StartupPath, "Icons\\MIcon.ico"))) { // アイコンセットにMIcon.icoが存在する場合それを使用 - var cibd = _core.GetType( - "Growl.CoreLibrary.BinaryData").GetConstructor( + var cibd = this.core.GetType("Growl.CoreLibrary.BinaryData").GetConstructor( BindingFlags.Public | BindingFlags.Instance, - null, new Type[] { typeof(byte[]) }, null); + null, + new Type[] { typeof(byte[]) }, + null); var bdata = cibd.Invoke( - new object[] { IconToByteArray(Path.Combine(Application.StartupPath, "Icons\\MIcon.ico")) }); + new object[] { this.IconToByteArray(Path.Combine(Application.StartupPath, "Icons\\MIcon.ico")) }); - var ciRes = _core.GetType( - "Growl.CoreLibrary.Resource").GetConstructor( + var ciRes = this.core.GetType("Growl.CoreLibrary.Resource").GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, - null, new Type[] { bdata.GetType() }, null); + null, + new Type[] { bdata.GetType() }, + null); var data = ciRes.Invoke(new object[] { bdata }); - var pi = _growlApp.GetType().GetProperty("Icon"); - pi.SetValue(_growlApp, data, null); + var pi = this.growlApp.GetType().GetProperty("Icon"); + pi.SetValue(this.growlApp, data, null); } else { - //内蔵アイコンリソースを使用 - var cibd = _core.GetType( - "Growl.CoreLibrary.BinaryData").GetConstructor( + // 内蔵アイコンリソースを使用 + var cibd = this.core.GetType("Growl.CoreLibrary.BinaryData").GetConstructor( BindingFlags.Public | BindingFlags.Instance, - null, new Type[] { typeof(byte[]) }, null); + null, + new Type[] { typeof(byte[]) }, + null); var bdata = cibd.Invoke( - new object[] { IconToByteArray(Properties.Resources.MIcon) }); + new object[] { this.IconToByteArray(Properties.Resources.MIcon) }); - var ciRes = _core.GetType( - "Growl.CoreLibrary.Resource").GetConstructor( + var ciRes = this.core.GetType("Growl.CoreLibrary.Resource").GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, - null, new Type[] { bdata.GetType() }, null); + null, + new Type[] { bdata.GetType() }, + null); var data = ciRes.Invoke(new object[] { bdata }); - var pi = _growlApp.GetType().GetProperty("Icon"); - pi.SetValue(_growlApp, data, null); + var pi = this.growlApp.GetType().GetProperty("Icon"); + pi.SetValue(this.growlApp, data, null); } - var mi = _targetConnector.GetType().GetMethod("Register", new Type[] { _growlApp.GetType(), _connector.GetType("Growl.Connector.NotificationType[]") }); + var mi = this.targetConnector.GetType().GetMethod("Register", new Type[] { this.growlApp.GetType(), this.connector.GetType("Growl.Connector.NotificationType[]") }); - _t = _connector.GetType("Growl.Connector.NotificationType"); + t = this.connector.GetType("Growl.Connector.NotificationType"); var arglist = new ArrayList { - _growlNTreply, - _growlNTdm, - _growlNTnew, + this.growlNTreply, + this.growlNTdm, + this.growlNTnew, }; - mi.Invoke(_targetConnector, new object[] { _growlApp, arglist.ToArray(_t) }); + mi.Invoke(this.targetConnector, new object[] { this.growlApp, arglist.ToArray(t) }); // コールバックメソッドの登録 - var tGrowlConnector = _connector.GetType("Growl.Connector.GrowlConnector"); + var tGrowlConnector = this.connector.GetType("Growl.Connector.GrowlConnector"); var evNotificationCallback = tGrowlConnector.GetEvent("NotificationCallback"); var tDelegate = evNotificationCallback.EventHandlerType; var miHandler = typeof(GrowlHelper).GetMethod("GrowlCallbackHandler", BindingFlags.NonPublic | BindingFlags.Instance); var d = Delegate.CreateDelegate(tDelegate, this, miHandler); var miAddHandler = evNotificationCallback.GetAddMethod(); object[] addHandlerArgs = { d }; - miAddHandler.Invoke(_targetConnector, addHandlerArgs); + miAddHandler.Invoke(this.targetConnector, addHandlerArgs); - _initialized = true; + this.initialized = true; } catch (Exception) { - _initialized = false; + this.initialized = false; return false; } @@ -250,7 +269,7 @@ public bool RegisterGrowl() public void Notify(NotifyType notificationType, string id, string title, string text, Image? icon = null, string url = "") { - if (!_initialized) return; + if (!this.initialized) return; var notificationName = notificationType switch { @@ -263,7 +282,7 @@ public void Notify(NotifyType notificationType, string id, string title, string object? n; if (icon != null || !MyCommon.IsNullOrEmpty(url)) { - var gCore = _core!.GetType("Growl.CoreLibrary.Resource"); + var gCore = this.core!.GetType("Growl.CoreLibrary.Resource"); object? res; if (icon != null) { @@ -284,43 +303,52 @@ public void Notify(NotifyType notificationType, string id, string title, string CultureInfo.InvariantCulture); } var priority = - _connector!.GetType("Growl.Connector.Priority").InvokeMember( + this.connector!.GetType("Growl.Connector.Priority").InvokeMember( "Normal", BindingFlags.GetField, null, null, null, CultureInfo.InvariantCulture); - n = _connector!.GetType("Growl.Connector.Notification").InvokeMember( + n = this.connector!.GetType("Growl.Connector.Notification").InvokeMember( "Notification", BindingFlags.CreateInstance, null, - _connector, - new object[] {AppName, - notificationName, - id, - title, - text, - res, - false, - priority, - "aaa"}, + this.connector, + new object[] + { + this.AppName, + notificationName, + id, + title, + text, + res, + false, + priority, + "aaa", + }, CultureInfo.InvariantCulture); } else { - n = _connector!.GetType("Growl.Connector.Notification").InvokeMember( + n = this.connector!.GetType("Growl.Connector.Notification").InvokeMember( "Notification", BindingFlags.CreateInstance, null, - _connector, - new object[] {AppName, - notificationName, - id, - title, - text}, + this.connector, + new object[] + { + this.AppName, + notificationName, + id, + title, + text, + }, CultureInfo.InvariantCulture); } - var cc = _connector.GetType("Growl.Connector.CallbackContext").InvokeMember( - null, BindingFlags.CreateInstance, null, _connector, + var cc = this.connector.GetType("Growl.Connector.CallbackContext").InvokeMember( + null, + BindingFlags.CreateInstance, + null, + this.connector, new object[] { "some fake information", notificationName }, CultureInfo.InvariantCulture); - _targetConnector!.GetType().InvokeMember("Notify", BindingFlags.InvokeMethod, null, _targetConnector, new object[] { n, cc }, CultureInfo.InvariantCulture); + this.targetConnector!.GetType().InvokeMember("Notify", BindingFlags.InvokeMethod, null, this.targetConnector, new object[] { n, cc }, CultureInfo.InvariantCulture); } private void GrowlCallbackHandler(object response, object callbackData, object state) @@ -329,9 +357,9 @@ private void GrowlCallbackHandler(object response, object callbackData, object s { // 定数取得 var vCLICK = - _core!.GetType("Growl.CoreLibrary.CallbackResult").GetField( - "CLICK", - BindingFlags.Public | BindingFlags.Static).GetRawConstantValue(); + this.core!.GetType("Growl.CoreLibrary.CallbackResult").GetField( + "CLICK", + BindingFlags.Public | BindingFlags.Static).GetRawConstantValue(); // 実際の値 var vResult = callbackData.GetType().GetProperty( "Result", @@ -357,7 +385,7 @@ private void GrowlCallbackHandler(object response, object callbackData, object s return; } - NotifyClicked?.Invoke(this, new NotifyCallbackEventArgs(nt, notifyId)); + this.NotifyClicked?.Invoke(this, new NotifyCallbackEventArgs(nt, notifyId)); } } catch (Exception) diff --git a/OpenTween/HashtagManage.cs b/OpenTween/HashtagManage.cs index 01d8b3367..db006fa2d 100644 --- a/OpenTween/HashtagManage.cs +++ b/OpenTween/HashtagManage.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -41,8 +41,11 @@ namespace OpenTween public partial class HashtagManage : OTBaseForm { public string UseHash { get; private set; } = ""; + public bool IsPermanent { get; private set; } = false; + public bool IsHead { get; private set; } = false; + public bool IsNotAddToAtReply { get; private set; } = true; /// @@ -50,11 +53,11 @@ public partial class HashtagManage : OTBaseForm /// public bool RunSilent { get; set; } - //入力補助画面 - private readonly AtIdSupplement _hashSupl; + // 入力補助画面 + private readonly AtIdSupplement hashSupl; - //編集モード - private bool _isAdd = false; + // 編集モード + private bool isAdd = false; private void ChangeMode(bool isEdit) { @@ -76,16 +79,16 @@ private void Cancel_Button_Click(object sender, EventArgs e) private void AddButton_Click(object sender, EventArgs e) { this.UseHashText.Text = ""; - ChangeMode(true); - _isAdd = true; + this.ChangeMode(true); + this.isAdd = true; } private void EditButton_Click(object sender, EventArgs e) { if (this.HistoryHashList.SelectedIndices.Count == 0) return; this.UseHashText.Text = this.HistoryHashList.SelectedItems[0].ToString(); - ChangeMode(true); - _isAdd = false; + this.ChangeMode(true); + this.isAdd = false; } private void DeleteButton_Click(object sender, EventArgs e) @@ -106,12 +109,12 @@ private void DeleteButton_Click(object sender, EventArgs e) foreach (var idx in selectedIndices) { - if (UseHashText.Text == HistoryHashList.Items[idx].ToString()) UseHashText.Text = ""; - HistoryHashList.Items.RemoveAt(idx); + if (this.UseHashText.Text == this.HistoryHashList.Items[idx].ToString()) this.UseHashText.Text = ""; + this.HistoryHashList.Items.RemoveAt(idx); } - if (HistoryHashList.Items.Count > 0) + if (this.HistoryHashList.Items.Count > 0) { - HistoryHashList.SelectedIndex = 0; + this.HistoryHashList.SelectedIndex = 0; } } @@ -119,8 +122,9 @@ private void UnSelectButton_Click(object sender, EventArgs e) { do { - HistoryHashList.SelectedIndices.Clear(); - } while (HistoryHashList.SelectedIndices.Count > 0); + this.HistoryHashList.SelectedIndices.Clear(); + } + while (this.HistoryHashList.SelectedIndices.Count > 0); } private int GetIndexOf(ListBox.ObjectCollection list, string value) @@ -155,36 +159,36 @@ public void AddHashToHistory(string hash, bool isIgnorePermanent) { if (isIgnorePermanent || !this.IsPermanent) { - //無条件に先頭に挿入 - var idx = GetIndexOf(HistoryHashList.Items, hash); + // 無条件に先頭に挿入 + var idx = this.GetIndexOf(this.HistoryHashList.Items, hash); - if (idx != -1) HistoryHashList.Items.RemoveAt(idx); - HistoryHashList.Items.Insert(0, hash); + if (idx != -1) this.HistoryHashList.Items.RemoveAt(idx); + this.HistoryHashList.Items.Insert(0, hash); } else { - //固定されていたら2行目に挿入 - var idx = GetIndexOf(HistoryHashList.Items, hash); + // 固定されていたら2行目に挿入 + var idx = this.GetIndexOf(this.HistoryHashList.Items, hash); if (this.IsPermanent) { if (idx > 0) { - //重複アイテムが2行目以降にあれば2行目へ - HistoryHashList.Items.RemoveAt(idx); - HistoryHashList.Items.Insert(1, hash); + // 重複アイテムが2行目以降にあれば2行目へ + this.HistoryHashList.Items.RemoveAt(idx); + this.HistoryHashList.Items.Insert(1, hash); } else if (idx == -1) { - //重複アイテムなし - if (HistoryHashList.Items.Count == 0) + // 重複アイテムなし + if (this.HistoryHashList.Items.Count == 0) { - //リストが空なら追加 - HistoryHashList.Items.Add(hash); + // リストが空なら追加 + this.HistoryHashList.Items.Add(hash); } else { - //リストにアイテムがあれば2行目へ - HistoryHashList.Items.Insert(1, hash); + // リストにアイテムがあれば2行目へ + this.HistoryHashList.Items.Insert(1, hash); } } } @@ -194,11 +198,11 @@ public void AddHashToHistory(string hash, bool isIgnorePermanent) private void HashtagManage_Shown(object sender, EventArgs e) { - //オプション + // オプション this.CheckPermanent.Checked = this.IsPermanent; this.RadioHead.Checked = this.IsHead; this.RadioLast.Checked = !this.IsHead; - //リスト選択 + // リスト選択 if (this.HistoryHashList.Items.Contains(this.UseHash)) { this.HistoryHashList.SelectedItem = this.UseHash; @@ -211,41 +215,41 @@ private void HashtagManage_Shown(object sender, EventArgs e) this.ChangeMode(false); } - public HashtagManage(AtIdSupplement hashSuplForm, string[] history, string permanentHash, bool IsPermanent, bool IsHead, bool IsNotAddToAtReply) + public HashtagManage(AtIdSupplement hashSuplForm, string[] history, string permanentHash, bool isPermanent, bool isHead, bool isNotAddToAtReply) { // この呼び出しは、Windows フォーム デザイナで必要です。 - InitializeComponent(); + this.InitializeComponent(); // InitializeComponent() 呼び出しの後で初期化を追加します。 - _hashSupl = hashSuplForm; - HistoryHashList.Items.AddRange(history); + this.hashSupl = hashSuplForm; + this.HistoryHashList.Items.AddRange(history); this.UseHash = permanentHash; - this.IsPermanent = IsPermanent; - this.IsHead = IsHead; - this.IsNotAddToAtReply = IsNotAddToAtReply; + this.IsPermanent = isPermanent; + this.IsHead = isHead; + this.IsNotAddToAtReply = isNotAddToAtReply; } private void UseHashText_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '#') { - _hashSupl.ShowDialog(); - if (!MyCommon.IsNullOrEmpty(_hashSupl.inputText)) + this.hashSupl.ShowDialog(); + if (!MyCommon.IsNullOrEmpty(this.hashSupl.InputText)) { var fHalf = ""; var eHalf = ""; - var selStart = UseHashText.SelectionStart; + var selStart = this.UseHashText.SelectionStart; if (selStart > 0) { - fHalf = UseHashText.Text.Substring(0, selStart); + fHalf = this.UseHashText.Text.Substring(0, selStart); } - if (selStart < UseHashText.Text.Length) + if (selStart < this.UseHashText.Text.Length) { - eHalf = UseHashText.Text.Substring(selStart); + eHalf = this.UseHashText.Text.Substring(selStart); } - UseHashText.Text = fHalf + _hashSupl.inputText + eHalf; - UseHashText.SelectionStart = selStart + _hashSupl.inputText.Length; + this.UseHashText.Text = fHalf + this.hashSupl.InputText + eHalf; + this.UseHashText.SelectionStart = selStart + this.hashSupl.InputText.Length; } e.Handled = true; } @@ -272,7 +276,7 @@ public List HashHistories get { var hash = new List(); - foreach (string item in HistoryHashList.Items) + foreach (string item in this.HistoryHashList.Items) { hash.Add(item); } @@ -285,27 +289,28 @@ public void ClearHashtag() public void SetPermanentHash(string hash) { - //固定ハッシュタグの変更 + // 固定ハッシュタグの変更 this.UseHash = hash.Trim(); - this.AddHashToHistory(UseHash, false); + this.AddHashToHistory(this.UseHash, false); this.IsPermanent = true; } private void PermOK_Button_Click(object sender, EventArgs e) { - //ハッシュタグの整形 - var hashStr = UseHashText.Text; + // ハッシュタグの整形 + var hashStr = this.UseHashText.Text; if (!this.AdjustHashtags(ref hashStr, !this.RunSilent)) return; - UseHashText.Text = hashStr; - if (!this._isAdd && this.HistoryHashList.SelectedIndices.Count > 0) + this.UseHashText.Text = hashStr; + if (!this.isAdd && this.HistoryHashList.SelectedIndices.Count > 0) { var idx = this.HistoryHashList.SelectedIndices[0]; this.HistoryHashList.Items.RemoveAt(idx); do { this.HistoryHashList.SelectedIndices.Clear(); - } while (this.HistoryHashList.SelectedIndices.Count > 0); + } + while (this.HistoryHashList.SelectedIndices.Count > 0); this.HistoryHashList.Items.Insert(idx, hashStr); this.HistoryHashList.SelectedIndex = idx; } @@ -315,11 +320,12 @@ private void PermOK_Button_Click(object sender, EventArgs e) do { this.HistoryHashList.SelectedIndices.Clear(); - } while (this.HistoryHashList.SelectedIndices.Count > 0); + } + while (this.HistoryHashList.SelectedIndices.Count > 0); this.HistoryHashList.SelectedIndex = this.HistoryHashList.Items.IndexOf(hashStr); } - ChangeMode(false); + this.ChangeMode(false); } private void PermCancel_Button_Click(object sender, EventArgs e) @@ -329,7 +335,7 @@ private void PermCancel_Button_Click(object sender, EventArgs e) else this.UseHashText.Text = ""; - ChangeMode(false); + this.ChangeMode(false); } private void HistoryHashList_KeyDown(object sender, KeyEventArgs e) @@ -342,7 +348,7 @@ private void HistoryHashList_KeyDown(object sender, KeyEventArgs e) private bool AdjustHashtags(ref string hashtag, bool isShowWarn) { - //ハッシュタグの整形 + // ハッシュタグの整形 hashtag = hashtag.Trim(); if (MyCommon.IsNullOrEmpty(hashtag)) { @@ -366,7 +372,7 @@ private bool AdjustHashtags(ref string hashtag, bool isShowWarn) if (isShowWarn) MessageBox.Show("empty hashtag.", "Hashtag warning", MessageBoxButtons.OK, MessageBoxIcon.Asterisk); return false; } - //使用不可の文字チェックはしない + // 使用不可の文字チェックはしない adjust += hash + " "; } } @@ -389,7 +395,7 @@ private void OK_Button_Click(object sender, EventArgs e) } else { - //使用ハッシュが未選択ならば、固定オプション外す + // 使用ハッシュが未選択ならば、固定オプション外す this.IsPermanent = false; } this.IsHead = this.RadioHead.Checked; @@ -420,6 +426,6 @@ private void HashtagManage_KeyDown(object sender, KeyEventArgs e) } private void CheckNotAddToAtReply_CheckedChanged(object sender, EventArgs e) - => this.IsNotAddToAtReply = CheckNotAddToAtReply.Checked; + => this.IsNotAddToAtReply = this.CheckNotAddToAtReply.Checked; } } diff --git a/OpenTween/HookGlobalHotkey.cs b/OpenTween/HookGlobalHotkey.cs index e7c36da3c..ed3b8dfe6 100644 --- a/OpenTween/HookGlobalHotkey.cs +++ b/OpenTween/HookGlobalHotkey.cs @@ -36,20 +36,14 @@ namespace OpenTween { public class HookGlobalHotkey : NativeWindow, IDisposable { - private readonly Form _targetForm; - private class KeyEventValue - { - public KeyEventArgs KeyEvent { get; } - public int Value { get; } + private readonly Form targetForm; - public KeyEventValue(KeyEventArgs keyEvent, int Value) - { - this.KeyEvent = keyEvent; - this.Value = Value; - } - } + private readonly record struct KeyEventValue( + KeyEventArgs KeyEvent, + int Value + ); - private readonly Dictionary _hotkeyID; + private readonly Dictionary hotkeyID; [Flags] public enum ModKeys @@ -68,9 +62,9 @@ protected override void WndProc(ref Message m) const int WM_HOTKEY = 0x312; if (m.Msg == WM_HOTKEY) { - if (_hotkeyID.ContainsKey(m.WParam.ToInt32())) + if (this.hotkeyID.ContainsKey(m.WParam.ToInt32())) { - HotkeyPressed?.Invoke(this, _hotkeyID[m.WParam.ToInt32()].KeyEvent); + this.HotkeyPressed?.Invoke(this, this.hotkeyID[m.WParam.ToInt32()].KeyEvent); } return; } @@ -79,15 +73,15 @@ protected override void WndProc(ref Message m) public HookGlobalHotkey(Form targetForm) { - _targetForm = targetForm; - _hotkeyID = new Dictionary(); + this.targetForm = targetForm; + this.hotkeyID = new Dictionary(); - _targetForm.HandleCreated += this.OnHandleCreated; - _targetForm.HandleDestroyed += this.OnHandleDestroyed; + this.targetForm.HandleCreated += this.OnHandleCreated; + this.targetForm.HandleDestroyed += this.OnHandleDestroyed; } public void OnHandleCreated(object sender, EventArgs e) - => this.AssignHandle(_targetForm.Handle); + => this.AssignHandle(this.targetForm.Handle); public void OnHandleDestroyed(object sender, EventArgs e) => this.ReleaseHandle(); @@ -100,14 +94,14 @@ public bool RegisterOriginalHotkey(Keys hotkey, int hotkeyValue, ModKeys modifie if ((modifiers & ModKeys.Shift) == ModKeys.Shift) modKey |= Keys.Shift; if ((modifiers & ModKeys.Win) == ModKeys.Win) modKey |= Keys.LWin; var key = new KeyEventArgs(hotkey | modKey); - foreach (var (_, value) in this._hotkeyID) + foreach (var (_, value) in this.hotkeyID) { if (value.KeyEvent.KeyData == key.KeyData && value.Value == hotkeyValue) return true; // 登録済みなら正常終了 } - var hotkeyId = NativeMethods.RegisterGlobalHotKey(hotkeyValue, (int)modifiers, this._targetForm); + var hotkeyId = NativeMethods.RegisterGlobalHotKey(hotkeyValue, (int)modifiers, this.targetForm); if (hotkeyId > 0) { - this._hotkeyID.Add(hotkeyId, new KeyEventValue(key, hotkeyValue)); + this.hotkeyID.Add(hotkeyId, new KeyEventValue(key, hotkeyValue)); return true; } return false; @@ -115,11 +109,11 @@ public bool RegisterOriginalHotkey(Keys hotkey, int hotkeyValue, ModKeys modifie public void UnregisterAllOriginalHotkey() { - foreach (ushort hotkeyId in this._hotkeyID.Keys) + foreach (ushort hotkeyId in this.hotkeyID.Keys) { - NativeMethods.UnregisterGlobalHotKey(hotkeyId, this._targetForm); + NativeMethods.UnregisterGlobalHotKey(hotkeyId, this.targetForm); } - this._hotkeyID.Clear(); + this.hotkeyID.Clear(); } private bool disposedValue = false; // 重複する呼び出しを検出するには @@ -133,11 +127,11 @@ protected virtual void Dispose(bool disposing) { } - if (this._targetForm != null && !this._targetForm.IsDisposed) + if (this.targetForm != null && !this.targetForm.IsDisposed) { this.UnregisterAllOriginalHotkey(); - _targetForm.HandleCreated -= this.OnHandleCreated; - _targetForm.HandleDestroyed -= this.OnHandleDestroyed; + this.targetForm.HandleCreated -= this.OnHandleCreated; + this.targetForm.HandleDestroyed -= this.OnHandleDestroyed; } } this.disposedValue = true; @@ -148,7 +142,7 @@ protected virtual void Dispose(bool disposing) public void Dispose() { // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。 - Dispose(true); + this.Dispose(true); GC.SuppressFinalize(this); } #endregion diff --git a/OpenTween/IconAssetsManager.cs b/OpenTween/IconAssetsManager.cs new file mode 100644 index 000000000..b416efe2b --- /dev/null +++ b/OpenTween/IconAssetsManager.cs @@ -0,0 +1,173 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2007-2011 kiri_feather (@kiri_feather) +// (c) 2008-2011 Moz (@syo68k) +// (c) 2008-2011 takeshik (@takeshik) +// (c) 2010-2011 anis774 (@anis774) +// (c) 2010-2011 fantasticswallow (@f_swallow) +// (c) 2011 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License +// for more details. +// +// You should have received a copy of the GNU General public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Drawing; +using System.IO; +using System.Windows.Forms; + +namespace OpenTween +{ + public sealed class IconAssetsManager : IDisposable + { + public bool IsDisposed { get; private set; } = false; + + /// ウィンドウ左上のアイコン + public Icon IconMain { get; } + + /// タブ見出し未読表示アイコン + public Icon IconTab { get; } + + /// タスクトレイ: 通常時アイコン + public Icon IconTray { get; } + + /// タスクトレイ: エラー時アイコン + public Icon IconTrayError { get; } + + /// タスクトレイ: オフライン時アイコン + public Icon IconTrayOffline { get; } + + /// タスクトレイ: Reply通知アイコン + public Icon IconTrayReply { get; } + + /// タスクトレイ: Reply通知アイコン(点滅時) + public Icon IconTrayReplyBlink { get; } + + /// タスクトレイ: 更新中アイコン + public Icon[] IconTrayRefresh { get; } + + private readonly Icon? iconMain; + private readonly Icon? iconTab; + private readonly Icon? iconAt; + private readonly Icon? iconAtRed; + private readonly Icon? iconAtSmoke; + private readonly Icon? iconReply; + private readonly Icon? iconReplyBlink; + private readonly Icon? iconRefresh1; + private readonly Icon? iconRefresh2; + private readonly Icon? iconRefresh3; + private readonly Icon? iconRefresh4; + + public IconAssetsManager() + : this(Path.Combine(Application.StartupPath, "Icons")) + { + } + + public IconAssetsManager(string iconsDir) + { + this.iconMain = this.LoadIcon(iconsDir, "MIcon.ico"); + this.iconTab = this.LoadIcon(iconsDir, "Tab.ico"); + this.iconAt = this.LoadIcon(iconsDir, "At.ico"); + this.iconAtRed = this.LoadIcon(iconsDir, "AtRed.ico"); + this.iconAtSmoke = this.LoadIcon(iconsDir, "AtSmoke.ico"); + this.iconReply = this.LoadIcon(iconsDir, "Reply.ico"); + this.iconReplyBlink = this.LoadIcon(iconsDir, "ReplyBlink.ico"); + this.iconRefresh1 = this.LoadIcon(iconsDir, "Refresh.ico"); + this.iconRefresh2 = this.LoadIcon(iconsDir, "Refresh2.ico"); + this.iconRefresh3 = this.LoadIcon(iconsDir, "Refresh3.ico"); + this.iconRefresh4 = this.LoadIcon(iconsDir, "Refresh4.ico"); + + this.IconMain = this.iconMain ?? Properties.Resources.MIcon; + this.IconTab = this.iconTab ?? Properties.Resources.TabIcon; + this.IconTray = this.iconAt ?? this.iconMain ?? Properties.Resources.At; + this.IconTrayError = this.iconAtRed ?? Properties.Resources.AtRed; + this.IconTrayOffline = this.iconAtSmoke ?? Properties.Resources.AtSmoke; + + if (this.iconReply != null && this.iconReplyBlink != null) + { + this.IconTrayReply = this.iconReply; + this.IconTrayReplyBlink = this.iconReplyBlink; + } + else + { + this.IconTrayReply = this.iconReply ?? this.iconReplyBlink ?? Properties.Resources.Reply; + this.IconTrayReplyBlink = this.IconTray; + } + + if (this.iconRefresh1 == null) + { + this.IconTrayRefresh = new[] + { + Properties.Resources.Refresh, Properties.Resources.Refresh2, + Properties.Resources.Refresh3, Properties.Resources.Refresh4, + }; + } + else if (this.iconRefresh2 == null) + { + this.IconTrayRefresh = new[] { this.iconRefresh1 }; + } + else if (this.iconRefresh3 == null) + { + this.IconTrayRefresh = new[] { this.iconRefresh1, this.iconRefresh2 }; + } + else if (this.iconRefresh4 == null) + { + this.IconTrayRefresh = new[] { this.iconRefresh1, this.iconRefresh2, this.iconRefresh3 }; + } + else // iconRefresh1 から iconRefresh4 まで全て揃っている + { + this.IconTrayRefresh = new[] { this.iconRefresh1, this.iconRefresh2, this.iconRefresh3, this.iconRefresh4 }; + } + } + + private Icon? LoadIcon(string baseDir, string fileName) + { + var filePath = Path.Combine(baseDir, fileName); + if (!File.Exists(filePath)) + return null; + + try + { + return new(filePath); + } + catch + { + return null; + } + } + + public void Dispose() + { + if (this.IsDisposed) + return; + + this.iconMain?.Dispose(); + this.iconTab?.Dispose(); + this.iconAt?.Dispose(); + this.iconAtRed?.Dispose(); + this.iconAtSmoke?.Dispose(); + this.iconReply?.Dispose(); + this.iconReplyBlink?.Dispose(); + this.iconRefresh1?.Dispose(); + this.iconRefresh2?.Dispose(); + this.iconRefresh3?.Dispose(); + this.iconRefresh4?.Dispose(); + this.IsDisposed = true; + } + } +} diff --git a/OpenTween/ImageCache.cs b/OpenTween/ImageCache.cs index 5b2bdd0a4..690a7aced 100644 --- a/OpenTween/ImageCache.cs +++ b/OpenTween/ImageCache.cs @@ -24,12 +24,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Net; +using System.Net.Http; +using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Xml.Serialization; -using System.Net.Http; using OpenTween.Connection; namespace OpenTween @@ -39,7 +39,7 @@ public class ImageCache : IDisposable /// /// キャッシュとして URL と取得した画像を対に保持する辞書 /// - internal LRUCacheDictionary> innerDictionary; + internal LRUCacheDictionary> InnerDictionary; /// /// 非同期タスクをキャンセルするためのトークンのもと @@ -49,7 +49,7 @@ public class ImageCache : IDisposable /// /// innerDictionary の排他制御のためのロックオブジェクト /// - private readonly object lockObject = new object(); + private readonly object lockObject = new(); /// /// オブジェクトが破棄された否か @@ -58,8 +58,9 @@ public class ImageCache : IDisposable public ImageCache() { - this.innerDictionary = new LRUCacheDictionary>(trimLimit: 300, autoTrimCount: 100); - this.innerDictionary.CacheRemoved += (s, e) => { + this.InnerDictionary = new LRUCacheDictionary>(trimLimit: 300, autoTrimCount: 100); + this.InnerDictionary.CacheRemoved += (s, e) => + { // まだ参照されている場合もあるのでDisposeはファイナライザ任せ this.CacheRemoveCount++; }; @@ -71,7 +72,7 @@ public ImageCache() /// 保持しているキャッシュの件数 /// public long CacheCount - => this.innerDictionary.Count; + => this.InnerDictionary.Count; /// /// 破棄されたキャッシュの件数 @@ -92,12 +93,12 @@ public Task DownloadImageAsync(string address, bool force = false) { lock (this.lockObject) { - innerDictionary.TryGetValue(address, out var cachedImageTask); + this.InnerDictionary.TryGetValue(address, out var cachedImageTask); if (cachedImageTask != null) { if (force) - this.innerDictionary.Remove(address); + this.InnerDictionary.Remove(address); else return cachedImageTask; } @@ -105,11 +106,12 @@ public Task DownloadImageAsync(string address, bool force = false) cancelToken.ThrowIfCancellationRequested(); var imageTask = this.FetchImageAsync(address, cancelToken); - this.innerDictionary[address] = imageTask; + this.InnerDictionary[address] = imageTask; return imageTask; } - }, cancelToken); + }, + cancelToken); } private async Task FetchImageAsync(string uri, CancellationToken cancelToken) @@ -130,7 +132,7 @@ private async Task FetchImageAsync(string uri, CancellationToken ca { lock (this.lockObject) { - if (!this.innerDictionary.TryGetValue(address, out var imageTask) || + if (!this.InnerDictionary.TryGetValue(address, out var imageTask) || imageTask.Status != TaskStatus.RanToCompletion) return null; @@ -138,6 +140,22 @@ private async Task FetchImageAsync(string uri, CancellationToken ca } } + public MemoryImage? TryGetLargerOrSameSizeFromCache(string normalUrl, string size) + { + var sizes = new[] { "mini", "normal", "bigger", "original" }; + var minimumIndex = sizes.FindIndex(x => x == size); + + foreach (var candidateSize in sizes.Skip(minimumIndex)) + { + var imageUrl = Twitter.CreateProfileImageUrl(normalUrl, candidateSize); + var image = this.TryGetFromCache(imageUrl); + if (image != null) + return image; + } + + return null; + } + public void CancelAsync() { lock (this.lockObject) @@ -160,13 +178,13 @@ protected virtual void Dispose(bool disposing) lock (this.lockObject) { - foreach (var (_, task) in this.innerDictionary) + foreach (var (_, task) in this.InnerDictionary) { if (task.Status == TaskStatus.RanToCompletion) task.Result?.Dispose(); } - this.innerDictionary.Clear(); + this.InnerDictionary.Clear(); this.cancelTokenSource.Dispose(); } } diff --git a/OpenTween/ImageListViewItem.cs b/OpenTween/ImageListViewItem.cs deleted file mode 100644 index 65cfe65f2..000000000 --- a/OpenTween/ImageListViewItem.cs +++ /dev/null @@ -1,127 +0,0 @@ -// OpenTween - Client of Twitter -// Copyright (c) 2007-2011 kiri_feather (@kiri_feather) -// (c) 2008-2011 Moz (@syo68k) -// (c) 2008-2011 takeshik (@takeshik) -// (c) 2010-2011 anis774 (@anis774) -// (c) 2010-2011 fantasticswallow (@f_swallow) -// (c) 2011 Egtra (@egtra) -// All rights reserved. -// -// This file is part of OpenTween. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see , or write to -// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, -// Boston, MA 02110-1301, USA. - -#nullable enable - -using System; -using System.Net.Http; -using System.Runtime.Serialization; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace OpenTween -{ - [Serializable] - public class ImageListViewItem : ListViewItem - { - protected readonly ImageCache? imageCache; - protected readonly string? imageUrl; - - /// - /// 状態表示に使用するアイコンのインデックスを取得・設定する。 - /// - /// - /// StateImageIndex は不必要な処理が挟まるため、使用しないようにする。 - /// - public int StateIndex { get; set; } - - private readonly WeakReference imageReference = new WeakReference(null); - private Task? imageTask = null; - - public event EventHandler? ImageDownloaded; - - public ImageListViewItem(string[] items) - : this(items, null, null) - { - } - - public ImageListViewItem(string[] items, ImageCache? imageCache, string? imageUrl) - : base(items) - { - this.imageCache = imageCache; - this.imageUrl = imageUrl; - this.StateIndex = -1; - - var image = imageUrl != null ? imageCache?.TryGetFromCache(imageUrl) : null; - - if (image != null) - this.imageReference.Target = image; - } - - protected ImageListViewItem(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - public Task GetImageAsync(bool force = false) - { - if (this.imageTask == null || this.imageTask.IsCompleted) - { - this.imageTask = this.GetImageAsyncInternal(force); - } - - return this.imageTask; - } - - private async Task GetImageAsyncInternal(bool force) - { - if (MyCommon.IsNullOrEmpty(this.imageUrl) || this.imageCache == null) - return; - - if (!force && this.imageReference.Target != null) - return; - - try - { - var image = await this.imageCache.DownloadImageAsync(this.imageUrl, force); - - this.imageReference.Target = image; - - if (this.ListView == null || !this.ListView.Created || this.ListView.IsDisposed) - return; - - if (this.Index < this.ListView.VirtualListSize) - { - this.ListView.RedrawItems(this.Index, this.Index, true); - - this.ImageDownloaded?.Invoke(this, EventArgs.Empty); - } - } - catch (HttpRequestException) { } - catch (InvalidImageException) { } - catch (TaskCanceledException) { } - } - - public MemoryImage Image - => (MemoryImage)this.imageReference.Target; - - public Task RefreshImageAsync() - { - this.imageReference.Target = null; - return this.GetImageAsync(true); - } - } -} diff --git a/OpenTween/IndexedSortedSet.cs b/OpenTween/IndexedSortedSet.cs index 1dea94701..f08ff38f8 100644 --- a/OpenTween/IndexedSortedSet.cs +++ b/OpenTween/IndexedSortedSet.cs @@ -74,7 +74,7 @@ public bool Add(T item) return true; } - var index = this.innerList.BinarySearch(item, comparer); + var index = this.innerList.BinarySearch(item, this.comparer); if (index >= 0) return false; diff --git a/OpenTween/InputDialog.Designer.cs b/OpenTween/InputDialog.Designer.cs index c25e6261f..7f0c27c93 100644 --- a/OpenTween/InputDialog.Designer.cs +++ b/OpenTween/InputDialog.Designer.cs @@ -42,7 +42,7 @@ private void InitializeComponent() resources.ApplyResources(this.buttonOK, "buttonOK"); this.buttonOK.Name = "buttonOK"; this.buttonOK.UseVisualStyleBackColor = true; - this.buttonOK.Click += new System.EventHandler(this.buttonOK_Click); + this.buttonOK.Click += new System.EventHandler(this.ButtonOK_Click); // // buttonCancel // @@ -50,7 +50,7 @@ private void InitializeComponent() this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.buttonCancel.Name = "buttonCancel"; this.buttonCancel.UseVisualStyleBackColor = true; - this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + this.buttonCancel.Click += new System.EventHandler(this.ButtonCancel_Click); // // tableLayoutPanel1 // diff --git a/OpenTween/InputDialog.cs b/OpenTween/InputDialog.cs index 869fb30fc..31b6e6790 100644 --- a/OpenTween/InputDialog.cs +++ b/OpenTween/InputDialog.cs @@ -37,10 +37,10 @@ public partial class InputDialog : OTBaseForm protected InputDialog() => this.InitializeComponent(); - private void buttonOK_Click(object sender, EventArgs e) + private void ButtonOK_Click(object sender, EventArgs e) => this.DialogResult = DialogResult.OK; - private void buttonCancel_Click(object sender, EventArgs e) + private void ButtonCancel_Click(object sender, EventArgs e) => this.DialogResult = DialogResult.Cancel; public static DialogResult Show(string text, out string inputText) diff --git a/OpenTween/InputTabName.cs b/OpenTween/InputTabName.cs index ac8c350f4..acac6db53 100644 --- a/OpenTween/InputTabName.cs +++ b/OpenTween/InputTabName.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -50,7 +50,7 @@ private void OK_Button_Click(object sender, EventArgs e) private void Cancel_Button_Click(object sender, EventArgs e) { - TextTabName.Text = ""; + this.TextTabName.Text = ""; this.DialogResult = DialogResult.Cancel; this.Close(); } @@ -58,7 +58,7 @@ private void Cancel_Button_Click(object sender, EventArgs e) public string TabName { get => this.TextTabName.Text.Trim(); - set => TextTabName.Text = value.Trim(); + set => this.TextTabName.Text = value.Trim(); } public string FormTitle @@ -74,6 +74,7 @@ public string FormDescription } public bool IsShowUsage { get; set; } + public MyCommon.TabUsageType Usage { get; set; } private void InputTabName_Load(object sender, EventArgs e) @@ -88,7 +89,7 @@ private void InputTabName_Load(object sender, EventArgs e) private void InputTabName_Shown(object sender, EventArgs e) { - ActiveControl = TextTabName; + this.ActiveControl = this.TextTabName; if (this.IsShowUsage) { this.LabelUsage.Visible = true; @@ -98,7 +99,7 @@ private void InputTabName_Shown(object sender, EventArgs e) private void ComboUsage_SelectedIndexChanged(object sender, EventArgs e) { - this.Usage = ComboUsage.SelectedIndex switch + this.Usage = this.ComboUsage.SelectedIndex switch { 0 => MyCommon.TabUsageType.UserDefined, 1 => MyCommon.TabUsageType.Lists, diff --git a/OpenTween/WebBrowserController.cs b/OpenTween/InternetSecurityManager.cs similarity index 88% rename from OpenTween/WebBrowserController.cs rename to OpenTween/InternetSecurityManager.cs index 1d66983fd..e5f4bee61 100644 --- a/OpenTween/WebBrowserController.cs +++ b/OpenTween/InternetSecurityManager.cs @@ -6,38 +6,224 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 Egtra (@egtra) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable +#pragma warning disable SA1310 using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Runtime.InteropServices; -using System.Threading; -using System.Text.RegularExpressions; using System.Runtime.InteropServices.ComTypes; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; using System.Windows.Forms; namespace OpenTween { + public class InternetSecurityManager : WebBrowserAPI.IServiceProvider, WebBrowserAPI.IInternetSecurityManager + { + #region "HRESULT" + private enum HRESULT + { + S_OK = 0x0, + S_FALSE = 0x1, + E_NOTIMPL = unchecked((int)0x80004001), + E_NOINTERFACE = unchecked((int)0x80004002), + } + #endregion + + [Flags] + public enum POLICY + { + ALLOW_ACTIVEX = 0x1, + ALLOW_SCRIPT = 0x2, + } + + private readonly object ocx = new(); + private readonly WebBrowserAPI.IServiceProvider ocxServiceProvider; + private readonly IntPtr profferServicePtr = new(); + private readonly WebBrowserAPI.IProfferService profferService = null!; + + public POLICY SecurityPolicy { get; set; } = 0; + + public InternetSecurityManager(WebBrowser webBrowser) + { + // ActiveXコントロール取得 + webBrowser.Url = new Uri("about:blank"); // ActiveXを初期化する + + do + { + Thread.Sleep(100); + Application.DoEvents(); + } + while (webBrowser.ReadyState != WebBrowserReadyState.Complete); + + this.ocx = webBrowser.ActiveXInstance; + + // IServiceProvider.QueryService() を使って IProfferService を取得 + this.ocxServiceProvider = (WebBrowserAPI.IServiceProvider)this.ocx; + + try + { + this.ocxServiceProvider.QueryService( + ref WebBrowserAPI.SID_SProfferService, + ref WebBrowserAPI.IID_IProfferService, + out this.profferServicePtr); + } + catch (SEHException ex) + { + MyCommon.TraceOut(ex, "ocxServiceProvider.QueryService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); + return; + } + catch (ExternalException ex) + { + MyCommon.TraceOut(ex, "ocxServiceProvider.QueryService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); + return; + } + + this.profferService = (WebBrowserAPI.IProfferService)Marshal.GetObjectForIUnknown(this.profferServicePtr); + + // IProfferService.ProfferService() を使って + // 自分を IInternetSecurityManager として提供 + try + { + this.profferService.ProfferService( + ref WebBrowserAPI.IID_IInternetSecurityManager, this, out var cookie); + } + catch (SEHException ex) + { + MyCommon.TraceOut(ex, "IProfferSerive.ProfferService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); + return; + } + catch (ExternalException ex) + { + MyCommon.TraceOut(ex, "IProfferSerive.ProfferService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); + return; + } + } + + int WebBrowserAPI.IServiceProvider.QueryService( + ref Guid guidService, + ref Guid riid, + out IntPtr ppvObject) + { + ppvObject = IntPtr.Zero; + if (guidService.CompareTo( + WebBrowserAPI.IID_IInternetSecurityManager) == 0) + { + // 自分から IID_IInternetSecurityManager を + // QueryInterface して返す + var punk = Marshal.GetIUnknownForObject(this); + return Marshal.QueryInterface(punk, ref riid, out ppvObject); + } + return (int)HRESULT.E_NOINTERFACE; + } + + int WebBrowserAPI.IInternetSecurityManager.GetSecurityId(string pwszUrl, byte[] pbSecurityId, ref uint pcbSecurityId, uint dwReserved) + => WebBrowserAPI.INET_E_DEFAULT_ACTION; + + int WebBrowserAPI.IInternetSecurityManager.GetSecuritySite(out WebBrowserAPI.IInternetSecurityMgrSite? pSite) + { + pSite = null; + return WebBrowserAPI.INET_E_DEFAULT_ACTION; + } + + int WebBrowserAPI.IInternetSecurityManager.GetZoneMappings(int dwZone, ref IEnumString? ppenumstring, int dwFlags) + { + ppenumstring = null; + return WebBrowserAPI.INET_E_DEFAULT_ACTION; + } + + int WebBrowserAPI.IInternetSecurityManager.MapUrlToZone(string pwszUrl, out int pdwZone, int dwFlags) + { + pdwZone = 0; + if (pwszUrl == "about:blank") return WebBrowserAPI.INET_E_DEFAULT_ACTION; + try + { + var urlStr = MyCommon.IDNEncode(pwszUrl); + if (urlStr == null) return WebBrowserAPI.URLPOLICY_DISALLOW; + var url = new Uri(urlStr); + if (url.Scheme == "data") + { + return WebBrowserAPI.URLPOLICY_DISALLOW; + } + } + catch (Exception) + { + return WebBrowserAPI.URLPOLICY_DISALLOW; + } + return WebBrowserAPI.INET_E_DEFAULT_ACTION; + } + + private const byte URLPOLICY_ALLOW = 0; + + int WebBrowserAPI.IInternetSecurityManager.ProcessUrlAction(string pwszUrl, int dwAction, out byte pPolicy, int cbPolicy, byte pContext, int cbContext, int dwFlags, int dwReserved) + { + pPolicy = URLPOLICY_ALLOW; + // スクリプト実行状態かを検査しポリシー設定 + if (WebBrowserAPI.URLACTION_SCRIPT_MIN <= dwAction & + dwAction <= WebBrowserAPI.URLACTION_SCRIPT_MAX) + { + // スクリプト実行状態 + if ((this.SecurityPolicy & POLICY.ALLOW_SCRIPT) == POLICY.ALLOW_SCRIPT) + { + pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; + } + else + { + pPolicy = WebBrowserAPI.URLPOLICY_DISALLOW; + } + if (Regex.IsMatch(pwszUrl, @"^https?://((api\.)?twitter\.com/|([a-zA-Z0-9]+\.)?twimg\.com/)")) pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; + return (int)HRESULT.S_OK; + } + // ActiveX実行状態かを検査しポリシー設定 + if (WebBrowserAPI.URLACTION_ACTIVEX_MIN <= dwAction & + dwAction <= WebBrowserAPI.URLACTION_ACTIVEX_MAX) + { + // ActiveX実行状態 + if ((this.SecurityPolicy & POLICY.ALLOW_ACTIVEX) == POLICY.ALLOW_ACTIVEX) + { + pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; + } + else + { + pPolicy = WebBrowserAPI.URLPOLICY_DISALLOW; + } + return (int)HRESULT.S_OK; + } + // 他のものについてはデフォルト処理 + return WebBrowserAPI.INET_E_DEFAULT_ACTION; + } + + int WebBrowserAPI.IInternetSecurityManager.QueryCustomPolicy(string pwszUrl, ref Guid guidKey, byte ppPolicy, int pcbPolicy, byte pContext, int cbContext, int dwReserved) + => WebBrowserAPI.INET_E_DEFAULT_ACTION; + + int WebBrowserAPI.IInternetSecurityManager.SetSecuritySite(WebBrowserAPI.IInternetSecurityMgrSite pSite) + => WebBrowserAPI.INET_E_DEFAULT_ACTION; + + int WebBrowserAPI.IInternetSecurityManager.SetZoneMapping(int dwZone, string lpszPattern, int dwFlags) + => WebBrowserAPI.INET_E_DEFAULT_ACTION; + } + #region "WebBrowserAPI" internal static class WebBrowserAPI { @@ -83,11 +269,11 @@ public enum URLZONE public static int URLACTION_SCRIPT_MAX = 0x15FF; public static int URLACTION_HTML_MIN = 0x1600; - public static int URLACTION_HTML_SUBMIT_FORMS = 0x1601; // aggregate next two - public static int URLACTION_HTML_SUBMIT_FORMS_FROM = 0x1602; // - public static int URLACTION_HTML_SUBMIT_FORMS_TO = 0x1603; // + public static int URLACTION_HTML_SUBMIT_FORMS = 0x1601; // aggregate next two + public static int URLACTION_HTML_SUBMIT_FORMS_FROM = 0x1602; + public static int URLACTION_HTML_SUBMIT_FORMS_TO = 0x1603; public static int URLACTION_HTML_FONT_DOWNLOAD = 0x1604; - public static int URLACTION_HTML_JAVA_RUN = 0x1605; // derive from Java custom policy + public static int URLACTION_HTML_JAVA_RUN = 0x1605; // derive from Java custom policy public static int URLACTION_HTML_USERDATA_SAVE = 0x1606; public static int URLACTION_HTML_SUBFRAME_NAVIGATE = 0x1607; public static int URLACTION_HTML_META_REFRESH = 0x1608; @@ -117,7 +303,6 @@ public enum URLZONE public static int URLPOLICY_AUTHENTICATE_CHALLENGE_RESPONSE = 0x10000; public static int URLPOLICY_AUTHENTICATE_MUTUAL_ONLY = 0x30000; - public static int URLACTION_COOKIES = 0x1A02; public static int URLACTION_COOKIES_SESSION = 0x1A03; @@ -131,7 +316,6 @@ public enum URLZONE public static int URLACTION_NETWORK_CURR_MAX = 0x1A10; public static int URLACTION_NETWORK_MAX = 0x1BFF; - public static int URLACTION_JAVA_MIN = 0x1C00; public static int URLACTION_JAVA_PERMISSIONS = 0x1C00; public static int URLPOLICY_JAVA_PROHIBIT = 0x0; @@ -142,7 +326,6 @@ public enum URLZONE public static int URLACTION_JAVA_CURR_MAX = 0x1C00; public static int URLACTION_JAVA_MAX = 0x1CFF; - // The following Infodelivery actions should have no default policies // in the registry. They assume that no default policy means fall // back to the global restriction. If an admin sets a policy per @@ -189,28 +372,26 @@ public enum URLZONE public static int URLPOLICY_MASK_PERMISSIONS = 0xF; - public static int URLPOLICY_DONTCHECKDLGBOX = 0x100; - // ---------------------------------------------------------------------- // ここ以下は COM Interface の宣言です。 - public static Guid IID_IProfferService = new Guid("cb728b20-f786-11ce-92ad-00aa00a74cd0"); - public static Guid SID_SProfferService = new Guid("cb728b20-f786-11ce-92ad-00aa00a74cd0"); - public static Guid IID_IInternetSecurityManager = new Guid("79eac9ee-baf9-11ce-8c82-00aa004ba90b"); + public static Guid IID_IProfferService = new("cb728b20-f786-11ce-92ad-00aa00a74cd0"); + public static Guid SID_SProfferService = new("cb728b20-f786-11ce-92ad-00aa00a74cd0"); + public static Guid IID_IInternetSecurityManager = new("79eac9ee-baf9-11ce-8c82-00aa004ba90b"); - [ComImport, - Guid("6d5140c1-7436-11ce-8034-00aa006009fa"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IServiceProvider { [PreserveSig] int QueryService([In] ref Guid guidService, [In] ref Guid riid, out IntPtr ppvObject); } - [ComImport, - Guid("cb728b20-f786-11ce-92ad-00aa00a74cd0"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + [Guid("cb728b20-f786-11ce-92ad-00aa00a74cd0")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IProfferService { [PreserveSig] @@ -220,9 +401,9 @@ public interface IProfferService int RevokeService([In] int cookie); } - [ComImport, - Guid("79eac9ed-baf9-11ce-8c82-00aa004ba90b"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + [Guid("79eac9ed-baf9-11ce-8c82-00aa004ba90b")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IInternetSecurityMgrSite { [PreserveSig] @@ -232,9 +413,9 @@ public interface IInternetSecurityMgrSite int EnableModeless([In, MarshalAs(UnmanagedType.Bool)] bool fEnable); } - [ComImport, - Guid("79eac9ee-baf9-11ce-8c82-00aa004ba90b"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + [Guid("79eac9ee-baf9-11ce-8c82-00aa004ba90b")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IInternetSecurityManager { [PreserveSig] @@ -263,186 +444,4 @@ public interface IInternetSecurityManager } } #endregion - - public class InternetSecurityManager : WebBrowserAPI.IServiceProvider, WebBrowserAPI.IInternetSecurityManager - { - #region "HRESULT" - private enum HRESULT - { - S_OK = 0x0, - S_FALSE = 0x1, - E_NOTIMPL = unchecked((int)0x80004001), - E_NOINTERFACE = unchecked((int)0x80004002), - } - #endregion - - [Flags] public enum POLICY - { - ALLOW_ACTIVEX = 0x1, - ALLOW_SCRIPT = 0x2, - } - - private readonly object ocx = new object(); - private readonly WebBrowserAPI.IServiceProvider ocxServiceProvider; - private readonly IntPtr profferServicePtr = new IntPtr(); - private readonly WebBrowserAPI.IProfferService profferService = null!; - - public POLICY SecurityPolicy { get; set; } = 0; - - public InternetSecurityManager(WebBrowser _WebBrowser) - { - // ActiveXコントロール取得 - _WebBrowser.Url = new Uri("about:blank"); //ActiveXを初期化する - - do - { - Thread.Sleep(100); - Application.DoEvents(); - } while (_WebBrowser.ReadyState != WebBrowserReadyState.Complete); - - ocx = _WebBrowser.ActiveXInstance; - - // IServiceProvider.QueryService() を使って IProfferService を取得 - ocxServiceProvider = (WebBrowserAPI.IServiceProvider)ocx; - - try - { - ocxServiceProvider.QueryService( - ref WebBrowserAPI.SID_SProfferService, - ref WebBrowserAPI.IID_IProfferService, out profferServicePtr); - } - catch (SEHException ex) - { - MyCommon.TraceOut(ex, "ocxServiceProvider.QueryService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); - return; - } - catch (ExternalException ex) - { - MyCommon.TraceOut(ex, "ocxServiceProvider.QueryService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); - return; - } - - - profferService = (WebBrowserAPI.IProfferService)Marshal.GetObjectForIUnknown(profferServicePtr); - - // IProfferService.ProfferService() を使って - // 自分を IInternetSecurityManager として提供 - try - { - profferService.ProfferService( - ref WebBrowserAPI.IID_IInternetSecurityManager, this, out var cookie); - } - catch (SEHException ex) - { - MyCommon.TraceOut(ex, "IProfferSerive.ProfferService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); - return; - } - catch (ExternalException ex) - { - MyCommon.TraceOut(ex, "IProfferSerive.ProfferService() HRESULT:" + ex.ErrorCode.ToString("X8") + Environment.NewLine); - return; - } - } - - int WebBrowserAPI.IServiceProvider.QueryService(ref Guid guidService, - ref Guid riid, out IntPtr ppvObject) - { - - ppvObject = IntPtr.Zero; - if (guidService.CompareTo( - WebBrowserAPI.IID_IInternetSecurityManager) == 0) - { - // 自分から IID_IInternetSecurityManager を - // QueryInterface して返す - var punk = Marshal.GetIUnknownForObject(this); - return Marshal.QueryInterface(punk, ref riid, out ppvObject); - } - return (int)HRESULT.E_NOINTERFACE; - } - - int WebBrowserAPI.IInternetSecurityManager.GetSecurityId(string pwszUrl, byte[] pbSecurityId, ref uint pcbSecurityId, uint dwReserved) - => WebBrowserAPI.INET_E_DEFAULT_ACTION; - - int WebBrowserAPI.IInternetSecurityManager.GetSecuritySite(out WebBrowserAPI.IInternetSecurityMgrSite? pSite) - { - pSite = null; - return WebBrowserAPI.INET_E_DEFAULT_ACTION; - } - - int WebBrowserAPI.IInternetSecurityManager.GetZoneMappings(int dwZone, ref IEnumString? ppenumstring, int dwFlags) - { - ppenumstring = null; - return WebBrowserAPI.INET_E_DEFAULT_ACTION; - } - - int WebBrowserAPI.IInternetSecurityManager.MapUrlToZone(string pwszUrl, out int pdwZone, int dwFlags) - { - pdwZone = 0; - if (pwszUrl == "about:blank") return WebBrowserAPI.INET_E_DEFAULT_ACTION; - try - { - var urlStr = MyCommon.IDNEncode(pwszUrl); - if (urlStr == null) return WebBrowserAPI.URLPOLICY_DISALLOW; - var url = new Uri(urlStr); - if (url.Scheme == "data") - { - return WebBrowserAPI.URLPOLICY_DISALLOW; - } - } - catch (Exception) - { - return WebBrowserAPI.URLPOLICY_DISALLOW; - } - return WebBrowserAPI.INET_E_DEFAULT_ACTION; - } - - const byte URLPOLICY_ALLOW = 0; - - int WebBrowserAPI.IInternetSecurityManager.ProcessUrlAction(string pwszUrl, int dwAction, out byte pPolicy, int cbPolicy, byte pContext, int cbContext, int dwFlags, int dwReserved) - { - pPolicy = URLPOLICY_ALLOW; - //スクリプト実行状態かを検査しポリシー設定 - if (WebBrowserAPI.URLACTION_SCRIPT_MIN <= dwAction & - dwAction <= WebBrowserAPI.URLACTION_SCRIPT_MAX) - { - // スクリプト実行状態 - if ((this.SecurityPolicy & POLICY.ALLOW_SCRIPT) == POLICY.ALLOW_SCRIPT) - { - pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; - } - else - { - pPolicy = WebBrowserAPI.URLPOLICY_DISALLOW; - } - if (Regex.IsMatch(pwszUrl, @"^https?://((api\.)?twitter\.com/|([a-zA-Z0-9]+\.)?twimg\.com/)")) pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; - return (int)HRESULT.S_OK; - } - // ActiveX実行状態かを検査しポリシー設定 - if (WebBrowserAPI.URLACTION_ACTIVEX_MIN <= dwAction & - dwAction <= WebBrowserAPI.URLACTION_ACTIVEX_MAX) - { - // ActiveX実行状態 - if ((this.SecurityPolicy & POLICY.ALLOW_ACTIVEX) == POLICY.ALLOW_ACTIVEX) - { - pPolicy = WebBrowserAPI.URLPOLICY_ALLOW; - } - else - { - pPolicy = WebBrowserAPI.URLPOLICY_DISALLOW; - } - return (int)HRESULT.S_OK; - } - //他のものについてはデフォルト処理 - return WebBrowserAPI.INET_E_DEFAULT_ACTION; - } - - int WebBrowserAPI.IInternetSecurityManager.QueryCustomPolicy(string pwszUrl, ref Guid guidKey, byte ppPolicy, int pcbPolicy, byte pContext, int cbContext, int dwReserved) - => WebBrowserAPI.INET_E_DEFAULT_ACTION; - - int WebBrowserAPI.IInternetSecurityManager.SetSecuritySite(WebBrowserAPI.IInternetSecurityMgrSite pSite) - => WebBrowserAPI.INET_E_DEFAULT_ACTION; - - int WebBrowserAPI.IInternetSecurityManager.SetZoneMapping(int dwZone, string lpszPattern, int dwFlags) - => WebBrowserAPI.INET_E_DEFAULT_ACTION; - } } diff --git a/OpenTween/IsExternalInit.cs b/OpenTween/IsExternalInit.cs new file mode 100644 index 000000000..49c27b35f --- /dev/null +++ b/OpenTween/IsExternalInit.cs @@ -0,0 +1,29 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + internal sealed class IsExternalInit + { + } +} diff --git a/OpenTween/LRUCacheDictionary.cs b/OpenTween/LRUCacheDictionary.cs index 5c8292050..403e52125 100644 --- a/OpenTween/LRUCacheDictionary.cs +++ b/OpenTween/LRUCacheDictionary.cs @@ -22,18 +22,18 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; -using System.Collections; -using System.Diagnostics.CodeAnalysis; namespace OpenTween { /// /// LRU によるキャッシュを行うための辞書クラス /// - class LRUCacheDictionary : IDictionary + public class LRUCacheDictionary : IDictionary { /// /// 保持するアイテムの個数 @@ -58,12 +58,13 @@ public class CacheRemovedEventArgs : EventArgs public CacheRemovedEventArgs(KeyValuePair item) => this.Item = item; } + public event EventHandler? CacheRemoved; - internal LinkedList> innerList; - internal Dictionary>> innerDict; + internal LinkedList> InnerList; + internal Dictionary>> InnerDict; - internal int accessCount = 0; + internal int AccessCount = 0; public LRUCacheDictionary() : this(trimLimit: int.MaxValue, autoTrimCount: int.MaxValue) @@ -75,8 +76,8 @@ public LRUCacheDictionary(int trimLimit, int autoTrimCount) this.TrimLimit = trimLimit; this.AutoTrimCount = autoTrimCount; - this.innerList = new LinkedList>(); - this.innerDict = new Dictionary>>(); + this.InnerList = new LinkedList>(); + this.InnerDict = new Dictionary>>(); } /// @@ -85,8 +86,8 @@ public LRUCacheDictionary(int trimLimit, int autoTrimCount) /// protected void UpdateAccess(LinkedListNode> node) { - this.innerList.Remove(node); - this.innerList.AddFirst(node); + this.InnerList.Remove(node); + this.InnerList.AddFirst(node); } public bool Trim() @@ -95,9 +96,9 @@ public bool Trim() for (var i = this.Count; i > this.TrimLimit; i--) { - var node = this.innerList.Last; - this.innerList.Remove(node); - this.innerDict.Remove(node.Value.Key); + var node = this.InnerList.Last; + this.InnerList.Remove(node); + this.InnerDict.Remove(node.Value.Key); this.CacheRemoved?.Invoke(this, new CacheRemovedEventArgs(node.Value)); } @@ -107,9 +108,9 @@ public bool Trim() internal bool AutoTrim() { - if (this.accessCount < this.AutoTrimCount) return false; + if (this.AccessCount < this.AutoTrimCount) return false; - this.accessCount = 0; // カウンターをリセット + this.AccessCount = 0; // カウンターをリセット return this.Trim(); } @@ -120,47 +121,49 @@ public void Add(TKey key, TValue value) public void Add(KeyValuePair item) { var node = new LinkedListNode>(item); - this.innerList.AddFirst(node); - this.innerDict.Add(item.Key, node); + this.InnerList.AddFirst(node); + this.InnerDict.Add(item.Key, node); - this.accessCount++; + this.AccessCount++; this.AutoTrim(); } public bool ContainsKey(TKey key) - => this.innerDict.ContainsKey(key); + => this.InnerDict.ContainsKey(key); public bool Contains(KeyValuePair item) { - if (!this.innerDict.TryGetValue(item.Key, out var node)) return false; + if (!this.InnerDict.TryGetValue(item.Key, out var node)) return false; return EqualityComparer.Default.Equals(node.Value.Value, item.Value); } public bool Remove(TKey key) { - if (!this.innerDict.TryGetValue(key, out var node)) return false; + if (!this.InnerDict.TryGetValue(key, out var node)) return false; - this.innerList.Remove(node); + this.InnerList.Remove(node); - return this.innerDict.Remove(key); + return this.InnerDict.Remove(key); } public bool Remove(KeyValuePair item) { - if (!this.innerDict.TryGetValue(item.Key, out var node)) return false; + if (!this.InnerDict.TryGetValue(item.Key, out var node)) return false; if (!EqualityComparer.Default.Equals(node.Value.Value, item.Value)) return false; - this.innerList.Remove(node); + this.InnerList.Remove(node); - return this.innerDict.Remove(item.Key); + return this.InnerDict.Remove(item.Key); } +#pragma warning disable CS8767 public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) +#pragma warning restore CS8767 { - var ret = this.innerDict.TryGetValue(key, out var node); + var ret = this.InnerDict.TryGetValue(key, out var node); if (!ret) { @@ -171,56 +174,57 @@ public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) this.UpdateAccess(node); value = node.Value.Value; - this.accessCount++; + this.AccessCount++; this.AutoTrim(); return true; } public ICollection Keys - => this.innerDict.Keys; + => this.InnerDict.Keys; public ICollection Values - => this.innerDict.Values.Select(x => x.Value.Value).ToList(); + => this.InnerDict.Values.Select(x => x.Value.Value).ToList(); public TValue this[TKey key] { get { - var node = this.innerDict[key]; + var node = this.InnerDict[key]; this.UpdateAccess(node); - this.accessCount++; + this.AccessCount++; this.AutoTrim(); return node.Value.Value; } + set { var pair = new KeyValuePair(key, value); - if (this.innerDict.TryGetValue(key, out var node)) + if (this.InnerDict.TryGetValue(key, out var node)) { - this.innerList.Remove(node); + this.InnerList.Remove(node); node.Value = pair; } else { node = new LinkedListNode>(pair); - this.innerDict[key] = node; + this.InnerDict[key] = node; } - this.innerList.AddFirst(node); + this.InnerList.AddFirst(node); - this.accessCount++; + this.AccessCount++; this.AutoTrim(); } } public void Clear() { - this.innerList.Clear(); - this.innerDict.Clear(); + this.InnerList.Clear(); + this.InnerDict.Clear(); } public void CopyTo(KeyValuePair[] array, int arrayIndex) @@ -239,13 +243,13 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) } public int Count - => this.innerDict.Count; + => this.InnerDict.Count; public bool IsReadOnly => false; public IEnumerator> GetEnumerator() - => this.innerDict.Select(x => x.Value.Value).GetEnumerator(); + => this.InnerDict.Select(x => x.Value.Value).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); diff --git a/OpenTween/ListAvailable.cs b/OpenTween/ListAvailable.cs index f894476c5..0a88f0547 100644 --- a/OpenTween/ListAvailable.cs +++ b/OpenTween/ListAvailable.cs @@ -5,19 +5,19 @@ // (c) 2010-2011 anis774 (@anis774) // (c) 2010-2011 fantasticswallow (@f_swallow) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -47,7 +47,8 @@ public ListAvailable() private void OK_Button_Click(object sender, EventArgs e) { - if (this.ListsList.SelectedIndex > -1) { + if (this.ListsList.SelectedIndex > -1) + { this.SelectedList = (ListElement)this.ListsList.SelectedItem; this.DialogResult = System.Windows.Forms.DialogResult.OK; this.Close(); @@ -133,7 +134,9 @@ private async void RefreshButton_Click(object sender, EventArgs e) var lists = await this.FetchListsAsync(); this.UpdateListsListBox(lists); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + } catch (WebApiException ex) { MessageBox.Show("Failed to get lists. (" + ex.Message + ")"); diff --git a/OpenTween/ListElement.cs b/OpenTween/ListElement.cs index 179b17077..b7c9d4da8 100644 --- a/OpenTween/ListElement.cs +++ b/OpenTween/ListElement.cs @@ -40,15 +40,15 @@ public class ListElement public string Description = ""; public string Slug = ""; public bool IsPublic = true; - public int SubscriberCount = 0; //購読者数 - public int MemberCount = 0; //リストメンバ数 + public int SubscriberCount = 0; // 購読者数 + public int MemberCount = 0; // リストメンバ数 public long UserId = 0; public string Username = ""; public string Nickname = ""; - protected Twitter _tw = null!; + protected Twitter tw = null!; - private List _members = new List(); + private List members = new(); [XmlIgnore] public long Cursor { get; private set; } = -1; @@ -61,7 +61,7 @@ public ListElement(TwitterList listElementData, Twitter tw) { this.Description = listElementData.Description; this.Id = listElementData.Id; - this.IsPublic = (listElementData.Mode == "public"); + this.IsPublic = listElementData.Mode == "public"; this.MemberCount = listElementData.MemberCount; this.Name = listElementData.Name; this.SubscriberCount = listElementData.SubscriberCount; @@ -70,12 +70,12 @@ public ListElement(TwitterList listElementData, Twitter tw) this.Username = listElementData.User.ScreenName; this.UserId = listElementData.User.Id; - this._tw = tw; + this.tw = tw; } public virtual async Task Refresh() { - var newList = await _tw.EditList(this.Id, Name, !this.IsPublic, this.Description) + var newList = await this.tw.EditList(this.Id, this.Name, !this.IsPublic, this.Description) .ConfigureAwait(false); this.Description = newList.Description; @@ -92,21 +92,21 @@ public virtual async Task Refresh() [XmlIgnore] public List Members - => this._members; + => this.members; public async Task RefreshMembers() { var users = new List(); - this.Cursor = await this._tw.GetListMembers(this.Id, users, cursor: -1) + this.Cursor = await this.tw.GetListMembers(this.Id, users, cursor: -1) .ConfigureAwait(false); - this._members = users; + this.members = users; } public async Task GetMoreMembers() - => this.Cursor = await this._tw.GetListMembers(this.Id, this._members, this.Cursor) + => this.Cursor = await this.tw.GetListMembers(this.Id, this.members, this.Cursor) .ConfigureAwait(false); public override string ToString() - => $"@{Username}/{Name} [{(this.IsPublic ? "public" : "Protected")}]"; + => $"@{this.Username}/{this.Name} [{(this.IsPublic ? "public" : "Protected")}]"; } } diff --git a/OpenTween/ListManage.cs b/OpenTween/ListManage.cs index 1e3b59ad8..f3723e6b4 100644 --- a/OpenTween/ListManage.cs +++ b/OpenTween/ListManage.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -46,7 +46,7 @@ public partial class ListManage : OTBaseForm public ListManage(Twitter tw) { - InitializeComponent(); + this.InitializeComponent(); this.tw = tw; } @@ -86,7 +86,7 @@ private void ListsList_SelectedIndexChanged(object sender, EventArgs e) { if (this.ListsList.SelectedItem == null) return; - var list = (ListElement) this.ListsList.SelectedItem; + var list = (ListElement)this.ListsList.SelectedItem; this.UsernameTextBox.Text = list.Username; this.NameTextBox.Text = list.Name; this.PublicRadioButton.Checked = list.IsPublic; @@ -170,8 +170,10 @@ private void CancelEditButton_Click(object sender, EventArgs e) this.EditCheckBox.Checked = false; for (var i = this.ListsList.Items.Count - 1; i >= 0; i--) + { if (this.ListsList.Items[i] is NewListElement) this.ListsList.Items.RemoveAt(i); + } this.ListsList_SelectedIndexChanged(this.ListsList, EventArgs.Empty); } @@ -244,10 +246,10 @@ await this.tw.Api.ListsMembersDestroy(list.Id, user.ScreenName) return; } - var idx = ListsList.SelectedIndex; + var idx = this.ListsList.SelectedIndex; list.Members.Remove(user); this.ListsList_SelectedIndexChanged(this.ListsList, EventArgs.Empty); - if (idx < ListsList.Items.Count) ListsList.SelectedIndex = idx; + if (idx < this.ListsList.Items.Count) this.ListsList.SelectedIndex = idx; } } } @@ -299,7 +301,7 @@ private void AddListButton_Click(object sender, EventArgs e) private async void UserList_SelectedIndexChanged(object sender, EventArgs e) { - if (UserList.SelectedItem == null) + if (this.UserList.SelectedItem == null) { this.UserIcon.Image?.Dispose(); this.UserIcon.Image = null; @@ -347,7 +349,8 @@ private async Task LoadUserIconAsync(Uri imageUri, long userId) await this.UserIcon.SetImageFromTask(async () => { - var uri = imageUri.AbsoluteUri.Replace("_normal", "_bigger"); + var sizeName = Twitter.DecideProfileImageSize(this.UserIcon.Width); + var uri = Twitter.CreateProfileImageUrl(imageUri.AbsoluteUri, sizeName); using var imageStream = await Networking.Http.GetStreamAsync(uri); var image = await MemoryImage.CopyFromStreamAsync(imageStream); @@ -373,7 +376,9 @@ private async void RefreshListsButton_Click(object sender, EventArgs e) var lists = await this.FetchListsAsync(); this.UpdateListsListBox(lists); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + } catch (WebApiException ex) { MessageBox.Show(string.Format(Properties.Resources.ListsDeleteFailed, ex.Message)); @@ -417,7 +422,7 @@ private class NewListElement : ListElement public bool IsCreated { get; private set; } = false; public NewListElement(Twitter tw) - => this._tw = tw; + => this.tw = tw; public override async Task Refresh() { @@ -427,7 +432,7 @@ public override async Task Refresh() } else { - await this._tw.CreateListApi(this.Name, !this.IsPublic, this.Description) + await this.tw.CreateListApi(this.Name, !this.IsPublic, this.Description) .ConfigureAwait(false); this.IsCreated = true; @@ -436,7 +441,7 @@ await this._tw.CreateListApi(this.Name, !this.IsPublic, this.Description) public override string ToString() { - if (IsCreated) + if (this.IsCreated) return base.ToString(); else return "NewList"; diff --git a/OpenTween/LoginDialog.Designer.cs b/OpenTween/LoginDialog.Designer.cs index 65cf871f7..9e75f7598 100644 --- a/OpenTween/LoginDialog.Designer.cs +++ b/OpenTween/LoginDialog.Designer.cs @@ -63,7 +63,7 @@ private void InitializeComponent() resources.ApplyResources(this.buttonLogin, "buttonLogin"); this.buttonLogin.Name = "buttonLogin"; this.buttonLogin.UseVisualStyleBackColor = true; - this.buttonLogin.Click += new System.EventHandler(this.buttonLogin_Click); + this.buttonLogin.Click += new System.EventHandler(this.ButtonLogin_Click); // // buttonCancel // diff --git a/OpenTween/LoginDialog.cs b/OpenTween/LoginDialog.cs index e626a6d28..8c4cd4fcb 100644 --- a/OpenTween/LoginDialog.cs +++ b/OpenTween/LoginDialog.cs @@ -36,15 +36,17 @@ namespace OpenTween public partial class LoginDialog : OTBaseForm { public string LoginName => this.textboxLoginName.Text; + public string Password => this.textboxPassword.Text; public Func>? LoginCallback { get; set; } = null; + public bool LoginSuccessed { get; set; } = false; public LoginDialog() => this.InitializeComponent(); - private async void buttonLogin_Click(object sender, EventArgs e) + private async void ButtonLogin_Click(object sender, EventArgs e) { if (this.LoginCallback == null) return; diff --git a/OpenTween/MediaItem.cs b/OpenTween/MediaItem.cs index e243c2144..695bc2ee1 100644 --- a/OpenTween/MediaItem.cs +++ b/OpenTween/MediaItem.cs @@ -1,25 +1,26 @@ // OpenTween - Client of Twitter // Copyright (c) 2015 spx (@5px) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable +#pragma warning disable SA1649 using System; using System.Drawing; @@ -94,6 +95,7 @@ public interface IMediaItem public class FileMediaItem : IMediaItem { public FileInfo FileInfo { get; } + public string? AltText { get; set; } public FileMediaItem(string path) @@ -128,7 +130,9 @@ public bool IsImage try { // MemoryImage が生成できるかを検証する - using (var image = this.CreateImage()) { } + using (var image = this.CreateImage()) + { + } this.isImage = true; } @@ -170,39 +174,40 @@ public void CopyTo(Stream stream) public class MemoryImageMediaItem : IMediaItem, IDisposable { public const string PathPrefix = "<>MemoryImage://"; - private static int _fileNumber = 0; - private readonly MemoryImage _image; + private static int fileNumber = 0; + private readonly MemoryImage image; public bool IsDisposed { get; private set; } = false; public MemoryImageMediaItem(MemoryImage image) { - this._image = image ?? throw new ArgumentNullException(nameof(image)); + this.image = image ?? throw new ArgumentNullException(nameof(image)); - var num = Interlocked.Increment(ref _fileNumber); - this.Path = PathPrefix + num + this._image.ImageFormatExt; + var num = Interlocked.Increment(ref fileNumber); + this.Path = PathPrefix + num + this.image.ImageFormatExt; } public string Path { get; } + public string? AltText { get; set; } public string Name => this.Path.Substring(PathPrefix.Length); public string Extension - => this._image.ImageFormatExt; + => this.image.ImageFormatExt; public bool Exists - => this._image != null; + => this.image != null; public long Size - => this._image.Stream.Length; + => this.image.Stream.Length; public bool IsImage => true; public MemoryImage CreateImage() - => this._image.Clone(); + => this.image.Clone(); public Stream OpenRead() { @@ -212,7 +217,7 @@ public Stream OpenRead() // コピーを作成する memstream = new MemoryStream(); - this._image.Stream.WriteTo(memstream); + this.image.Stream.WriteTo(memstream); memstream.Seek(0, SeekOrigin.Begin); return memstream; @@ -225,7 +230,7 @@ public Stream OpenRead() } public void CopyTo(Stream stream) - => this._image.Stream.WriteTo(stream); + => this.image.Stream.WriteTo(stream); protected virtual void Dispose(bool disposing) { @@ -233,7 +238,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - this._image.Dispose(); + this.image.Dispose(); } this.IsDisposed = true; diff --git a/OpenTween/MediaSelector.cs b/OpenTween/MediaSelector.cs index 7c2224b6e..bffec5451 100644 --- a/OpenTween/MediaSelector.cs +++ b/OpenTween/MediaSelector.cs @@ -1,19 +1,19 @@ // OpenTween - Client of Twitter // Copyright (c) 2014 spx (@5px) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -40,9 +40,11 @@ namespace OpenTween public partial class MediaSelector : UserControl { public event EventHandler? BeginSelecting; + public event EventHandler? EndSelecting; public event EventHandler? FilePickDialogOpening; + public event EventHandler? FilePickDialogClosed; public event EventHandler? SelectedServiceChanged; @@ -103,7 +105,9 @@ public ICollection GetServices() private class SelectedMedia { public IMediaItem? Item { get; set; } + public MyCommon.UploadFileType Type { get; set; } + public string Text { get; set; } public SelectedMedia(IMediaItem? item, MyCommon.UploadFileType type, string text) @@ -130,13 +134,14 @@ public override string ToString() => this.Text; } - private Dictionary pictureService = new Dictionary(); + private Dictionary pictureService = new(); private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig) { this.pictureService?.Clear(); - this.pictureService = new Dictionary { + this.pictureService = new Dictionary + { ["Twitter"] = new TwitterPhoto(tw, twitterConfig), ["Imgur"] = new Imgur(twitterConfig), ["Mobypicture"] = new Mobypicture(tw, twitterConfig), @@ -145,7 +150,7 @@ private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig) public MediaSelector() { - InitializeComponent(); + this.InitializeComponent(); this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage; } @@ -155,12 +160,12 @@ public MediaSelector() /// public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null) { - CreateServices(tw, twitterConfig); + this.CreateServices(tw, twitterConfig); - SetImageServiceCombo(); - SetImagePageCombo(); + this.SetImageServiceCombo(); + this.SetImagePageCombo(); - SelectImageServiceComboItem(svc, index); + this.SelectImageServiceComboItem(svc, index); } /// @@ -168,9 +173,9 @@ public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string sv /// public void Reset(Twitter tw, TwitterConfiguration twitterConfig) { - CreateServices(tw, twitterConfig); + this.CreateServices(tw, twitterConfig); - SetImageServiceCombo(); + this.SetImageServiceCombo(); } /// @@ -182,12 +187,12 @@ public bool HasUploadableService(string fileName, bool ignoreSize) var ext = fl.Extension; var size = ignoreSize ? (long?)null : fl.Length; - if (IsUploadable(this.ServiceName, ext, size)) + if (this.IsUploadable(this.ServiceName, ext, size)) return true; - foreach (string svc in ImageServiceCombo.Items) + foreach (string svc in this.ImageServiceCombo.Items) { - if (IsUploadable(svc, ext, size)) + if (this.IsUploadable(svc, ext, size)) return true; } @@ -222,7 +227,7 @@ private void BeginSelection(IMediaItem[] items) { if (items == null || items.Length == 0) { - BeginSelection(); + this.BeginSelection(); return; } @@ -233,7 +238,7 @@ private void BeginSelection(IMediaItem[] items) if (!this.Visible || count > 1) { // 非表示時または複数のファイル指定は新規選択として扱う - SetImagePageCombo(); + this.SetImagePageCombo(); this.BeginSelecting?.Invoke(this, EventArgs.Empty); @@ -243,21 +248,21 @@ private void BeginSelection(IMediaItem[] items) if (count == 1) { - ImagefilePathText.Text = items[0].Path; - AlternativeTextBox.Text = items[0].AltText; - ImageFromSelectedFile(items[0], false); + this.ImagefilePathText.Text = items[0].Path; + this.AlternativeTextBox.Text = items[0].AltText; + this.ImageFromSelectedFile(items[0], false); } else { for (var i = 0; i < count; i++) { - var index = ImagePageCombo.Items.Count - 1; + var index = this.ImagePageCombo.Items.Count - 1; if (index == 0) { - ImagefilePathText.Text = items[i].Path; - AlternativeTextBox.Text = items[i].AltText; + this.ImagefilePathText.Text = items[i].Path; + this.AlternativeTextBox.Text = items[i].AltText; } - ImageFromSelectedFile(index, items[i], false); + this.ImageFromSelectedFile(index, items[i], false); } } } @@ -269,12 +274,12 @@ public void BeginSelection(string[] fileNames) { if (fileNames == null || fileNames.Length == 0) { - BeginSelection(); + this.BeginSelection(); return; } - var items = fileNames.Select(x => CreateFileMediaItem(x, false)).OfType().ToArray(); - BeginSelection(items); + var items = fileNames.Select(x => this.CreateFileMediaItem(x, false)).OfType().ToArray(); + this.BeginSelection(items); } /// @@ -284,12 +289,12 @@ public void BeginSelection(Image image) { if (image == null) { - BeginSelection(); + this.BeginSelection(); return; } - var items = new [] { CreateMemoryImageMediaItem(image, false) }.OfType().ToArray(); - BeginSelection(items); + var items = new[] { this.CreateMemoryImageMediaItem(image, false) }.OfType().ToArray(); + this.BeginSelection(items); } /// @@ -304,9 +309,9 @@ public void BeginSelection() this.Visible = true; this.Enabled = true; - var media = (SelectedMedia)ImagePageCombo.SelectedItem; - ImageFromSelectedFile(media.Item, true); - ImagefilePathText.Focus(); + var media = (SelectedMedia)this.ImagePageCombo.SelectedItem; + this.ImageFromSelectedFile(media.Item, true); + this.ImagefilePathText.Focus(); } } @@ -317,15 +322,15 @@ public void EndSelection() { if (this.Visible) { - ImagefilePathText.CausesValidation = false; + this.ImagefilePathText.CausesValidation = false; this.EndSelecting?.Invoke(this, EventArgs.Empty); this.Visible = false; this.Enabled = false; - ClearImageSelectedPicture(); + this.ClearImageSelectedPicture(); - ImagefilePathText.CausesValidation = true; + this.ImagefilePathText.CausesValidation = true; } } @@ -334,11 +339,11 @@ public void EndSelection() /// public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems) { - var validItems = ImagePageCombo.Items.Cast() + var validItems = this.ImagePageCombo.Items.Cast() .Where(x => x.IsValid).Select(x => x.Item).OfType().ToArray(); if (validItems.Length > 0 && - ImageServiceCombo.SelectedIndex > -1) + this.ImageServiceCombo.SelectedIndex > -1) { var serviceName = this.ServiceName; if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validItems.Length), @@ -348,16 +353,16 @@ public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [N MessageBoxDefaultButton.Button1) == DialogResult.OK) { - //収集した MediaItem が破棄されないように、予め null を代入しておく - foreach (SelectedMedia media in ImagePageCombo.Items) + // 収集した MediaItem が破棄されないように、予め null を代入しておく + foreach (SelectedMedia media in this.ImagePageCombo.Items) { if (media != null) media.Item = null; } imageService = serviceName; mediaItems = validItems; - EndSelection(); - SetImagePageCombo(); + this.EndSelection(); + this.SetImagePageCombo(); return true; } } @@ -409,23 +414,23 @@ public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [N private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox) { - var media = (SelectedMedia)ImagePageCombo.SelectedItem; + var media = (SelectedMedia)this.ImagePageCombo.SelectedItem; var item = media.Item; if (path != media.Path) { - DisposeMediaItem(media.Item); + this.DisposeMediaItem(media.Item); media.Item = null; - item = CreateFileMediaItem(path, noMsgBox); + item = this.CreateFileMediaItem(path, noMsgBox); } if (item != null) item.AltText = altText; - ImagefilePathText.Text = path; - AlternativeTextBox.Text = altText; - ImageFromSelectedFile(item, noMsgBox); + this.ImagefilePathText.Text = path; + this.AlternativeTextBox.Text = altText; + this.ImageFromSelectedFile(item, noMsgBox); } private void DisposeMediaItem(IMediaItem? item) @@ -438,34 +443,34 @@ private void FilePickButton_Click(object sender, EventArgs e) { var service = this.SelectedService; - if (FilePickDialog == null || service == null) return; - FilePickDialog.Filter = service.SupportedFormatsStrForDialog; - FilePickDialog.Title = Properties.Resources.PickPictureDialog1; - FilePickDialog.FileName = ""; + if (this.FilePickDialog == null || service == null) return; + this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog; + this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1; + this.FilePickDialog.FileName = ""; this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty); try { - if (FilePickDialog.ShowDialog() == DialogResult.Cancel) return; + if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return; } finally { this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty); } - ValidateNewFileMediaItem(FilePickDialog.FileName, AlternativeTextBox.Text.Trim(), false); + this.ValidateNewFileMediaItem(this.FilePickDialog.FileName, this.AlternativeTextBox.Text.Trim(), false); } private void ImagefilePathText_Validating(object sender, CancelEventArgs e) { - if (ImageCancelButton.Focused) + if (this.ImageCancelButton.Focused) { - ImagefilePathText.CausesValidation = false; + this.ImagefilePathText.CausesValidation = false; return; } - ValidateNewFileMediaItem(ImagefilePathText.Text.Trim(), AlternativeTextBox.Text.Trim(), false); + this.ValidateNewFileMediaItem(this.ImagefilePathText.Text.Trim(), this.AlternativeTextBox.Text.Trim(), false); } private void ImageFromSelectedFile(IMediaItem? item, bool noMsgBox) @@ -480,13 +485,13 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) var imageService = this.SelectedService; if (imageService == null) return; - var selectedIndex = ImagePageCombo.SelectedIndex; + var selectedIndex = this.ImagePageCombo.SelectedIndex; if (index < 0) index = selectedIndex; - if (index >= ImagePageCombo.Items.Count) + if (index >= this.ImagePageCombo.Items.Count) throw new ArgumentOutOfRangeException(nameof(index)); - var isSelectedPage = (index == selectedIndex); + var isSelectedPage = index == selectedIndex; if (isSelectedPage) this.ClearImageSelectedPicture(); @@ -500,11 +505,11 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) if (!imageService.CheckFileExtension(ext)) { - //画像以外の形式 + // 画像以外の形式 if (!noMsgBox) { MessageBox.Show( - string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, MakeAvailableServiceText(ext, size), ext, item.Name), + string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, this.MakeAvailableServiceText(ext, size), ext, item.Name), Properties.Resources.PostPictureWarn4, MessageBoxButtons.OK, MessageBoxIcon.Warning); @@ -518,7 +523,7 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) if (!noMsgBox) { MessageBox.Show( - string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, MakeAvailableServiceText(ext, size), item.Name), + string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, this.MakeAvailableServiceText(ext, size), item.Name), Properties.Resources.PostPictureWarn4, MessageBoxButtons.OK, MessageBoxIcon.Warning); @@ -529,15 +534,15 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) if (item.IsImage) { if (isSelectedPage) - ImageSelectedPicture.Image = item.CreateImage(); - SetImagePage(index, item, MyCommon.UploadFileType.Picture); + this.ImageSelectedPicture.Image = item.CreateImage(); + this.SetImagePage(index, item, MyCommon.UploadFileType.Picture); } else { - SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia); + this.SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia); } - valid = true; //正常終了 + valid = true; // 正常終了 } catch (FileNotFoundException) { @@ -552,8 +557,8 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) { if (!valid) { - ClearImagePage(index); - DisposeMediaItem(item); + this.ClearImagePage(index); + this.DisposeMediaItem(item); } } } @@ -561,7 +566,7 @@ private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox) private string MakeAvailableServiceText(string ext, long fileSize) { var text = string.Join(", ", - ImageServiceCombo.Items.Cast() + this.ImageServiceCombo.Items.Cast() .Where(serviceName => !MyCommon.IsNullOrEmpty(serviceName) && this.pictureService[serviceName].CheckFileExtension(ext) && @@ -589,7 +594,7 @@ private void ImageSelection_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Escape) { - EndSelection(); + this.EndSelection(); } } @@ -597,7 +602,7 @@ private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e) { if (Convert.ToInt32(e.KeyChar) == 0x1B) { - ImagefilePathText.CausesValidation = false; + this.ImagefilePathText.CausesValidation = false; e.Handled = true; } } @@ -606,25 +611,25 @@ private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArg { if (e.KeyCode == Keys.Escape) { - ImagefilePathText.CausesValidation = false; + this.ImagefilePathText.CausesValidation = false; } } private void SetImageServiceCombo() { - using (ControlTransaction.Update(ImageServiceCombo)) + using (ControlTransaction.Update(this.ImageServiceCombo)) { var svc = ""; - if (ImageServiceCombo.SelectedIndex > -1) svc = ImageServiceCombo.Text; - ImageServiceCombo.Items.Clear(); + if (this.ImageServiceCombo.SelectedIndex > -1) svc = this.ImageServiceCombo.Text; + this.ImageServiceCombo.Items.Clear(); // Add service names to combobox - foreach (var key in pictureService.Keys) + foreach (var key in this.pictureService.Keys) { - ImageServiceCombo.Items.Add(key); + this.ImageServiceCombo.Items.Add(key); } - SelectImageServiceComboItem(svc); + this.SelectImageServiceComboItem(svc); } } @@ -637,7 +642,7 @@ private void SelectImageServiceComboItem(string svc, int? index = null) } else { - idx = ImageServiceCombo.Items.IndexOf(svc); + idx = this.ImageServiceCombo.Items.IndexOf(svc); // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる // (廃止されたサービスを選択していた場合の対応) @@ -646,11 +651,11 @@ private void SelectImageServiceComboItem(string svc, int? index = null) try { - ImageServiceCombo.SelectedIndex = idx; + this.ImageServiceCombo.SelectedIndex = idx; } catch (ArgumentOutOfRangeException) { - ImageServiceCombo.SelectedIndex = 0; + this.ImageServiceCombo.SelectedIndex = 0; } this.UpdateAltTextPanelVisible(); @@ -672,29 +677,29 @@ private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e) { this.UpdateAltTextPanelVisible(); - if (ImagePageCombo.Items.Count > 0) + if (this.ImagePageCombo.Items.Count > 0) { // 画像が選択された投稿先に対応しているかをチェックする // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい if (this.ServiceName == "Twitter") { - ValidateSelectedImagePage(); + this.ValidateSelectedImagePage(); } else { - if (ImagePageCombo.Items.Count > 1) + if (this.ImagePageCombo.Items.Count > 1) { // 複数の選択済み画像のうち、1枚目のみを残す - SetImagePageCombo((SelectedMedia)ImagePageCombo.Items[0]); + this.SetImagePageCombo((SelectedMedia)this.ImagePageCombo.Items[0]); } else { - ImagePageCombo.Enabled = false; + this.ImagePageCombo.Enabled = false; var valid = false; try { - var item = ((SelectedMedia)ImagePageCombo.Items[0]).Item; + var item = ((SelectedMedia)this.ImagePageCombo.Items[0]).Item; if (item != null) { var ext = item.Extension; @@ -712,8 +717,8 @@ private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e) { if (!valid) { - ClearImageSelectedPicture(); - ClearSelectedImagePage(); + this.ClearImageSelectedPicture(); + this.ClearSelectedImagePage(); } } } @@ -727,25 +732,25 @@ private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e) private void SetImagePageCombo(SelectedMedia? media = null) { - using (ControlTransaction.Update(ImagePageCombo)) + using (ControlTransaction.Update(this.ImagePageCombo)) { - ImagePageCombo.Enabled = false; + this.ImagePageCombo.Enabled = false; - foreach (SelectedMedia oldMedia in ImagePageCombo.Items) + foreach (SelectedMedia oldMedia in this.ImagePageCombo.Items) { if (oldMedia == null || oldMedia == media) continue; - DisposeMediaItem(oldMedia.Item); + this.DisposeMediaItem(oldMedia.Item); } - ImagePageCombo.Items.Clear(); + this.ImagePageCombo.Items.Clear(); if (media == null) media = new SelectedMedia("1"); - ImagePageCombo.Items.Add(media); - ImagefilePathText.Text = media.Path; - AlternativeTextBox.Text = media.AltText; + this.ImagePageCombo.Items.Add(media); + this.ImagefilePathText.Text = media.Path; + this.AlternativeTextBox.Text = media.AltText; - ImagePageCombo.SelectedIndex = 0; + this.ImagePageCombo.SelectedIndex = 0; } } @@ -757,12 +762,12 @@ private void AddNewImagePage(int selectedIndex) if (selectedIndex < service.MaxMediaCount - 1) { // 投稿先の投稿可能枚数まで選択できるようにする - var count = ImagePageCombo.Items.Count; + var count = this.ImagePageCombo.Items.Count; if (selectedIndex == count - 1) { count++; - ImagePageCombo.Items.Add(new SelectedMedia(count.ToString())); - ImagePageCombo.Enabled = true; + this.ImagePageCombo.Items.Add(new SelectedMedia(count.ToString())); + this.ImagePageCombo.Enabled = true; } } } @@ -772,18 +777,18 @@ private void SetSelectedImagePage(IMediaItem item, MyCommon.UploadFileType type) private void SetImagePage(int index, IMediaItem item, MyCommon.UploadFileType type) { - var selectedIndex = ImagePageCombo.SelectedIndex; + var selectedIndex = this.ImagePageCombo.SelectedIndex; if (index < 0) index = selectedIndex; - var media = (SelectedMedia)ImagePageCombo.Items[index]; + var media = (SelectedMedia)this.ImagePageCombo.Items[index]; if (media.Item != item) { - DisposeMediaItem(media.Item); + this.DisposeMediaItem(media.Item); media.Item = item; } media.Type = type; - AddNewImagePage(index); + this.AddNewImagePage(index); } private void ClearSelectedImagePage() @@ -791,29 +796,29 @@ private void ClearSelectedImagePage() private void ClearImagePage(int index) { - var selectedIndex = ImagePageCombo.SelectedIndex; + var selectedIndex = this.ImagePageCombo.SelectedIndex; if (index < 0) index = selectedIndex; - var media = (SelectedMedia)ImagePageCombo.Items[index]; - DisposeMediaItem(media.Item); + var media = (SelectedMedia)this.ImagePageCombo.Items[index]; + this.DisposeMediaItem(media.Item); media.Item = null; media.Type = MyCommon.UploadFileType.Invalid; if (index == selectedIndex) { - ImagefilePathText.Text = ""; - AlternativeTextBox.Text = ""; + this.ImagefilePathText.Text = ""; + this.AlternativeTextBox.Text = ""; } } private void ValidateSelectedImagePage() { - var idx = ImagePageCombo.SelectedIndex; - var media = (SelectedMedia)ImagePageCombo.Items[idx]; - ImageServiceCombo.Enabled = (idx == 0); // idx == 0 以外では投稿先サービスを選べないようにする - ImagefilePathText.Text = media.Path; - AlternativeTextBox.Text = media.AltText; - ImageFromSelectedFile(media.Item, true); + var idx = this.ImagePageCombo.SelectedIndex; + var media = (SelectedMedia)this.ImagePageCombo.Items[idx]; + this.ImageServiceCombo.Enabled = idx == 0; // idx == 0 以外では投稿先サービスを選べないようにする + this.ImagefilePathText.Text = media.Path; + this.AlternativeTextBox.Text = media.AltText; + this.ImageFromSelectedFile(media.Item, true); } private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e) diff --git a/OpenTween/MediaUploadServices/Imgur.cs b/OpenTween/MediaUploadServices/Imgur.cs index fe0402a9c..3530e2b87 100644 --- a/OpenTween/MediaUploadServices/Imgur.cs +++ b/OpenTween/MediaUploadServices/Imgur.cs @@ -33,9 +33,9 @@ namespace OpenTween.MediaUploadServices { public class Imgur : IMediaUploadService { - private readonly static long MaxFileSize = 10L * 1024 * 1024; + private static readonly long MaxFileSize = 10L * 1024 * 1024; - private readonly static IEnumerable SupportedExtensions = new[] + private static readonly IEnumerable SupportedExtensions = new[] { ".jpg", ".jpeg", diff --git a/OpenTween/MediaUploadServices/TwitterPhoto.cs b/OpenTween/MediaUploadServices/TwitterPhoto.cs index 5356f7be8..4929d84c0 100644 --- a/OpenTween/MediaUploadServices/TwitterPhoto.cs +++ b/OpenTween/MediaUploadServices/TwitterPhoto.cs @@ -153,7 +153,7 @@ await this.tw.Api.MediaMetadataCreate(mediaId, media.AltText) using var origImage = mediaItem.CreateImage(); - if (SettingManager.Common.AlphaPNGWorkaround && this.AddAlphaChannelIfNeeded(origImage.Image, out var newImage)) + if (SettingManager.Instance.Common.AlphaPNGWorkaround && this.AddAlphaChannelIfNeeded(origImage.Image, out var newImage)) { using var newMediaItem = new MemoryImageMediaItem(newImage!); newMediaItem.AltText = mediaItem.AltText; diff --git a/OpenTween/MemoryImage.cs b/OpenTween/MemoryImage.cs index 0be699048..06d9a5df1 100644 --- a/OpenTween/MemoryImage.cs +++ b/OpenTween/MemoryImage.cs @@ -23,15 +23,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.IO; using System.Threading.Tasks; -using System.Drawing.Imaging; namespace OpenTween { @@ -44,41 +44,65 @@ namespace OpenTween /// public class MemoryImage : ICloneable, IDisposable, IEquatable { + private readonly byte[] buffer; + private readonly int bufferOffset; + private readonly int bufferCount; private readonly Image image; + private static readonly Dictionary ExtensionByFormat = new() + { + { ImageFormat.Bmp, ".bmp" }, + { ImageFormat.Emf, ".emf" }, + { ImageFormat.Gif, ".gif" }, + { ImageFormat.Icon, ".ico" }, + { ImageFormat.Jpeg, ".jpg" }, + { ImageFormat.MemoryBmp, ".bmp" }, + { ImageFormat.Png, ".png" }, + { ImageFormat.Tiff, ".tiff" }, + { ImageFormat.Wmf, ".wmf" }, + }; + /// /// ストリームから読みだされる画像データが不正な場合にスローされる /// - protected MemoryImage(MemoryStream stream) + protected MemoryImage(byte[] buffer, int offset, int count) { try { - this.image = Image.FromStream(stream); + this.buffer = buffer; + this.bufferOffset = offset; + this.bufferCount = count; + + this.Stream = new(buffer, offset, count, writable: false); + this.image = this.CreateImage(this.Stream); + } + catch + { + this.Stream?.Dispose(); + throw; + } + } + + private Image CreateImage(Stream stream) + { + try + { + return Image.FromStream(stream); } catch (ArgumentException e) { - stream.Dispose(); throw new InvalidImageException("Invalid image", e); } catch (OutOfMemoryException e) { // GDI+ がサポートしない画像形式で OutOfMemoryException がスローされる場合があるらしい - stream.Dispose(); throw new InvalidImageException("Invalid image?", e); } catch (ExternalException e) { // 「GDI+ で汎用エラーが発生しました」という大雑把な例外がスローされる場合があるらしい - stream.Dispose(); throw new InvalidImageException("Invalid image?", e); } - catch (Exception) - { - stream.Dispose(); - throw; - } - - this.Stream = stream; } /// @@ -115,71 +139,19 @@ public ImageFormat ImageFormat /// MemoryImage が保持している画像のフォーマットに相当する拡張子 (ピリオド付き) /// public string ImageFormatExt - { - get - { - var format = this.ImageFormat; - - // ImageFormat は == で正しく比較できないため Equals を使用する必要がある - if (format.Equals(ImageFormat.Bmp)) - return ".bmp"; - if (format.Equals(ImageFormat.Emf)) - return ".emf"; - if (format.Equals(ImageFormat.Gif)) - return ".gif"; - if (format.Equals(ImageFormat.Icon)) - return ".ico"; - if (format.Equals(ImageFormat.Jpeg)) - return ".jpg"; - if (format.Equals(ImageFormat.MemoryBmp)) - return ".bmp"; - if (format.Equals(ImageFormat.Png)) - return ".png"; - if (format.Equals(ImageFormat.Tiff)) - return ".tiff"; - if (format.Equals(ImageFormat.Wmf)) - return ".wmf"; - - // 対応する形式がなければ空文字列を返す - // (上記以外のフォーマットは Image.FromStream を通過できないため、ここが実行されることはまず無い) - return string.Empty; - } - } + => MemoryImage.ExtensionByFormat.TryGetValue(this.ImageFormat, out var ext) ? ext : ""; /// /// MemoryImage インスタンスを複製します /// - /// - /// メソッド実行中にストリームのシークが行われないよう注意して下さい。 - /// 特に PictureBox で Gif アニメーションを表示している場合は Enabled に false をセットするなどして更新を止めて下さい。 - /// /// 複製された MemoryImage public MemoryImage Clone() - { - this.Stream.Seek(0, SeekOrigin.Begin); - - return MemoryImage.CopyFromStream(this.Stream); - } - - /// - /// MemoryImage インスタンスを非同期に複製します - /// - /// - /// メソッド実行中にストリームのシークが行われないよう注意して下さい。 - /// 特に PictureBox で Gif アニメーションを表示している場合は Enabled に false をセットするなどして更新を止めて下さい。 - /// - /// 複製された MemoryImage を返すタスク - public Task CloneAsync() - { - this.Stream.Seek(0, SeekOrigin.Begin); - - return MemoryImage.CopyFromStreamAsync(this.Stream); - } + => new(this.buffer, this.bufferOffset, this.bufferCount); public override int GetHashCode() { using var sha1service = new System.Security.Cryptography.SHA1CryptoServiceProvider(); - var hash = sha1service.ComputeHash(this.Stream.GetBuffer(), 0, (int)this.Stream.Length); + var hash = sha1service.ComputeHash(this.buffer, this.bufferOffset, this.bufferCount); return Convert.ToBase64String(hash).GetHashCode(); } @@ -195,23 +167,13 @@ public bool Equals(MemoryImage? other) return false; // それぞれが保持する MemoryStream の内容が等しいことを検証する + var selfBuffer = new ArraySegment(this.buffer, this.bufferOffset, this.bufferCount); + var otherBuffer = new ArraySegment(other.buffer, other.bufferOffset, other.bufferCount); - var selfLength = this.Stream.Length; - var otherLength = other.Stream.Length; - - if (selfLength != otherLength) + if (selfBuffer.Count != otherBuffer.Count) return false; - var selfBuffer = this.Stream.GetBuffer(); - var otherBuffer = other.Stream.GetBuffer(); - - for (var pos = 0L; pos < selfLength; pos++) - { - if (selfBuffer[pos] != otherBuffer[pos]) - return false; - } - - return true; + return selfBuffer.Zip(otherBuffer, (x, y) => x == y).All(x => x); } object ICloneable.Clone() @@ -253,20 +215,11 @@ public void Dispose() /// 不正な画像データが入力された場合 public static MemoryImage CopyFromStream(Stream stream) { - MemoryStream? memstream = null; - try - { - memstream = new MemoryStream(); + using var memstream = new MemoryStream(); - stream.CopyTo(memstream); + stream.CopyTo(memstream); - return new MemoryImage(memstream); - } - catch - { - memstream?.Dispose(); - throw; - } + return new(memstream.GetBuffer(), 0, (int)memstream.Length); } /// @@ -279,22 +232,14 @@ public static MemoryImage CopyFromStream(Stream stream) /// 読み込む対象となる Stream /// 作成された MemoryImage を返すタスク /// 不正な画像データが入力された場合 - public async static Task CopyFromStreamAsync(Stream stream) + public static async Task CopyFromStreamAsync(Stream stream, int capacity = 0) { - MemoryStream? memstream = null; - try - { - memstream = new MemoryStream(); + using var memstream = new MemoryStream(capacity); - await stream.CopyToAsync(memstream).ConfigureAwait(false); + await stream.CopyToAsync(memstream) + .ConfigureAwait(false); - return new MemoryImage(memstream); - } - catch - { - memstream?.Dispose(); - throw; - } + return new(memstream.GetBuffer(), 0, (int)memstream.Length); } /// @@ -304,19 +249,7 @@ public async static Task CopyFromStreamAsync(Stream stream) /// 作成された MemoryImage /// 不正な画像データが入力された場合 public static MemoryImage CopyFromBytes(byte[] bytes) - { - MemoryStream? memstream = null; - try - { - memstream = new MemoryStream(bytes); - return new MemoryImage(memstream); - } - catch - { - memstream?.Dispose(); - throw; - } - } + => new(bytes, 0, bytes.Length); /// /// Image インスタンスから MemoryImage を作成します @@ -328,20 +261,11 @@ public static MemoryImage CopyFromBytes(byte[] bytes) /// 作成された MemoryImage public static MemoryImage CopyFromImage(Image image) { - MemoryStream? memstream = null; - try - { - memstream = new MemoryStream(); + using var memstream = new MemoryStream(); - image.Save(memstream, ImageFormat.Png); + image.Save(memstream, ImageFormat.Png); - return new MemoryImage(memstream); - } - catch - { - memstream?.Dispose(); - throw; - } + return new(memstream.GetBuffer(), 0, (int)memstream.Length); } } @@ -351,9 +275,23 @@ public static MemoryImage CopyFromImage(Image image) [Serializable] public class InvalidImageException : Exception { - public InvalidImageException() { } - public InvalidImageException(string message) : base(message) { } - public InvalidImageException(string message, Exception innerException) : base(message, innerException) { } - protected InvalidImageException(SerializationInfo info, StreamingContext context) : base(info, context) { } + public InvalidImageException() + { + } + + public InvalidImageException(string message) + : base(message) + { + } + + public InvalidImageException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected InvalidImageException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/OpenTween/Models/DirectMessagesTabModel.cs b/OpenTween/Models/DirectMessagesTabModel.cs index 601dc4977..d3a957394 100644 --- a/OpenTween/Models/DirectMessagesTabModel.cs +++ b/OpenTween/Models/DirectMessagesTabModel.cs @@ -41,21 +41,23 @@ public class DirectMessagesTabModel : InternalStorageTabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.DirectMessage; - public DirectMessagesTabModel() : this(MyCommon.DEFAULTTAB.DM) + public DirectMessagesTabModel() + : this(MyCommon.DEFAULTTAB.DM) { } - public DirectMessagesTabModel(string tabName) : base(tabName) + public DirectMessagesTabModel(string tabName) + : base(tabName) { } public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) { bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText8, backward ? -1 : 1)); diff --git a/OpenTween/Models/FavoritesTabModel.cs b/OpenTween/Models/FavoritesTabModel.cs index 4afa68e68..40d348fa5 100644 --- a/OpenTween/Models/FavoritesTabModel.cs +++ b/OpenTween/Models/FavoritesTabModel.cs @@ -41,21 +41,23 @@ public class FavoritesTabModel : TabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.Favorites; - public FavoritesTabModel() : this(MyCommon.DEFAULTTAB.FAV) + public FavoritesTabModel() + : this(MyCommon.DEFAULTTAB.FAV) { } - public FavoritesTabModel(string tabName) : base(tabName) + public FavoritesTabModel(string tabName) + : base(tabName) { } public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) { bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText19); diff --git a/OpenTween/Models/FilterTabModel.cs b/OpenTween/Models/FilterTabModel.cs index 6bad336ca..a6c32b8a0 100644 --- a/OpenTween/Models/FilterTabModel.cs +++ b/OpenTween/Models/FilterTabModel.cs @@ -43,14 +43,15 @@ public override MyCommon.TabUsageType TabType public bool FilterModified { get; set; } - private readonly List _filters = new List(); - private readonly object lockObjFilters = new object(); + private readonly List filters = new(); + private readonly object lockObjFilters = new(); - public FilterTabModel(string tabName) : base(tabName) + public FilterTabModel(string tabName) + : base(tabName) { } - //フィルタに合致したら追加 + // フィルタに合致したら追加 public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false) { if (this.IsInnerStorageTabType) @@ -58,14 +59,14 @@ public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false) var rslt = MyCommon.HITRESULT.None; - //全フィルタ評価(優先順位あり) + // 全フィルタ評価(優先順位あり) lock (this.lockObjFilters) { - foreach (var ft in _filters) + foreach (var ft in this.filters) { try { - switch (ft.ExecFilter(post)) //フィルタクラスでヒット判定 + switch (ft.ExecFilter(post)) // フィルタクラスでヒット判定 { case MyCommon.HITRESULT.None: break; @@ -103,15 +104,14 @@ public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false) this.AddPostQueue(post); } - return rslt; //マーク付けは呼び出し元で行うこと + return rslt; // マーク付けは呼び出し元で行うこと } - public PostFilterRule[] GetFilters() { lock (this.lockObjFilters) { - return _filters.ToArray(); + return this.filters.ToArray(); } } @@ -119,7 +119,7 @@ public void RemoveFilter(PostFilterRule filter) { lock (this.lockObjFilters) { - _filters.Remove(filter); + this.filters.Remove(filter); filter.PropertyChanged -= this.OnFilterModified; this.FilterModified = true; } @@ -129,9 +129,9 @@ public bool AddFilter(PostFilterRule filter) { lock (this.lockObjFilters) { - if (_filters.Contains(filter)) return false; + if (this.filters.Contains(filter)) return false; filter.PropertyChanged += this.OnFilterModified; - _filters.Add(filter); + this.filters.Add(filter); this.FilterModified = true; return true; } @@ -146,24 +146,25 @@ public PostFilterRule[] FilterArray { lock (this.lockObjFilters) { - return _filters.ToArray(); + return this.filters.ToArray(); } } + set { lock (this.lockObjFilters) { - foreach (var oldFilter in this._filters) + foreach (var oldFilter in this.filters) { oldFilter.PropertyChanged -= this.OnFilterModified; } - this._filters.Clear(); + this.filters.Clear(); this.FilterModified = true; foreach (var newFilter in value) { - _filters.Add(newFilter); + this.filters.Add(newFilter); newFilter.PropertyChanged += this.OnFilterModified; } } diff --git a/OpenTween/Models/HomeTabModel.cs b/OpenTween/Models/HomeTabModel.cs index 284e97ca4..4bbc880f0 100644 --- a/OpenTween/Models/HomeTabModel.cs +++ b/OpenTween/Models/HomeTabModel.cs @@ -47,13 +47,15 @@ public override MyCommon.TabUsageType TabType // 流速計測用 private int tweetsPerHour = 0; - private readonly ConcurrentDictionary tweetsTimestamps = new ConcurrentDictionary(); + private readonly ConcurrentDictionary tweetsTimestamps = new(); - public HomeTabModel() : this(MyCommon.DEFAULTTAB.RECENT) + public HomeTabModel() + : this(MyCommon.DEFAULTTAB.RECENT) { } - public HomeTabModel(string tabName) : base(tabName) + public HomeTabModel(string tabName) + : base(tabName) { } @@ -66,10 +68,10 @@ public override void AddPostQueue(PostClass post) public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) { bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText5, backward ? -1 : 1)); @@ -77,7 +79,7 @@ await tw.GetHomeTimelineApi(read, this, backward, startup) .ConfigureAwait(false); // 新着時未読クリア - if (SettingManager.Common.ReadOldPosts) + if (SettingManager.Instance.Common.ReadOldPosts) TabInformations.GetInstance().SetReadHomeTab(); TabInformations.GetInstance().DistributePosts(); diff --git a/OpenTween/Models/InternalStorageTabModel.cs b/OpenTween/Models/InternalStorageTabModel.cs index 0e39cd5b3..81ee24140 100644 --- a/OpenTween/Models/InternalStorageTabModel.cs +++ b/OpenTween/Models/InternalStorageTabModel.cs @@ -38,12 +38,13 @@ namespace OpenTween.Models { public abstract class InternalStorageTabModel : TabModel { - protected readonly ConcurrentDictionary internalPosts = new ConcurrentDictionary(); + protected readonly ConcurrentDictionary internalPosts = new(); public override ConcurrentDictionary Posts => this.internalPosts; - protected InternalStorageTabModel(string tabName) : base(tabName) + protected InternalStorageTabModel(string tabName) + : base(tabName) { } diff --git a/OpenTween/Models/ListTimelineTabModel.cs b/OpenTween/Models/ListTimelineTabModel.cs index 6769bc73f..fcdae9ace 100644 --- a/OpenTween/Models/ListTimelineTabModel.cs +++ b/OpenTween/Models/ListTimelineTabModel.cs @@ -55,10 +55,10 @@ public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, return; bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report("List refreshing..."); diff --git a/OpenTween/Models/LocalSearchTabModel.cs b/OpenTween/Models/LocalSearchTabModel.cs index 4773cfeeb..4776c1015 100644 --- a/OpenTween/Models/LocalSearchTabModel.cs +++ b/OpenTween/Models/LocalSearchTabModel.cs @@ -36,7 +36,8 @@ public override MyCommon.TabUsageType TabType public override bool IsPermanentTabType => false; - public LocalSearchTabModel(string tabName) : base(tabName) + public LocalSearchTabModel(string tabName) + : base(tabName) { } diff --git a/OpenTween/Models/MediaInfo.cs b/OpenTween/Models/MediaInfo.cs index 2390e15d3..43c60b042 100644 --- a/OpenTween/Models/MediaInfo.cs +++ b/OpenTween/Models/MediaInfo.cs @@ -32,7 +32,9 @@ namespace OpenTween.Models public class MediaInfo { public string Url { get; } + public string? AltText { get; } + public string? VideoUrl { get; } public MediaInfo(string url) diff --git a/OpenTween/Models/MentionsTabModel.cs b/OpenTween/Models/MentionsTabModel.cs index 2d38b60ae..508fb6c01 100644 --- a/OpenTween/Models/MentionsTabModel.cs +++ b/OpenTween/Models/MentionsTabModel.cs @@ -41,21 +41,23 @@ public class MentionsTabModel : FilterTabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.Mentions; - public MentionsTabModel() : this(MyCommon.DEFAULTTAB.REPLY) + public MentionsTabModel() + : this(MyCommon.DEFAULTTAB.REPLY) { } - public MentionsTabModel(string tabName) : base(tabName) + public MentionsTabModel(string tabName) + : base(tabName) { } public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) { bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText4, backward ? -1 : 1)); diff --git a/OpenTween/Models/MuteTabModel.cs b/OpenTween/Models/MuteTabModel.cs index adc1c83ed..9af031b84 100644 --- a/OpenTween/Models/MuteTabModel.cs +++ b/OpenTween/Models/MuteTabModel.cs @@ -34,11 +34,13 @@ public class MuteTabModel : FilterTabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.Mute; - public MuteTabModel() : this(MyCommon.DEFAULTTAB.MUTE) + public MuteTabModel() + : this(MyCommon.DEFAULTTAB.MUTE) { } - public MuteTabModel(string tabName) : base(tabName) + public MuteTabModel(string tabName) + : base(tabName) { } diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 1e21938c4..e52e333f7 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -40,86 +40,93 @@ namespace OpenTween.Models { public class PostClass : ICloneable { - public readonly struct StatusGeo : IEquatable - { - public double Longitude { get; } - public double Latitude { get; } - - public StatusGeo(double longitude, double latitude) - { - this.Longitude = longitude; - this.Latitude = latitude; - } - - public override int GetHashCode() - => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode(); - - public override bool Equals(object obj) - => obj is StatusGeo && this.Equals((StatusGeo)obj); - - public bool Equals(StatusGeo other) - => this.Longitude == other.Longitude && this.Latitude == other.Longitude; - - public static bool operator ==(StatusGeo left, StatusGeo right) - => left.Equals(right); - - public static bool operator !=(StatusGeo left, StatusGeo right) - => !left.Equals(right); - } + public readonly record struct StatusGeo( + double Longitude, + double Latitude + ); public string Nickname { get; set; } = ""; + public string TextFromApi { get; set; } = ""; /// スクリーンリーダーでの読み上げを考慮したテキスト public string AccessibleText { get; set; } = ""; public string ImageUrl { get; set; } = ""; + public string ScreenName { get; set; } = ""; + public DateTimeUtc CreatedAt { get; set; } + public long StatusId { get; set; } - private bool _IsFav; + + private bool isFav; public string Text { get { if (this.expandComplatedAll) - return this._text; + return this.text; - var expandedHtml = this.ReplaceToExpandedUrl(this._text, out this.expandComplatedAll); + var expandedHtml = this.ReplaceToExpandedUrl(this.text, out this.expandComplatedAll); if (this.expandComplatedAll) - this._text = expandedHtml; + this.text = expandedHtml; return expandedHtml; } - set => this._text = value; + set => this.text = value; } - private string _text = ""; + + private string text = ""; public bool IsRead { get; set; } + public bool IsReply { get; set; } + public bool IsExcludeReply { get; set; } - private bool _IsProtect; + + private bool isProtect; + public bool IsOwl { get; set; } - private bool _IsMark; + + private bool isMark; + public string? InReplyToUser { get; set; } - private long? _InReplyToStatusId; + + private long? inReplyToStatusId; + public string Source { get; set; } = ""; + public Uri? SourceUri { get; set; } + public List<(long UserId, string ScreenName)> ReplyToList { get; set; } + public bool IsMe { get; set; } + public bool IsDm { get; set; } + public long UserId { get; set; } + public bool FilterHit { get; set; } + public string? RetweetedBy { get; set; } + public long? RetweetedId { get; set; } - private bool _IsDeleted = false; - private StatusGeo? _postGeo = null; + + private bool isDeleted = false; + private StatusGeo? postGeo = null; + public int RetweetedCount { get; set; } + public long? RetweetedByUserId { get; set; } + public long? InReplyToUserId { get; set; } + public List Media { get; set; } + public long[] QuoteStatusIds { get; set; } + public ExpandedUrlInfo[] ExpandedUrls { get; set; } /// @@ -127,6 +134,8 @@ public string Text /// public class ExpandedUrlInfo : ICloneable { + public static bool AutoExpand { get; set; } = true; + /// 展開前の t.co ドメインの URL public string Url { get; } @@ -134,7 +143,7 @@ public class ExpandedUrlInfo : ICloneable /// /// による展開が完了するまでは Entity に含まれる expanded_url の値を返します /// - public string ExpandedUrl => this._expandedUrl; + public string ExpandedUrl => this.expandedUrl; /// による展開を行うタスク public Task ExpandTask { get; private set; } @@ -142,7 +151,7 @@ public class ExpandedUrlInfo : ICloneable /// による展開が完了したか否か public bool ExpandedCompleted => this.ExpandTask.IsCompleted; - protected string _expandedUrl; + protected string expandedUrl; public ExpandedUrlInfo(string url, string expandedUrl) : this(url, expandedUrl, deepExpand: true) @@ -152,9 +161,9 @@ public ExpandedUrlInfo(string url, string expandedUrl) public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) { this.Url = url; - this._expandedUrl = expandedUrl; + this.expandedUrl = expandedUrl; - if (deepExpand) + if (AutoExpand && deepExpand) this.ExpandTask = this.DeepExpandAsync(); else this.ExpandTask = Task.CompletedTask; @@ -162,15 +171,15 @@ public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) protected virtual async Task DeepExpandAsync() { - var origUrl = this._expandedUrl; + var origUrl = this.expandedUrl; var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl) .ConfigureAwait(false); - Interlocked.CompareExchange(ref this._expandedUrl, newUrl, origUrl); + Interlocked.CompareExchange(ref this.expandedUrl, newUrl, origUrl); } public ExpandedUrlInfo Clone() - => new ExpandedUrlInfo(this.Url, this.ExpandedUrl, deepExpand: false); + => new(this.Url, this.ExpandedUrl, deepExpand: false); object ICloneable.Clone() => this.Clone(); @@ -178,7 +187,7 @@ object ICloneable.Clone() public int FavoritedCount { get; set; } - private States _states = States.None; + private States states = States.None; private bool expandComplatedAll = false; [Flags] @@ -193,10 +202,10 @@ private enum States public PostClass() { - Media = new List(); - ReplyToList = new List<(long, string)>(); - QuoteStatusIds = Array.Empty(); - ExpandedUrls = Array.Empty(); + this.Media = new List(); + this.ReplyToList = new List<(long, string)>(); + this.QuoteStatusIds = Array.Empty(); + this.ExpandedUrls = Array.Empty(); } public string TextSingleLine @@ -215,11 +224,12 @@ public bool IsFav } } - return _IsFav; + return this.isFav; } + set { - _IsFav = value; + this.isFav = value; if (this.RetweetedId != null) { var post = this.RetweetSource; @@ -233,47 +243,49 @@ public bool IsFav public bool IsProtect { - get => this._IsProtect; + get => this.isProtect; set { if (value) - _states |= States.Protect; + this.states |= States.Protect; else - _states &= ~States.Protect; + this.states &= ~States.Protect; - _IsProtect = value; + this.isProtect = value; } } + public bool IsMark { - get => this._IsMark; + get => this.isMark; set { if (value) - _states |= States.Mark; + this.states |= States.Mark; else - _states &= ~States.Mark; + this.states &= ~States.Mark; - _IsMark = value; + this.isMark = value; } } + public long? InReplyToStatusId { - get => this._InReplyToStatusId; + get => this.inReplyToStatusId; set { if (value != null) - _states |= States.Reply; + this.states |= States.Reply; else - _states &= ~States.Reply; + this.states &= ~States.Reply; - _InReplyToStatusId = value; + this.inReplyToStatusId = value; } } public bool IsDeleted { - get => this._IsDeleted; + get => this.isDeleted; set { if (value) @@ -283,9 +295,9 @@ public bool IsDeleted this.InReplyToUserId = null; this.IsReply = false; this.ReplyToList = new List<(long, string)>(); - this._states = States.None; + this.states = States.None; } - _IsDeleted = value; + this.isDeleted = value; } } @@ -294,23 +306,23 @@ protected virtual PostClass? RetweetSource public StatusGeo? PostGeo { - get => this._postGeo; + get => this.postGeo; set { if (value != null) { - _states |= States.Geo; + this.states |= States.Geo; } else { - _states &= ~States.Geo; + this.states &= ~States.Geo; } - _postGeo = value; + this.postGeo = value; } } public int StateIndex - => (int)_states - 1; + => (int)this.states - 1; // 互換性のために用意 public string SourceHtml @@ -320,8 +332,10 @@ public string SourceHtml if (this.SourceUri == null) return WebUtility.HtmlEncode(this.Source); - return string.Format("{1}", - WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source)); + return string.Format( + "{1}", + WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), + WebUtility.HtmlEncode(this.Source)); } } @@ -463,7 +477,7 @@ public bool Equals(PostClass? other) (this.InReplyToStatusId == other.InReplyToStatusId) && (this.Source == other.Source) && (this.SourceUri == other.SourceUri) && - (this.ReplyToList.SequenceEqual(other.ReplyToList)) && + this.ReplyToList.SequenceEqual(other.ReplyToList) && (this.IsMe == other.IsMe) && (this.IsDm == other.IsDm) && (this.UserId == other.UserId) && @@ -472,7 +486,6 @@ public bool Equals(PostClass? other) (this.RetweetedId == other.RetweetedId) && (this.IsDeleted == other.IsDeleted) && (this.InReplyToUserId == other.InReplyToUserId); - } public override int GetHashCode() diff --git a/OpenTween/Models/PostFilterRule.cs b/OpenTween/Models/PostFilterRule.cs index 2a1df9997..ad6e52bad 100644 --- a/OpenTween/Models/PostFilterRule.cs +++ b/OpenTween/Models/PostFilterRule.cs @@ -1,19 +1,19 @@ // OpenTween - Client of Twitter // Copyright (c) 2013 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -49,7 +49,7 @@ public class PostFilterRule : NotifyPropertyChangedBase, IEquatable [XmlIgnore] - protected Func? FilterDelegate; + protected Func? filterDelegate; /// /// 振り分けルールの概要 @@ -68,164 +68,185 @@ public string SummaryText public bool Enabled { - get => this._enabled; - set => this.SetProperty(ref this._enabled, value); + get => this.enabled; + set => this.SetProperty(ref this.enabled, value); } - private bool _enabled; + + private bool enabled; [XmlElement("NameFilter")] public string? FilterName { - get => this._FilterName; - set => this.SetProperty(ref this._FilterName, value); + get => this.filterName; + set => this.SetProperty(ref this.filterName, value); } - private string? _FilterName; + + private string? filterName; [XmlElement("ExNameFilter")] public string? ExFilterName { - get => this._ExFilterName; - set => this.SetProperty(ref this._ExFilterName, value); + get => this.exFilterName; + set => this.SetProperty(ref this.exFilterName, value); } - private string? _ExFilterName; + + private string? exFilterName; [XmlArray("BodyFilterArray")] public string[] FilterBody { - get => this._FilterBody; - set => this.SetProperty(ref this._FilterBody, value ?? throw new ArgumentNullException(nameof(value))); + get => this.filterBody; + set => this.SetProperty(ref this.filterBody, value ?? throw new ArgumentNullException(nameof(value))); } - private string[] _FilterBody = Array.Empty(); + + private string[] filterBody = Array.Empty(); [XmlArray("ExBodyFilterArray")] public string[] ExFilterBody { - get => this._ExFilterBody; - set => this.SetProperty(ref this._ExFilterBody, value ?? throw new ArgumentNullException(nameof(value))); + get => this.exFilterBody; + set => this.SetProperty(ref this.exFilterBody, value ?? throw new ArgumentNullException(nameof(value))); } - private string[] _ExFilterBody = Array.Empty(); + + private string[] exFilterBody = Array.Empty(); [XmlElement("SearchBoth")] public bool UseNameField { - get => this._UseNameField; - set => this.SetProperty(ref this._UseNameField, value); + get => this.useNameField; + set => this.SetProperty(ref this.useNameField, value); } - private bool _UseNameField; + + private bool useNameField; [XmlElement("ExSearchBoth")] public bool ExUseNameField { - get => this._ExUseNameField; - set => this.SetProperty(ref this._ExUseNameField, value); + get => this.exUseNameField; + set => this.SetProperty(ref this.exUseNameField, value); } - private bool _ExUseNameField; + + private bool exUseNameField; [XmlElement("MoveFrom")] public bool MoveMatches { - get => this._MoveMatches; - set => this.SetProperty(ref this._MoveMatches, value); + get => this.moveMatches; + set => this.SetProperty(ref this.moveMatches, value); } - private bool _MoveMatches; + + private bool moveMatches; [XmlElement("SetMark")] public bool MarkMatches { - get => this._MarkMatches; - set => this.SetProperty(ref this._MarkMatches, value); + get => this.markMatches; + set => this.SetProperty(ref this.markMatches, value); } - private bool _MarkMatches; + + private bool markMatches; [XmlElement("SearchUrl")] public bool FilterByUrl { - get => this._FilterByUrl; - set => this.SetProperty(ref this._FilterByUrl, value); + get => this.filterByUrl; + set => this.SetProperty(ref this.filterByUrl, value); } - private bool _FilterByUrl; + + private bool filterByUrl; [XmlElement("ExSearchUrl")] public bool ExFilterByUrl { - get => this._ExFilterByUrl; - set => this.SetProperty(ref this._ExFilterByUrl, value); + get => this.exFilterByUrl; + set => this.SetProperty(ref this.exFilterByUrl, value); } - private bool _ExFilterByUrl; + + private bool exFilterByUrl; public bool CaseSensitive { - get => this._CaseSensitive; - set => this.SetProperty(ref this._CaseSensitive, value); + get => this.caseSensitive; + set => this.SetProperty(ref this.caseSensitive, value); } - private bool _CaseSensitive; + + private bool caseSensitive; public bool ExCaseSensitive { - get => this._ExCaseSensitive; - set => this.SetProperty(ref this._ExCaseSensitive, value); + get => this.exCaseSensitive; + set => this.SetProperty(ref this.exCaseSensitive, value); } - private bool _ExCaseSensitive; + + private bool exCaseSensitive; public bool UseLambda { - get => this._UseLambda; - set => this.SetProperty(ref this._UseLambda, value); + get => this.useLambda; + set => this.SetProperty(ref this.useLambda, value); } - private bool _UseLambda; + + private bool useLambda; public bool ExUseLambda { - get => this._ExUseLambda; - set => this.SetProperty(ref this._ExUseLambda, value); + get => this.exUseLambda; + set => this.SetProperty(ref this.exUseLambda, value); } - private bool _ExUseLambda; + + private bool exUseLambda; public bool UseRegex { - get => this._UseRegex; - set => this.SetProperty(ref this._UseRegex, value); + get => this.useRegex; + set => this.SetProperty(ref this.useRegex, value); } - private bool _UseRegex; + + private bool useRegex; public bool ExUseRegex { - get => this._ExUseRegex; - set => this.SetProperty(ref this._ExUseRegex, value); + get => this.exUseRegex; + set => this.SetProperty(ref this.exUseRegex, value); } - private bool _ExUseRegex; + + private bool exUseRegex; [XmlElement("IsRt")] public bool FilterRt { - get => this._FilterRt; - set => this.SetProperty(ref this._FilterRt, value); + get => this.filterRt; + set => this.SetProperty(ref this.filterRt, value); } - private bool _FilterRt; + + private bool filterRt; [XmlElement("IsExRt")] public bool ExFilterRt { - get => this._ExFilterRt; - set => this.SetProperty(ref this._ExFilterRt, value); + get => this.exFilterRt; + set => this.SetProperty(ref this.exFilterRt, value); } - private bool _ExFilterRt; + + private bool exFilterRt; [XmlElement("Source")] public string? FilterSource { - get => this._FilterSource; - set => this.SetProperty(ref this._FilterSource, value); + get => this.filterSource; + set => this.SetProperty(ref this.filterSource, value); } - private string? _FilterSource; + + private string? filterSource; [XmlElement("ExSource")] public string? ExFilterSource { - get => this._ExFilterSource; - set => this.SetProperty(ref this._ExFilterSource, value); + get => this.exFilterSource; + set => this.SetProperty(ref this.exFilterSource, value); } - private string? _ExFilterSource; + + private string? exFilterSource; public PostFilterRule() { @@ -248,7 +269,7 @@ public void Compile() { if (!this.Enabled) { - this.FilterDelegate = x => MyCommon.HITRESULT.None; + this.filterDelegate = x => MyCommon.HITRESULT.None; this.IsDirty = false; return; } @@ -257,13 +278,27 @@ public void Compile() var matchExpr = this.MakeFiltersExpr( postParam, - this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt, - this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl); + this.FilterName, + this.FilterBody, + this.FilterSource, + this.FilterRt, + this.UseRegex, + this.CaseSensitive, + this.UseNameField, + this.UseLambda, + this.FilterByUrl); var excludeExpr = this.MakeFiltersExpr( postParam, - this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt, - this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl); + this.ExFilterName, + this.ExFilterBody, + this.ExFilterSource, + this.ExFilterRt, + this.ExUseRegex, + this.ExCaseSensitive, + this.ExUseNameField, + this.ExUseLambda, + this.ExFilterByUrl); Expression> filterExpr; @@ -320,14 +355,21 @@ public void Compile() filterExpr = x => MyCommon.HITRESULT.None; } - this.FilterDelegate = filterExpr.Compile(); + this.filterDelegate = filterExpr.Compile(); this.IsDirty = false; } protected virtual Expression? MakeFiltersExpr( ParameterExpression postParam, - string? filterName, string[] filterBody, string? filterSource, bool filterRt, - bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl) + string? filterName, + string[] filterBody, + string? filterSource, + bool filterRt, + bool useRegex, + bool caseSensitive, + bool useNameField, + bool useLambda, + bool filterByUrl) { var filterExprs = new List(); @@ -413,8 +455,12 @@ public void Compile() } protected Expression MakeGenericFilter( - ParameterExpression postParam, string targetFieldName, string pattern, - bool useRegex, bool caseSensitive, bool exactMatch = false) + ParameterExpression postParam, + string targetFieldName, + string pattern, + bool useRegex, + bool caseSensitive, + bool exactMatch = false) { // x. var targetField = Expression.Property( @@ -447,7 +493,6 @@ protected Expression MakeGenericFilter( typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }), targetValue, Expression.Constant(compOpt)); - } else { @@ -479,7 +524,7 @@ public MyCommon.HITRESULT ExecFilter(PostClass post) this.Compile(); } - return this.FilterDelegate!(post); + return this.filterDelegate!(post); } public PostFilterRule Clone() @@ -602,7 +647,7 @@ protected virtual string MakeSummary() } if (this.HasExcludeConditions()) { - //除外 + // 除外 fs.Append(Properties.Resources.SetFiltersText12); if (this.ExUseNameField) { diff --git a/OpenTween/Models/PublicSearchTabModel.cs b/OpenTween/Models/PublicSearchTabModel.cs index e4c5dadbc..43c6ade10 100644 --- a/OpenTween/Models/PublicSearchTabModel.cs +++ b/OpenTween/Models/PublicSearchTabModel.cs @@ -43,28 +43,29 @@ public override MyCommon.TabUsageType TabType public string SearchWords { - get => this._searchWords; + get => this.searchWords; set { - this._searchWords = value; + this.searchWords = value; this.ResetFetchIds(); } } public string SearchLang { - get => this._searchLang; + get => this.searchLang; set { - this._searchLang = value; + this.searchLang = value; this.ResetFetchIds(); } } - private string _searchWords = ""; - private string _searchLang = ""; + private string searchWords = ""; + private string searchLang = ""; - public PublicSearchTabModel(string tabName) : base(tabName) + public PublicSearchTabModel(string tabName) + : base(tabName) { } @@ -74,10 +75,10 @@ public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, return; bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report("Search refreshing..."); diff --git a/OpenTween/Models/RelatedPostsTabModel.cs b/OpenTween/Models/RelatedPostsTabModel.cs index be635dc19..9f46cd5bd 100644 --- a/OpenTween/Models/RelatedPostsTabModel.cs +++ b/OpenTween/Models/RelatedPostsTabModel.cs @@ -54,13 +54,13 @@ public RelatedPostsTabModel(string tabName, PostClass targetPost) public Task RefreshAsync(Twitter tw, bool startup, IProgress progress) => this.RefreshAsync(tw, false, startup, progress); - public override async Task RefreshAsync(Twitter tw, bool _, bool startup, IProgress progress) + public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress) { bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; try { diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index ca99569a3..1eac80f61 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -41,30 +41,31 @@ namespace OpenTween.Models { public sealed class TabInformations { - //個別タブの情報をDictionaryで保持 + // 個別タブの情報をDictionaryで保持 public IReadOnlyTabCollection Tabs => this.tabs; - public MuteTabModel MuteTab { get; private set; } = new MuteTabModel(); + public MuteTabModel MuteTab { get; private set; } = new(); - public ConcurrentDictionary Posts { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary Posts { get; } = new(); - private readonly Dictionary _quotes = new Dictionary(); - private readonly ConcurrentDictionary retweetsCount = new ConcurrentDictionary(); + private readonly Dictionary quotes = new(); + private readonly ConcurrentDictionary retweetsCount = new(); - public Stack RemovedTab { get; } = new Stack(); + public Stack RemovedTab { get; } = new(); public ISet BlockIds { get; set; } = new HashSet(); + public ISet MuteUserIds { get; set; } = new HashSet(); - //発言の追加 - //AddPost(複数回) -> DistributePosts -> SubmitUpdate + // 発言の追加 + // AddPost(複数回) -> DistributePosts -> SubmitUpdate - private readonly TabCollection tabs = new TabCollection(); - private readonly ConcurrentQueue addQueue = new ConcurrentQueue(); + private readonly TabCollection tabs = new(); + private readonly ConcurrentQueue addQueue = new(); /// 通知サウンドを再生する優先順位 - private readonly Dictionary notifyPriorityByTabType = new Dictionary + private readonly Dictionary notifyPriorityByTabType = new() { [MyCommon.TabUsageType.DirectMessage] = 100, [MyCommon.TabUsageType.Mentions] = 90, @@ -73,20 +74,20 @@ public IReadOnlyTabCollection Tabs [MyCommon.TabUsageType.Favorites] = 60, }; - //トランザクション用 - private readonly object LockObj = new object(); + // トランザクション用 + private readonly object lockObj = new(); - private static readonly TabInformations _instance = new TabInformations(); + private static readonly TabInformations Instance = new(); - //List - private List _lists = new List(); + // List + private List lists = new(); - private TabInformations() + internal TabInformations() { } public static TabInformations GetInstance() - => _instance; // singleton + => Instance; // singleton public string SelectedTabName { get; private set; } = ""; @@ -98,7 +99,7 @@ public int SelectedTabIndex public List SubscribableLists { - get => this._lists; + get => this.lists; set { if (value.Count > 0) @@ -115,13 +116,13 @@ public List SubscribableLists } } } - _lists = value; + this.lists = value; } } public bool AddTab(TabModel tab) { - lock (this.LockObj) + lock (this.lockObj) { if (tab is MuteTabModel muteTab) { @@ -139,12 +140,12 @@ public bool AddTab(TabModel tab) } } - public void RemoveTab(string TabName) + public void RemoveTab(string tabName) { - lock (LockObj) + lock (this.lockObj) { - var tb = GetTabByName(TabName); - if (tb == null || tb.IsDefaultTabType) return; //念のため + var tb = this.GetTabByName(tabName); + if (tb == null || tb.IsDefaultTabType) return; // 念のため if (!tb.IsInnerStorageTabType) { @@ -154,24 +155,24 @@ public void RemoveTab(string TabName) for (var idx = 0; idx < tb.AllCount; ++idx) { var exist = false; - var Id = tb.GetStatusIdAt(idx); - if (Id < 0) continue; + var id = tb.GetStatusIdAt(idx); + if (id < 0) continue; foreach (var tab in this.Tabs) { if (tab != tb && tab != dmTab) { - if (tab.Contains(Id)) + if (tab.Contains(id)) { exist = true; break; } } } - if (!exist) homeTab.AddPostImmediately(Id, this.Posts[Id].IsRead); + if (!exist) homeTab.AddPostImmediately(id, this.Posts[id].IsRead); } } this.RemovedTab.Push(tb); - this.tabs.Remove(TabName); + this.tabs.Remove(tabName); } } @@ -194,8 +195,8 @@ public void MoveTab(int newIndex, TabModel tab) this.tabs.Insert(newIndex, tab); } - public bool ContainsTab(string TabText) - => this.Tabs.Contains(TabText); + public bool ContainsTab(string tabText) + => this.Tabs.Contains(tabText); public bool ContainsTab(TabModel ts) => this.Tabs.Contains(ts); @@ -208,6 +209,69 @@ public void SelectTab(string tabName) this.SelectedTabName = tabName; } + public void LoadTabsFromSettings(SettingTabs settingTabs) + { + foreach (var tabSetting in settingTabs.Tabs) + { + var tab = this.CreateTabFromSettings(tabSetting); + if (tab == null) + continue; + + if (this.ContainsTab(tab.TabName)) + tab.TabName = this.MakeTabName("MyTab"); + + this.AddTab(tab); + } + } + + public TabModel? CreateTabFromSettings(SettingTabs.SettingTabItem tabSetting) + { + var tabName = tabSetting.TabName; + + TabModel? tab = tabSetting.TabType switch + { + MyCommon.TabUsageType.Home + => new HomeTabModel(tabName), + MyCommon.TabUsageType.Mentions + => new MentionsTabModel(tabName), + MyCommon.TabUsageType.DirectMessage + => new DirectMessagesTabModel(tabName), + MyCommon.TabUsageType.Favorites + => new FavoritesTabModel(tabName), + MyCommon.TabUsageType.UserDefined + => new FilterTabModel(tabName), + MyCommon.TabUsageType.UserTimeline + => new UserTimelineTabModel(tabName, tabSetting.User!), + MyCommon.TabUsageType.PublicSearch + => new PublicSearchTabModel(tabName) + { + SearchWords = tabSetting.SearchWords, + SearchLang = tabSetting.SearchLang, + }, + MyCommon.TabUsageType.Lists + => new ListTimelineTabModel(tabName, tabSetting.ListInfo!), + MyCommon.TabUsageType.Mute + => new MuteTabModel(tabName), + _ => null, + }; + + if (tab == null) + return null; + + tab.UnreadManage = tabSetting.UnreadManage; + tab.Protected = tabSetting.Protected; + tab.Notify = tabSetting.Notify; + tab.SoundFile = tabSetting.SoundFile; + + if (tab is FilterTabModel filterTab) + { + filterTab.FilterArray = tabSetting.FilterArray; + filterTab.FilterModified = false; + } + + return tab; + } + /// /// デフォルトのタブを追加する /// @@ -306,8 +370,8 @@ public SortOrder ToggleSortOrder(ComparerMode sortMode) return this.SortOrder; } - public PostClass? RetweetSource(long Id) - => this.Posts.TryGetValue(Id, out var status) ? status : null; + public PostClass? RetweetSource(long id) + => this.Posts.TryGetValue(id, out var status) ? status : null; public void RemovePostFromAllTabs(long statusId, bool setIsDeleted) { @@ -326,11 +390,14 @@ public void RemovePostFromAllTabs(long statusId, bool setIsDeleted) public int SubmitUpdate() => this.SubmitUpdate(out _, out _, out _, out _); - public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts, - out bool newMentionOrDm, out bool isDeletePost) + public int SubmitUpdate( + out string soundFile, + out PostClass[] notifyPosts, + out bool newMentionOrDm, + out bool isDeletePost) { // 注:メインスレッドから呼ぶこと - lock (this.LockObj) + lock (this.lockObj) { soundFile = ""; notifyPosts = Array.Empty(); @@ -417,7 +484,7 @@ public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts, public int DistributePosts() { - lock (this.LockObj) + lock (this.lockObj) { var homeTab = this.HomeTab; var replyTab = this.MentionTab; @@ -486,56 +553,56 @@ public int DistributePosts() } } - public void AddPost(PostClass Item) + public void AddPost(PostClass item) { - Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する"); + Debug.Assert(!item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する"); - lock (LockObj) + lock (this.lockObj) { - if (this.IsMuted(Item, isHomeTimeline: true)) + if (this.IsMuted(item, isHomeTimeline: true)) return; - if (Posts.TryGetValue(Item.StatusId, out var status)) + if (this.Posts.TryGetValue(item.StatusId, out var status)) { - if (Item.IsFav) + if (item.IsFav) { - if (Item.RetweetedId == null) + if (item.RetweetedId == null) { status.IsFav = true; } else { - Item.IsFav = false; + item.IsFav = false; } } else { - return; //追加済みなら何もしない + return; // 追加済みなら何もしない } } else { - if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false; + if (item.IsFav && item.RetweetedId != null) item.IsFav = false; - //既に持っている公式RTは捨てる - if (Item.RetweetedId != null && SettingManager.Common.HideDuplicatedRetweets) + // 既に持っている公式RTは捨てる + if (item.RetweetedId != null && SettingManager.Instance.Common.HideDuplicatedRetweets) { - var retweetCount = this.UpdateRetweetCount(Item); + var retweetCount = this.UpdateRetweetCount(item); - if (retweetCount > 1 && !Item.IsMe) + if (retweetCount > 1 && !item.IsMe) return; } - if (BlockIds.Contains(Item.UserId)) + if (this.BlockIds.Contains(item.UserId)) return; - Posts.TryAdd(Item.StatusId, Item); + this.Posts.TryAdd(item.StatusId, item); } - if (Item.IsFav && this.retweetsCount.ContainsKey(Item.StatusId)) + if (item.IsFav && this.retweetsCount.ContainsKey(item.StatusId)) { - return; //Fav済みのRetweet元発言は追加しない + return; // Fav済みのRetweet元発言は追加しない } - this.addQueue.Enqueue(Item.StatusId); + this.addQueue.Enqueue(item.StatusId); } } @@ -577,12 +644,12 @@ private int UpdateRetweetCount(PostClass retweetPost) public bool AddQuoteTweet(PostClass item) { - lock (LockObj) + lock (this.lockObj) { - if (IsMuted(item, isHomeTimeline: false) || BlockIds.Contains(item.UserId)) + if (this.IsMuted(item, isHomeTimeline: false) || this.BlockIds.Contains(item.UserId)) return false; - _quotes[item.StatusId] = item; + this.quotes[item.StatusId] = item; return true; } } @@ -595,7 +662,7 @@ public bool AddQuoteTweet(PostClass item) /// 既読状態に変化があれば true、変化がなければ false public bool SetReadAllTab(long statusId, bool read) { - lock (LockObj) + lock (this.lockObj) { foreach (var tab in this.Tabs) { @@ -621,7 +688,7 @@ public void SetReadHomeTab() { var homeTab = this.HomeTab; - lock (LockObj) + lock (this.lockObj) { foreach (var statusId in homeTab.GetUnreadIds()) { @@ -636,42 +703,42 @@ public void SetReadHomeTab() } } - public PostClass? this[long ID] + public PostClass? this[long id] { get { - if (this.Posts.TryGetValue(ID, out var status)) + if (this.Posts.TryGetValue(id, out var status)) return status; - if (this._quotes.TryGetValue(ID, out status)) + if (this.quotes.TryGetValue(id, out status)) return status; return this.GetTabsInnerStorageType() - .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null) + .Select(x => x.Posts.TryGetValue(id, out status) ? status : null) .FirstOrDefault(x => x != null); } } - public bool ContainsKey(long Id) + public bool ContainsKey(long id) { - //DM,公式検索は非対応 - lock (LockObj) + // DM,公式検索は非対応 + lock (this.lockObj) { - return Posts.ContainsKey(Id); + return this.Posts.ContainsKey(id); } } - public void RenameTab(string Original, string NewName) + public void RenameTab(string original, string newName) { - lock (this.LockObj) + lock (this.lockObj) { - var index = this.Tabs.IndexOf(Original); - var tb = this.Tabs[Original]; + var index = this.Tabs.IndexOf(original); + var tb = this.Tabs[original]; this.tabs.RemoveAt(index); - tb.TabName = NewName; + tb.TabName = newName; - if (this.SelectedTabName == Original) - this.SelectedTabName = NewName; + if (this.SelectedTabName == original) + this.SelectedTabName = newName; this.tabs.Insert(index, tb); } @@ -679,7 +746,7 @@ public void RenameTab(string Original, string NewName) public void FilterAll() { - lock (LockObj) + lock (this.lockObj) { var homeTab = this.HomeTab; var detachedIdsAll = Enumerable.Empty(); @@ -696,7 +763,7 @@ public void FilterAll() var orgIds = tab.StatusIds; tab.ClearIDs(); - foreach (var post in Posts.Values) + foreach (var post in this.Posts.Values) { var filterHit = false; // フィルタにヒットしたタブがあるか var mark = false; // フィルタによってマーク付けされたか @@ -771,44 +838,47 @@ public void FilterAll() } } - public void ClearTabIds(string TabName) + public void ClearTabIds(string tabName) { - //不要なPostを削除 - lock (LockObj) + // 不要なPostを削除 + lock (this.lockObj) { - var tb = this.Tabs[TabName]; + var tb = this.Tabs[tabName]; if (!tb.IsInnerStorageTabType) { - foreach (var Id in tb.StatusIds) + foreach (var id in tb.StatusIds) { - var Hit = false; + var hit = false; foreach (var tab in this.Tabs) { - if (tab.Contains(Id)) + if (tab.Contains(id)) { - Hit = true; + hit = true; break; } } - if (!Hit) - Posts.TryRemove(Id, out var removedPost); + if (!hit) + this.Posts.TryRemove(id, out var removedPost); } } - //指定タブをクリア + // 指定タブをクリア tb.ClearIDs(); } } public void RefreshOwl(ISet follower) { - lock (LockObj) + lock (this.lockObj) { + var allPosts = this.GetTabsInnerStorageType() + .SelectMany(x => x.Posts.Values) + .Concat(this.Posts.Values); + if (follower.Count > 0) { - foreach (var post in Posts.Values) + foreach (var post in allPosts) { - //if (post.UserId = 0 || post.IsDm) Continue For if (post.IsMe) { post.IsOwl = false; @@ -821,7 +891,7 @@ public void RefreshOwl(ISet follower) } else { - foreach (var post in Posts.Values) + foreach (var post in allPosts) { post.IsOwl = false; } @@ -843,24 +913,25 @@ public FavoritesTabModel FavoriteTab public TabModel? GetTabByType(MyCommon.TabUsageType tabType) { - //Home,Mentions,DM,Favは1つに制限する - //その他のタイプを指定されたら、最初に合致したものを返す - //合致しなければnullを返す - lock (LockObj) + // Home,Mentions,DM,Favは1つに制限する + // その他のタイプを指定されたら、最初に合致したものを返す + // 合致しなければnullを返す + lock (this.lockObj) { return this.Tabs.FirstOrDefault(x => x.TabType.HasFlag(tabType)); } } - public T? GetTabByType() where T : TabModel + public T? GetTabByType() + where T : TabModel { - lock (this.LockObj) + lock (this.lockObj) return this.Tabs.OfType().FirstOrDefault(); } public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType) { - lock (LockObj) + lock (this.lockObj) { return this.Tabs .Where(x => x.TabType.HasFlag(tabType)) @@ -868,15 +939,16 @@ public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType) } } - public T[] GetTabsByType() where T : TabModel + public T[] GetTabsByType() + where T : TabModel { - lock (this.LockObj) + lock (this.lockObj) return this.Tabs.OfType().ToArray(); } public TabModel[] GetTabsInnerStorageType() { - lock (LockObj) + lock (this.lockObj) { return this.Tabs .Where(x => x.IsInnerStorageTabType) @@ -886,7 +958,7 @@ public TabModel[] GetTabsInnerStorageType() public TabModel? GetTabByName(string tabName) { - lock (LockObj) + lock (this.lockObj) { return this.Tabs.TryGetValue(tabName, out var tab) ? tab diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index 99a7d40aa..d9c054279 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -44,14 +44,19 @@ public abstract class TabModel public string TabName { get; set; } public bool UnreadManage { get; set; } = true; + public bool Protected { get; set; } + public bool Notify { get; set; } = false; + public string SoundFile { get; set; } = ""; public ComparerMode SortMode { get; private set; } + public SortOrder SortOrder { get; private set; } public long OldestId { get; set; } = long.MaxValue; + public long SinceId { get; set; } public abstract MyCommon.TabUsageType TabType { get; } @@ -59,11 +64,14 @@ public abstract class TabModel public virtual ConcurrentDictionary Posts => TabInformations.GetInstance().Posts; - public int AllCount => this._ids.Count; - public long[] StatusIds => this._ids.ToArray(); + public int AllCount => this.ids.Count; + + public long[] StatusIds => this.ids.ToArray(); public bool IsDefaultTabType => this.TabType.IsDefault(); + public bool IsDistributableTabType => this.TabType.IsDistributable(); + public bool IsInnerStorageTabType => this.TabType.IsInnerStorage(); /// @@ -92,30 +100,40 @@ public int SelectedIndex } } - private IndexedSortedSet _ids = new IndexedSortedSet(); - private ConcurrentQueue addQueue = new ConcurrentQueue(); - private readonly ConcurrentQueue removeQueue = new ConcurrentQueue(); - private SortedSet unreadIds = new SortedSet(); - private List selectedStatusIds = new List(); + public long? AnchorStatusId { get; set; } + + public PostClass? AnchorPost + { + get + { + if (this.AnchorStatusId == null) + return null; + + if (!this.Posts.TryGetValue(this.AnchorStatusId.Value, out var post)) + return null; + + return post; + } + set => this.AnchorStatusId = value?.StatusId; + } + + private IndexedSortedSet ids = new(); + private ConcurrentQueue addQueue = new(); + private readonly ConcurrentQueue removeQueue = new(); + private SortedSet unreadIds = new(); + private List selectedStatusIds = new(); - private readonly object _lockObj = new object(); + private readonly object lockObj = new(); protected TabModel(string tabName) => this.TabName = tabName; public abstract Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress); - private readonly struct TemporaryId - { - public long StatusId { get; } - public bool Read { get; } - - public TemporaryId(long statusId, bool read) - { - this.StatusId = statusId; - this.Read = read; - } - } + private readonly record struct TemporaryId( + long StatusId, + bool Read + ); public virtual void AddPostQueue(PostClass post) { @@ -125,10 +143,10 @@ public virtual void AddPostQueue(PostClass post) this.addQueue.Enqueue(new TemporaryId(post.StatusId, post.IsRead)); } - //無条件に追加 + // 無条件に追加 internal bool AddPostImmediately(long statusId, bool read) { - if (!this._ids.Add(statusId)) + if (!this.ids.Add(statusId)) return false; if (!read) @@ -155,7 +173,7 @@ public virtual void EnqueueRemovePost(long statusId, bool setIsDeleted) public virtual bool RemovePostImmediately(long statusId) { - if (!this._ids.Remove(statusId)) + if (!this.ids.Remove(statusId)) return false; this.unreadIds.Remove(statusId); @@ -189,9 +207,12 @@ bool IsValidIndex(int index) this.selectedStatusIds = statusIds; } + public void ClearAnchor() + => this.AnchorStatusId = null; + public virtual void ClearIDs() { - this._ids.Clear(); + this.ids.Clear(); this.unreadIds.Clear(); this.selectedStatusIds.Clear(); @@ -229,23 +250,13 @@ private void ApplySortMode() } else { - Comparison postComparison; - switch (this.SortMode) + Comparison postComparison = this.SortMode switch { - default: - case ComparerMode.Data: - postComparison = (x, y) => Comparer.Default.Compare(x?.TextFromApi, y?.TextFromApi); - break; - case ComparerMode.Name: - postComparison = (x, y) => Comparer.Default.Compare(x?.ScreenName, y?.ScreenName); - break; - case ComparerMode.Nickname: - postComparison = (x, y) => Comparer.Default.Compare(x?.Nickname, y?.Nickname); - break; - case ComparerMode.Source: - postComparison = (x, y) => Comparer.Default.Compare(x?.Source, y?.Source); - break; - } + ComparerMode.Name => (x, y) => Comparer.Default.Compare(x?.ScreenName, y?.ScreenName), + ComparerMode.Nickname => (x, y) => Comparer.Default.Compare(x?.Nickname, y?.Nickname), + ComparerMode.Source => (x, y) => Comparer.Default.Compare(x?.Source, y?.Source), + _ => (x, y) => Comparer.Default.Compare(x?.TextFromApi, y?.TextFromApi), + }; comparison = (x, y) => { @@ -263,7 +274,7 @@ private void ApplySortMode() var comparer = Comparer.Create(comparison); - this._ids = new IndexedSortedSet(this._ids, comparer); + this.ids = new IndexedSortedSet(this.ids, comparer); this.unreadIds = new SortedSet(this.unreadIds, comparer); } @@ -275,7 +286,7 @@ public long NextUnreadId { get { - if (!this.UnreadManage || !SettingManager.Common.UnreadManage) + if (!this.UnreadManage || !SettingManager.Instance.Common.UnreadManage) return -1L; if (this.unreadIds.Count == 0) @@ -308,7 +319,7 @@ public int UnreadCount { get { - if (!this.UnreadManage || !SettingManager.Common.UnreadManage) + if (!this.UnreadManage || !SettingManager.Instance.Common.UnreadManage) return 0; return this.unreadIds.Count; @@ -320,7 +331,7 @@ public int UnreadCount /// public long[] GetUnreadIds() { - lock (this._lockObj) + lock (this.lockObj) return this.unreadIds.ToArray(); } @@ -335,7 +346,7 @@ public long[] GetUnreadIds() /// 既読状態に変化があれば true、変化がなければ false internal virtual bool SetReadState(long statusId, bool read) { - if (!this._ids.Contains(statusId)) + if (!this.ids.Contains(statusId)) throw new ArgumentOutOfRangeException(nameof(statusId)); if (read) @@ -345,7 +356,7 @@ internal virtual bool SetReadState(long statusId, bool read) } public bool Contains(long statusId) - => this._ids.Contains(statusId); + => this.ids.Contains(statusId); public PostClass this[int index] { @@ -387,7 +398,7 @@ public long[] GetStatusIdAt(IEnumerable indexes) => indexes.Select(x => this.GetStatusIdAt(x)).ToArray(); public long GetStatusIdAt(int index) - => this._ids[index]; + => this.ids[index]; public int[] IndexOf(long[] statusIds) { @@ -398,7 +409,7 @@ public int[] IndexOf(long[] statusIds) } public int IndexOf(long statusId) - => this._ids.IndexOf(statusId); + => this.ids.IndexOf(statusId); public IEnumerable SearchPostsAll(Func stringComparer) => this.SearchPostsAll(stringComparer, reverse: false); @@ -419,7 +430,6 @@ public IEnumerable SearchPostsAll(Func stringComparer, bool r /// 発言内容、スクリーン名、名前と比較する条件。マッチしたら true を返す /// 検索を開始する位置 /// インデックスの昇順に検索する場合は false、降順の場合は true - /// public IEnumerable SearchPostsAll(Func stringComparer, int startIndex, bool reverse) { if (this.AllCount == 0) diff --git a/OpenTween/Models/TabUsageTypeExt.cs b/OpenTween/Models/TabUsageTypeExt.cs index 318f9d7f1..3f85fe44a 100644 --- a/OpenTween/Models/TabUsageTypeExt.cs +++ b/OpenTween/Models/TabUsageTypeExt.cs @@ -34,19 +34,19 @@ namespace OpenTween.Models /// public static class TabUsageTypeExt { - const MyCommon.TabUsageType DefaultTabTypeMask = + private const MyCommon.TabUsageType DefaultTabTypeMask = MyCommon.TabUsageType.Home | MyCommon.TabUsageType.Mentions | MyCommon.TabUsageType.DirectMessage | MyCommon.TabUsageType.Favorites | MyCommon.TabUsageType.Mute; - const MyCommon.TabUsageType DistributableTabTypeMask = + private const MyCommon.TabUsageType DistributableTabTypeMask = MyCommon.TabUsageType.Mentions | MyCommon.TabUsageType.UserDefined | MyCommon.TabUsageType.Mute; - const MyCommon.TabUsageType InnerStorageTabTypeMask = + private const MyCommon.TabUsageType InnerStorageTabTypeMask = MyCommon.TabUsageType.DirectMessage | MyCommon.TabUsageType.PublicSearch | MyCommon.TabUsageType.Lists | diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs new file mode 100644 index 000000000..b792b4bda --- /dev/null +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -0,0 +1,570 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2022 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using OpenTween.Api.DataModel; + +namespace OpenTween.Models +{ + public class TwitterPostFactory + { + private static readonly Uri SourceUriBase = new("https://twitter.com/"); + + private readonly TabInformations tabinfo; + private readonly HashSet receivedHashTags = new(); + + public TwitterPostFactory(TabInformations tabinfo) + => this.tabinfo = tabinfo; + + public string[] GetReceivedHashtags() + { + lock (this.receivedHashTags) + { + var hashtags = this.receivedHashTags.ToArray(); + this.receivedHashTags.Clear(); + return hashtags; + } + } + + public PostClass CreateFromStatus( + TwitterStatus status, + long selfUserId, + ISet followerIds, + bool favTweet = false + ) + { + var post = new PostClass(); + TwitterEntities entities; + string sourceHtml; + + post.StatusId = status.Id; + if (status.RetweetedStatus != null) + { + var retweeted = status.RetweetedStatus; + + post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt); + + // Id + post.RetweetedId = retweeted.Id; + // 本文 + post.TextFromApi = retweeted.FullText; + entities = retweeted.MergedEntities; + sourceHtml = retweeted.Source; + // Reply先 + post.InReplyToStatusId = retweeted.InReplyToStatusId; + post.InReplyToUser = retweeted.InReplyToScreenName; + post.InReplyToUserId = status.InReplyToUserId; + + if (favTweet) + { + post.IsFav = true; + } + else + { + // 幻覚fav対策 + var favTab = this.tabinfo.FavoriteTab; + post.IsFav = favTab.Contains(retweeted.Id); + } + + if (retweeted.Coordinates != null) + post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]); + + // 以下、ユーザー情報 + var user = retweeted.User; + if (user != null) + { + post.UserId = user.Id; + post.ScreenName = user.ScreenName; + post.Nickname = user.Name.Trim(); + post.ImageUrl = user.ProfileImageUrlHttps; + post.IsProtect = user.Protected; + } + else + { + post.UserId = 0L; + post.ScreenName = "?????"; + post.Nickname = "Unknown User"; + } + + // Retweetした人 + if (status.User != null) + { + post.RetweetedBy = status.User.ScreenName; + post.RetweetedByUserId = status.User.Id; + post.IsMe = post.RetweetedByUserId == selfUserId; + } + else + { + post.RetweetedBy = "?????"; + post.RetweetedByUserId = 0L; + } + } + else + { + post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt); + // 本文 + post.TextFromApi = status.FullText; + entities = status.MergedEntities; + sourceHtml = status.Source; + post.InReplyToStatusId = status.InReplyToStatusId; + post.InReplyToUser = status.InReplyToScreenName; + post.InReplyToUserId = status.InReplyToUserId; + + if (favTweet) + { + post.IsFav = true; + } + else + { + // 幻覚fav対策 + var favTab = this.tabinfo.FavoriteTab; + post.IsFav = favTab.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav; + } + + if (status.Coordinates != null) + post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]); + + // 以下、ユーザー情報 + var user = status.User; + if (user != null) + { + post.UserId = user.Id; + post.ScreenName = user.ScreenName; + post.Nickname = user.Name.Trim(); + post.ImageUrl = user.ProfileImageUrlHttps; + post.IsProtect = user.Protected; + post.IsMe = post.UserId == selfUserId; + } + else + { + post.UserId = 0L; + post.ScreenName = "?????"; + post.Nickname = "Unknown User"; + } + } + // HTMLに整形 + var textFromApi = post.TextFromApi; + + var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink; + + if (quotedStatusLink != null && entities.Urls != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded)) + quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある + + post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink); + post.TextFromApi = textFromApi; + post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink); + post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); + post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); + post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink); + post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); + post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); + + this.ExtractEntities(entities, post.ReplyToList, post.Media); + + post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) + .Where(x => x != post.StatusId && x != post.RetweetedId) + .Distinct().ToArray(); + + post.ExpandedUrls = entities.OfType() + .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) + .ToArray(); + + // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) + if (post.Text == post.TextFromApi) + post.Text = post.TextFromApi; + if (post.AccessibleText == post.TextFromApi) + post.AccessibleText = post.TextFromApi; + + // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す + post.ScreenName = string.Intern(post.ScreenName); + post.Nickname = string.Intern(post.Nickname); + post.ImageUrl = string.Intern(post.ImageUrl); + post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null; + + // Source整形 + var (sourceText, sourceUri) = ParseSource(sourceHtml); + post.Source = string.Intern(sourceText); + post.SourceUri = sourceUri; + + post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == selfUserId); + post.IsExcludeReply = false; + + if (post.IsMe) + { + post.IsOwl = false; + } + else + { + if (followerIds.Count > 0) + post.IsOwl = !followerIds.Contains(post.UserId); + } + + post.IsDm = false; + return post; + } + + public PostClass CreateFromDirectMessageEvent( + TwitterMessageEvent eventItem, + IReadOnlyDictionary users, + IReadOnlyDictionary apps, + long selfUserId + ) + { + var post = new PostClass(); + post.StatusId = long.Parse(eventItem.Id); + + var timestamp = long.Parse(eventItem.CreatedTimestamp); + post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond); + // 本文 + var textFromApi = eventItem.MessageCreate.MessageData.Text; + + var entities = eventItem.MessageCreate.MessageData.Entities; + var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media; + + if (mediaEntity != null) + entities.Media = new[] { mediaEntity }; + + // HTMLに整形 + post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null); + post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null); + post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); + post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); + post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null); + post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); + post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); + post.IsFav = false; + + this.ExtractEntities(entities, post.ReplyToList, post.Media); + + post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null) + .Distinct().ToArray(); + + post.ExpandedUrls = entities.OfType() + .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) + .ToArray(); + + // 以下、ユーザー情報 + string userId; + if (eventItem.MessageCreate.SenderId != selfUserId.ToString(CultureInfo.InvariantCulture)) + { + userId = eventItem.MessageCreate.SenderId; + post.IsMe = false; + post.IsOwl = true; + } + else + { + userId = eventItem.MessageCreate.Target.RecipientId; + post.IsMe = true; + post.IsOwl = false; + } + + if (users.TryGetValue(userId, out var user)) + { + post.UserId = user.Id; + post.ScreenName = user.ScreenName; + post.Nickname = user.Name.Trim(); + post.ImageUrl = user.ProfileImageUrlHttps; + post.IsProtect = user.Protected; + } + else + { + post.UserId = 0L; + post.ScreenName = "?????"; + post.Nickname = "Unknown User"; + } + + // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) + if (post.Text == post.TextFromApi) + post.Text = post.TextFromApi; + if (post.AccessibleText == post.TextFromApi) + post.AccessibleText = post.TextFromApi; + + // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す + post.ScreenName = string.Intern(post.ScreenName); + post.Nickname = string.Intern(post.Nickname); + post.ImageUrl = string.Intern(post.ImageUrl); + + var appId = eventItem.MessageCreate.SourceAppId; + if (appId != null && apps.TryGetValue(appId, out var app)) + { + post.Source = string.Intern(app.Name); + + try + { + post.SourceUri = new Uri(SourceUriBase, app.Url); + } + catch (UriFormatException) + { + } + } + + post.IsReply = false; + post.IsExcludeReply = false; + post.IsDm = true; + + return post; + } + + private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink) + { + if (entities?.Urls != null) + { + foreach (var m in entities.Urls) + { + if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) + text = text.Replace(m.Url, m.DisplayUrl); + } + } + + if (entities?.Media != null) + { + foreach (var m in entities.Media) + { + if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) + text = text.Replace(m.Url, m.DisplayUrl); + } + } + + if (quotedStatusLink != null) + text += " " + quotedStatusLink.Display; + + return text; + } + + private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> atList, List media) + { + if (entities == null) + return; + + if (entities.Hashtags != null) + { + var hashtags = entities.Hashtags.Select(x => $"#{x.Text}"); + + lock (this.receivedHashTags) + this.receivedHashTags.UnionWith(hashtags); + } + + if (entities.UserMentions != null) + { + foreach (var ent in entities.UserMentions) + atList.Add((ent.Id, ent.ScreenName)); + } + + if (entities.Media != null) + { + if (media != null) + { + foreach (var ent in entities.Media) + { + if (media.Any(x => x.Url == ent.MediaUrlHttps)) + continue; + + var videoUrl = + ent.VideoInfo != null && ent.Type == "animated_gif" || ent.Type == "video" + ? ent.ExpandedUrl + : null; + + var mediaInfo = new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl); + media.Add(mediaInfo); + } + } + } + } + + private static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink) + { + if (entities == null) + return text; + + if (entities.Urls != null) + { + foreach (var entity in entities.Urls) + { + if (quotedStatus != null) + { + var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl); + if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr) + { + var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); + text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText)); + continue; + } + } + + if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl)) + text = text.Replace(entity.Url, entity.DisplayUrl); + } + } + + if (entities.Media != null) + { + foreach (var entity in entities.Media) + { + if (!MyCommon.IsNullOrEmpty(entity.AltText)) + { + text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText)); + } + else if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl)) + { + text = text.Replace(entity.Url, entity.DisplayUrl); + } + } + } + + if (quotedStatus != null && quotedStatusLink != null) + { + var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); + text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText); + } + + return text; + } + + internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink? quotedStatusLink) + { + var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text)); + + // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない + text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true); + + text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1$2$3"); + text = PreProcessUrl(text); // IDN置換 + + if (quotedStatusLink != null) + { + text += string.Format(" {1}", + WebUtility.HtmlEncode(quotedStatusLink.Expanded), + WebUtility.HtmlEncode(quotedStatusLink.Display)); + } + + return text; + } + + private static string PreProcessUrl(string orgData) + { + int posl1; + var posl2 = 0; + var href = " -1) + { + // IDN展開 + posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal); + posl1 += href.Length; + posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal); + var urlStr = orgData.Substring(posl1, posl2 - posl1); + + if (!urlStr.StartsWith("http://", StringComparison.Ordinal) + && !urlStr.StartsWith("https://", StringComparison.Ordinal) + && !urlStr.StartsWith("ftp://", StringComparison.Ordinal)) + { + continue; + } + + var replacedUrl = MyCommon.IDNEncode(urlStr); + if (replacedUrl == null) continue; + if (replacedUrl == urlStr) continue; + + orgData = orgData.Replace(" + /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します + /// + public static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml) + { + if (MyCommon.IsNullOrEmpty(sourceHtml)) + return ("", null); + + string sourceText; + Uri? sourceUri; + + // sourceHtmlの例: Twitter Web Client + + var match = Regex.Match(sourceHtml, "^.+?)\".*?>(?.+)$", RegexOptions.IgnoreCase); + if (match.Success) + { + sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value); + try + { + var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value); + sourceUri = new Uri(SourceUriBase, uriStr); + } + catch (UriFormatException) + { + sourceUri = null; + } + } + else + { + sourceText = WebUtility.HtmlDecode(sourceHtml); + sourceUri = null; + } + + return (sourceText, sourceUri); + } + + /// + /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出 + /// + public static IEnumerable GetQuoteTweetStatusIds(IEnumerable? entities, TwitterQuotedStatusPermalink? quotedStatusLink) + { + entities ??= Enumerable.Empty(); + + var urls = entities.OfType().Select(x => x.ExpandedUrl); + + if (quotedStatusLink != null) + urls = urls.Append(quotedStatusLink.Expanded); + + return GetQuoteTweetStatusIds(urls); + } + + public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) + { + foreach (var url in urls) + { + var match = Twitter.StatusUrlRegex.Match(url); + if (match.Success) + { + if (long.TryParse(match.Groups["StatusId"].Value, out var statusId)) + yield return statusId; + } + } + } + } +} diff --git a/OpenTween/Models/UserTimelineTabModel.cs b/OpenTween/Models/UserTimelineTabModel.cs index 07110ca8a..05329f8c5 100644 --- a/OpenTween/Models/UserTimelineTabModel.cs +++ b/OpenTween/Models/UserTimelineTabModel.cs @@ -55,10 +55,10 @@ public override async Task RefreshAsync(Twitter tw, bool backward, bool startup, return; bool read; - if (!SettingManager.Common.UnreadManage) + if (!SettingManager.Instance.Common.UnreadManage) read = true; else - read = startup && SettingManager.Common.Read; + read = startup && SettingManager.Instance.Common.Read; progress.Report("UserTimeline refreshing..."); diff --git a/OpenTween/MouseWheelMessageFilter.cs b/OpenTween/MouseWheelMessageFilter.cs index eb2e69156..fb4d545fb 100644 --- a/OpenTween/MouseWheelMessageFilter.cs +++ b/OpenTween/MouseWheelMessageFilter.cs @@ -37,7 +37,7 @@ namespace OpenTween /// public class MouseWheelMessageFilter : IMessageFilter { - private readonly List controls = new List(); + private readonly List controls = new(); public MouseWheelMessageFilter() => Application.AddMessageFilter(this); @@ -75,17 +75,10 @@ public bool PreFilterMessage(ref Message m) return false; } - internal class MouseEvent - { - public Point ScreenLocation { get; } - public int WheelDelta { get; } - - public MouseEvent(Point location, int delta) - { - this.ScreenLocation = location; - this.WheelDelta = delta; - } - } + internal readonly record struct MouseEvent( + Point ScreenLocation, + int WheelDelta + ); internal static MouseEvent ParseMessage(Message m) { diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index 0629adf42..ba9d2d90a 100644 --- a/OpenTween/MyCommon.cs +++ b/OpenTween/MyCommon.cs @@ -7,19 +7,19 @@ // (c) 2011 Egtra (@egtra) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General public License along // with this program. if not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -28,40 +28,40 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.Windows.Forms; -using System.Web; -using System.Globalization; -using System.Security.Cryptography; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; -using System.Collections; -using System.Security.Principal; -using System.Runtime.Serialization.Json; -using System.Reflection; -using System.Diagnostics; -using System.Text.RegularExpressions; +using System.Globalization; +using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; +using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Serialization.Json; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using System.Windows.Forms; using OpenTween.Api; using OpenTween.Models; using OpenTween.Setting; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using System.ComponentModel; namespace OpenTween { public static class MyCommon { - private static readonly object LockObj = new object(); - public static bool _endingFlag; //終了フラグ - public static string settingPath = null!; + private static readonly object LockObj = new(); + + public static bool EndingFlag { get; set; } // 終了フラグ public enum IconSizes { @@ -112,13 +112,27 @@ public enum UrlConverter Bitly, Jmp, Uxnu, - //特殊 + // 特殊 Nicoms, - //廃止 + // 廃止 Unu = -1, Twurl = -1, } + public enum ListItemDoubleClickActionType + { + // 設定ファイルの互換性を保つため新規の項目は途中に追加しないこと + Reply, + Favorite, + ShowProfile, + ShowTimeline, + ShowRelated, + OpenHomeInBrowser, + OpenStatusInBrowser, + None, + ReplyAll, + } + public enum HITRESULT { None, @@ -135,28 +149,28 @@ public enum HttpTimeOut DefaultValue = 20, } - //Backgroundworkerへ処理種別を通知するための引数用enum + // Backgroundworkerへ処理種別を通知するための引数用enum public enum WORKERTYPE { - Timeline, //タイムライン取得 - Reply, //返信取得 - DirectMessegeRcv, //受信DM取得 - DirectMessegeSnt, //送信DM取得 - PostMessage, //発言POST - FavAdd, //Fav追加 - FavRemove, //Fav削除 - Follower, //Followerリスト取得 - Favorites, //Fav取得 - Retweet, //Retweetする - PublicSearch, //公式検索 - List, //Lists - Related, //関連発言 - UserTimeline, //UserTimeline - BlockIds, //Blocking/ids - Configuration, //Twitter Configuration読み込み - NoRetweetIds, //RT非表示ユーザー取得 + Timeline, // タイムライン取得 + Reply, // 返信取得 + DirectMessegeRcv, // 受信DM取得 + DirectMessegeSnt, // 送信DM取得 + PostMessage, // 発言POST + FavAdd, // Fav追加 + FavRemove, // Fav削除 + Follower, // Followerリスト取得 + Favorites, // Fav取得 + Retweet, // Retweetする + PublicSearch, // 公式検索 + List, // Lists + Related, // 関連発言 + UserTimeline, // UserTimeline + BlockIds, // Blocking/ids + Configuration, // Twitter Configuration読み込み + NoRetweetIds, // RT非表示ユーザー取得 ////// - ErrorState, //エラー表示のみで後処理終了(認証エラー時など) + ErrorState, // エラー表示のみで後処理終了(認証エラー時など) } public static class DEFAULTTAB @@ -191,6 +205,7 @@ public enum REPLY_ICONSTATE } public static _Assembly EntryAssembly { get; internal set; } + public static string FileVersion { get; internal set; } static MyCommon() @@ -215,20 +230,20 @@ public static void TraceOut(WebApiException ex) TraceOut(TraceFlag, message); } - public static void TraceOut(Exception ex, string Message) + public static void TraceOut(Exception ex, string message) { var buf = ExceptionOutMessage(ex); - TraceOut(TraceFlag, Message + Environment.NewLine + buf); + TraceOut(TraceFlag, message + Environment.NewLine + buf); } - public static void TraceOut(string Message) - => TraceOut(TraceFlag, Message); + public static void TraceOut(string message) + => TraceOut(TraceFlag, message); - public static void TraceOut(bool OutputFlag, string Message) + public static void TraceOut(bool outputFlag, string message) { lock (LockObj) { - if (!OutputFlag) return; + if (!outputFlag) return; var logPath = MyCommon.GetErrorLogPath(); if (!Directory.Exists(logPath)) @@ -248,7 +263,7 @@ public static void TraceOut(bool OutputFlag, string Message) writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString); writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version); writer.WriteLine(Properties.Resources.TraceOutText6, ApplicationSettings.AssemblyName, FileVersion); - writer.WriteLine(Message); + writer.WriteLine(message); writer.WriteLine(); } } @@ -260,11 +275,11 @@ public static void TraceOut(bool OutputFlag, string Message) public static string ExceptionOutMessage(Exception ex) { - var IsTerminatePermission = true; - return ExceptionOutMessage(ex, ref IsTerminatePermission); + var isTerminatePermission = true; + return ExceptionOutMessage(ex, ref isTerminatePermission); } - public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermission) + public static string ExceptionOutMessage(Exception ex, ref bool isTerminatePermission) { if (ex == null) return ""; @@ -287,7 +302,7 @@ public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermi buf.AppendLine(); if (dt.Key.Equals("IsTerminatePermission")) { - IsTerminatePermission = (bool)dt.Value; + isTerminatePermission = (bool)dt.Value; } } if (!needHeader) @@ -298,20 +313,20 @@ public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermi buf.AppendLine(ex.StackTrace); buf.AppendLine(); - //InnerExceptionが存在する場合書き出す - var _ex = ex.InnerException; + // InnerExceptionが存在する場合書き出す + var innerException = ex.InnerException; var nesting = 0; - while (_ex != null) + while (innerException != null) { buf.AppendFormat("-----InnerException[{0}]-----\r\n", nesting); buf.AppendLine(); - buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, _ex.GetType().FullName, _ex.Message); + buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, innerException.GetType().FullName, innerException.Message); buf.AppendLine(); - if (_ex.Data != null) + if (innerException.Data != null) { var needHeader = true; - foreach (DictionaryEntry dt in _ex.Data) + foreach (DictionaryEntry dt in innerException.Data) { if (needHeader) { @@ -322,7 +337,7 @@ public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermi buf.AppendFormat("{0} : {1}", dt.Key, dt.Value); if (dt.Key.Equals("IsTerminatePermission")) { - IsTerminatePermission = (bool)dt.Value; + isTerminatePermission = (bool)dt.Value; } } if (!needHeader) @@ -330,10 +345,10 @@ public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermi buf.AppendLine("-----End Extra Information-----"); } } - buf.AppendLine(_ex.StackTrace); + buf.AppendLine(innerException.StackTrace); buf.AppendLine(); nesting++; - _ex = _ex.InnerException; + innerException = innerException.InnerException; } return buf.ToString(); } @@ -342,7 +357,7 @@ public static bool ExceptionOut(Exception ex) { lock (LockObj) { - var IsTerminatePermission = true; + var isTerminatePermission = true; var ident = WindowsIdentity.GetCurrent(); var princ = new WindowsPrincipal(ident); @@ -362,7 +377,7 @@ public static bool ExceptionOut(Exception ex) string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version), string.Format(Properties.Resources.UnhandledExceptionText7, ApplicationSettings.AssemblyName, FileVersion), - ExceptionOutMessage(ex, ref IsTerminatePermission)); + ExceptionOutMessage(ex, ref isTerminatePermission)); var logPath = MyCommon.GetErrorLogPath(); if (!Directory.Exists(logPath)) @@ -374,7 +389,7 @@ public static bool ExceptionOut(Exception ex) writer.Write(errorReport); } - var settings = SettingManager.Common; + var settings = SettingManager.Instance; var mainForm = Application.OpenForms.OfType().FirstOrDefault(); ErrorReport report; @@ -383,15 +398,15 @@ public static bool ExceptionOut(Exception ex) else report = new ErrorReport(errorReport); - report.AnonymousReport = settings.ErrorReportAnonymous; + report.AnonymousReport = settings.Common.ErrorReportAnonymous; OpenErrorReportDialog(mainForm, report); // ダイアログ内で設定が変更されていれば保存する - if (settings.ErrorReportAnonymous != report.AnonymousReport) + if (settings.Common.ErrorReportAnonymous != report.AnonymousReport) { - settings.ErrorReportAnonymous = report.AnonymousReport; - settings.Save(); + settings.Common.ErrorReportAnonymous = report.AnonymousReport; + settings.SaveCommon(); } return false; @@ -492,13 +507,11 @@ public static void MoveArrayItem(int[] values, int idx_fr, int idx_to) if (idx_to < idx_fr) { - Array.Copy(values, idx_to, values, - idx_to + 1, num_moved); + Array.Copy(values, idx_to, values, idx_to + 1, num_moved); } else { - Array.Copy(values, idx_fr + 1, values, - idx_fr, num_moved); + Array.Copy(values, idx_fr + 1, values, idx_fr, num_moved); } values[idx_to] = moved_value; @@ -508,16 +521,16 @@ public static string EncryptString(string str) { if (MyCommon.IsNullOrEmpty(str)) return ""; - //文字列をバイト型配列にする + // 文字列をバイト型配列にする var bytesIn = Encoding.UTF8.GetBytes(str); - //DESCryptoServiceProviderオブジェクトの作成 + // DESCryptoServiceProviderオブジェクトの作成 using var des = new DESCryptoServiceProvider(); - //共有キーと初期化ベクタを決定 - //パスワードをバイト配列にする + // 共有キーと初期化ベクタを決定 + // パスワードをバイト配列にする var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_"); - //共有キーと初期化ベクタを設定 + // 共有キーと初期化ベクタを設定 des.Key = ResizeBytesArray(bytesKey, des.Key.Length); des.IV = ResizeBytesArray(bytesKey, des.IV.Length); @@ -526,27 +539,27 @@ public static string EncryptString(string str) try { - //暗号化されたデータを書き出すためのMemoryStream + // 暗号化されたデータを書き出すためのMemoryStream msOut = new MemoryStream(); - //DES暗号化オブジェクトの作成 + // DES暗号化オブジェクトの作成 desdecrypt = des.CreateEncryptor(); - //書き込むためのCryptoStreamの作成 + // 書き込むためのCryptoStreamの作成 using var cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write); - //Disposeが重複して呼ばれないようにする + // Disposeが重複して呼ばれないようにする var msTmp = msOut; msOut = null; desdecrypt = null; - //書き込む + // 書き込む cryptStream.Write(bytesIn, 0, bytesIn.Length); cryptStream.FlushFinalBlock(); - //暗号化されたデータを取得 + // 暗号化されたデータを取得 var bytesOut = msTmp.ToArray(); - //Base64で文字列に変更して結果を返す + // Base64で文字列に変更して結果を返す return Convert.ToBase64String(bytesOut); } finally @@ -560,17 +573,17 @@ public static string DecryptString(string str) { if (MyCommon.IsNullOrEmpty(str)) return ""; - //DESCryptoServiceProviderオブジェクトの作成 + // DESCryptoServiceProviderオブジェクトの作成 using var des = new DESCryptoServiceProvider(); - //共有キーと初期化ベクタを決定 - //パスワードをバイト配列にする + // 共有キーと初期化ベクタを決定 + // パスワードをバイト配列にする var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_"); - //共有キーと初期化ベクタを設定 + // 共有キーと初期化ベクタを設定 des.Key = ResizeBytesArray(bytesKey, des.Key.Length); des.IV = ResizeBytesArray(bytesKey, des.IV.Length); - //Base64で文字列をバイト配列に戻す + // Base64で文字列をバイト配列に戻す var bytesIn = Convert.FromBase64String(str); MemoryStream? msIn = null; @@ -579,24 +592,24 @@ public static string DecryptString(string str) try { - //暗号化されたデータを読み込むためのMemoryStream + // 暗号化されたデータを読み込むためのMemoryStream msIn = new MemoryStream(bytesIn); - //DES復号化オブジェクトの作成 + // DES復号化オブジェクトの作成 desdecrypt = des.CreateDecryptor(); - //読み込むためのCryptoStreamの作成 + // 読み込むためのCryptoStreamの作成 cryptStreem = new CryptoStream(msIn, desdecrypt, CryptoStreamMode.Read); - //Disposeが重複して呼ばれないようにする + // Disposeが重複して呼ばれないようにする msIn = null; desdecrypt = null; - //復号化されたデータを取得するためのStreamReader + // 復号化されたデータを取得するためのStreamReader using var srOut = new StreamReader(cryptStreem, Encoding.UTF8); - //Disposeが重複して呼ばれないようにする + // Disposeが重複して呼ばれないようにする cryptStreem = null; - //復号化されたデータを取得する + // 復号化されたデータを取得する var result = srOut.ReadToEnd(); return result; @@ -640,14 +653,14 @@ public static byte[] ResizeBytesArray(byte[] bytes, public enum TabUsageType { Undefined = 0, - Home = 1, //Unique - Mentions = 2, //Unique - DirectMessage = 4, //Unique - Favorites = 8, //Unique + Home = 1, // Unique + Mentions = 2, // Unique + DirectMessage = 4, // Unique + Favorites = 8, // Unique UserDefined = 16, - LocalQuery = 32, //Pin(no save/no save query/distribute/no update(normal update)) - Profile = 64, //Pin(save/no distribute/manual update) - PublicSearch = 128, //Pin(save/no distribute/auto update) + LocalQuery = 32, // Pin(no save/no save query/distribute/no update(normal update)) + Profile = 64, // Pin(save/no distribute/manual update) + PublicSearch = 128, // Pin(save/no distribute/auto update) Lists = 256, Related = 512, UserTimeline = 1024, @@ -655,7 +668,7 @@ public enum TabUsageType SearchResults = 4096, } - public static TwitterApiStatus TwitterApiInfo = new TwitterApiStatus(); + public static TwitterApiStatus TwitterApiInfo = new(); public static bool IsAnimatedGif(string filename) { @@ -691,7 +704,8 @@ public static bool IsAnimatedGif(string filename) public static DateTimeUtc DateTimeParse(string input) { - var formats = new[] { + var formats = new[] + { "ddd MMM dd HH:mm:ss zzzz yyyy", "ddd, d MMM yyyy HH:mm:ss zzzz", }; @@ -712,7 +726,7 @@ public static T CreateDataFromJson(string content) { UseSimpleDictionaryFormat = true, }; - return (T)((new DataContractJsonSerializer(typeof(T), settings)).ReadObject(stream)); + return (T)new DataContractJsonSerializer(typeof(T), settings).ReadObject(stream); } public static bool IsNetworkAvailable() @@ -721,7 +735,7 @@ public static bool IsNetworkAvailable() { return NetworkInterface.GetIsNetworkAvailable(); } - catch(Exception) + catch (Exception) { return false; } @@ -729,10 +743,11 @@ public static bool IsNetworkAvailable() public static bool IsValidEmail(string strIn) { + var pattern = @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" + + @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$"; + // Return true if strIn is in valid e-mail format. - return Regex.IsMatch(strIn, - @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" + - @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$"); + return Regex.IsMatch(strIn, pattern); } /// @@ -741,9 +756,9 @@ public static bool IsValidEmail(string strIn) /// 状態を調べるキー /// で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。 public static bool IsKeyDown(params Keys[] keys) - => MyCommon._IsKeyDown(Control.ModifierKeys, keys); + => MyCommon.IsKeyDownInternal(Control.ModifierKeys, keys); - internal static bool _IsKeyDown(Keys modifierKeys, Keys[] targetKeys) + internal static bool IsKeyDownInternal(Keys modifierKeys, Keys[] targetKeys) { foreach (var key in targetKeys) { @@ -857,7 +872,7 @@ public static string BuildQueryString(IEnumerable> // .NET 4.5+: Reserved characters のうち、Uriクラスによってエスケープ強制解除されてしまうものも最初から Unreserved として扱う private static readonly HashSet UnreservedChars = - new HashSet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:"); + new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:"); /// /// 2バイト文字も考慮したクエリ用エンコード @@ -970,23 +985,23 @@ public static bool IsNullOrEmpty([NotNullWhen(false)] string? value) => string.IsNullOrEmpty(value); public static Task OpenInBrowserAsync(IWin32Window? owner, string url) - => MyCommon.OpenInBrowserAsync(owner, SettingManager.Local.BrowserPath, url); + => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, url); - public static Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, string url) + public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, string url) { - return Task.Run(() => + try { - try + await Task.Run(() => { var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPath, url); Process.Start(startInfo); - } - catch (Win32Exception ex) - { - var message = string.Format(Properties.Resources.BrowserStartFailed, ex.ErrorCode); - MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - }); + }); + } + catch (Win32Exception ex) + { + var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message); + MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); + } } public static ProcessStartInfo CreateBrowserProcessStartInfo(string? browserPathWithArgs, string url) @@ -1000,7 +1015,7 @@ public static ProcessStartInfo CreateBrowserProcessStartInfo(string? browserPath }; } - int quoteEnd = -1; + var quoteEnd = -1; if (browserPathWithArgs.StartsWith("\"", StringComparison.Ordinal)) quoteEnd = browserPathWithArgs.IndexOf("\"", 1, StringComparison.Ordinal); @@ -1025,8 +1040,38 @@ public static ProcessStartInfo CreateBrowserProcessStartInfo(string? browserPath FileName = browserPath, Arguments = args, UseShellExecute = false, - }; } + + public static IEnumerable<(int Start, int End)> ToRangeChunk(IEnumerable values) + { + var start = -1; + var end = -1; + + foreach (var value in values.OrderBy(x => x)) + { + if (start == -1) + { + start = value; + end = value; + } + else + { + if (value == end + 1) + { + end = value; + } + else + { + yield return (start, end); + start = value; + end = value; + } + } + } + + if (start != -1) + yield return (start, end); + } } } diff --git a/OpenTween/MyLists.cs b/OpenTween/MyLists.cs index 5a255d0ec..80fba4739 100644 --- a/OpenTween/MyLists.cs +++ b/OpenTween/MyLists.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -36,8 +36,8 @@ using System.Threading.Tasks; using System.Windows.Forms; using OpenTween.Api; -using OpenTween.Connection; using OpenTween.Api.DataModel; +using OpenTween.Connection; namespace OpenTween { @@ -178,7 +178,7 @@ private void ListsCheckedListBox_MouseDown(object sender, MouseEventArgs e) switch (e.Button) { case MouseButtons.Left: - //項目が無い部分をクリックしても、選択されている項目のチェック状態が変更されてしまうので、その対策 + // 項目が無い部分をクリックしても、選択されている項目のチェック状態が変更されてしまうので、その対策 for (var index = 0; index < this.ListsCheckedListBox.Items.Count; index++) { if (this.ListsCheckedListBox.GetItemRectangle(index).Contains(e.Location)) @@ -187,7 +187,7 @@ private void ListsCheckedListBox_MouseDown(object sender, MouseEventArgs e) this.ListsCheckedListBox.SelectedItem = null; break; case MouseButtons.Right: - //コンテキストメニューの項目実行時にSelectedItemプロパティを利用出来るように + // コンテキストメニューの項目実行時にSelectedItemプロパティを利用出来るように for (var index = 0; index < this.ListsCheckedListBox.Items.Count; index++) { if (this.ListsCheckedListBox.GetItemRectangle(index).Contains(e.Location)) @@ -228,7 +228,9 @@ private async void MenuItemReload_Click(object sender, EventArgs e) { await this.RefreshListBox(); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + } catch (WebApiException ex) { MessageBox.Show($"Failed to get lists. ({ex.Message})"); @@ -244,7 +246,9 @@ private async void ListRefreshButton_Click(object sender, EventArgs e) { await this.RefreshListBox(); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + } catch (WebApiException ex) { MessageBox.Show($"Failed to get lists. ({ex.Message})"); diff --git a/OpenTween/NativeMethods.cs b/OpenTween/NativeMethods.cs index 6d972c62c..086a0fb55 100644 --- a/OpenTween/NativeMethods.cs +++ b/OpenTween/NativeMethods.cs @@ -7,19 +7,19 @@ // (c) 2011 Egtra (@egtra) // (c) 2014 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General public License along // with this program. if not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -33,9 +33,9 @@ using System.Linq; using System.Net; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Windows.Forms; -using System.Text; using OpenTween.Connection; namespace OpenTween @@ -44,14 +44,14 @@ internal static class NativeMethods { // 指定されたウィンドウへ、指定されたメッセージを送信します [DllImport("user32.dll")] - private extern static IntPtr SendMessage( + private static extern IntPtr SendMessage( IntPtr hwnd, SendMessageType wMsg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] - private extern static IntPtr SendMessage( + private static extern IntPtr SendMessage( IntPtr hwnd, SendMessageType wMsg, IntPtr wParam, @@ -60,16 +60,16 @@ private extern static IntPtr SendMessage( // SendMessageで送信するメッセージ private enum SendMessageType : uint { - WM_SETREDRAW = 0x000B, //再描画を許可するかを設定 - WM_USER = 0x400, //ユーザー定義メッセージ + WM_SETREDRAW = 0x000B, // 再描画を許可するかを設定 + WM_USER = 0x400, // ユーザー定義メッセージ - TCM_FIRST = 0x1300, //タブコントロールメッセージ - TCM_SETMINTABWIDTH = TCM_FIRST + 49, //タブアイテムの最小幅を設定 + TCM_FIRST = 0x1300, // タブコントロールメッセージ + TCM_SETMINTABWIDTH = TCM_FIRST + 49, // タブアイテムの最小幅を設定 - LVM_FIRST = 0x1000, //リストビューメッセージ - LVM_SETITEMSTATE = LVM_FIRST + 43, //アイテムの状態を設定 - LVM_GETSELECTIONMARK = LVM_FIRST + 66, //複数選択時の起点になるアイテムの位置を取得 - LVM_SETSELECTIONMARK = LVM_FIRST + 67, //複数選択時の起点になるアイテムを設定 + LVM_FIRST = 0x1000, // リストビューメッセージ + LVM_SETITEMSTATE = LVM_FIRST + 43, // アイテムの状態を設定 + LVM_GETSELECTIONMARK = LVM_FIRST + 66, // 複数選択時の起点になるアイテムの位置を取得 + LVM_SETSELECTIONMARK = LVM_FIRST + 67, // 複数選択時の起点になるアイテムを設定 } /// @@ -174,13 +174,13 @@ public enum FlashSpecification : uint FlashTimer = FLASHW_TIMER, FlashTimerNoForeground = FLASHW_TIMERNOFG, } - /// http://www.atmarkit.co.jp/fdotnet/dotnettips/723flashwindow/flashwindow.html + + // http://www.atmarkit.co.jp/fdotnet/dotnettips/723flashwindow/flashwindow.html [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FlashWindowEx( ref FLASHWINFO FWInfo); - private struct FLASHWINFO { public int cbSize; // FLASHWINFO構造体のサイズ @@ -230,7 +230,7 @@ private static extern bool SystemParametersInfo( int intWinIniFlag); // returns non-zero value if function succeeds - //スクリーンセーバーが起動中かを取得する定数 + // スクリーンセーバーが起動中かを取得する定数 private const int SPI_GETSCREENSAVERRUNNING = 0x0072; public static bool IsScreenSaverRunning() @@ -243,16 +243,19 @@ public static bool IsScreenSaverRunning() #region "グローバルフック" [DllImport("user32")] - private static extern int RegisterHotKey(IntPtr hwnd, int id, - int fsModifiers, int vk); + private static extern int RegisterHotKey(IntPtr hwnd, int id, int fsModifiers, int vk); + [DllImport("user32")] private static extern int UnregisterHotKey(IntPtr hwnd, int id); + [DllImport("kernel32", CharSet = CharSet.Auto, BestFitMapping = false, ThrowOnUnmappableChar = true)] private static extern ushort GlobalAddAtom([MarshalAs(UnmanagedType.LPTStr)] string lpString); + [DllImport("kernel32")] private static extern ushort GlobalDeleteAtom(ushort nAtom); private static int registerCount = 0; + // register a global hot key public static int RegisterGlobalHotKey(int hotkeyValue, int modifiers, Form targetForm) { @@ -342,7 +345,7 @@ private static void RefreshProxySettings(string? strProxy) } else if (strProxy == null) { - //IE Default + // IE Default var p = WebRequest.GetSystemWebProxy(); if (p.IsBypassed(new Uri("http://www.google.com/"))) { @@ -417,7 +420,7 @@ private enum ScrollInfoMask SIF_POS = 0x4, SIF_DISABLENOSCROLL = 0x8, SIF_TRACKPOS = 0x10, - SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS), + SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS, } [DllImport("user32.dll")] @@ -441,7 +444,7 @@ public static int GetScrollPosition(Control control, ScrollBarDirection directio [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint procId); - + [return: MarshalAs(UnmanagedType.Bool)] private delegate bool EnumWindowCallback(IntPtr hWnd, int lParam); @@ -465,29 +468,31 @@ public static IntPtr GetWindowHandle(uint pid, string searchWindowTitle) { var foundHwnd = IntPtr.Zero; - EnumWindows((hWnd, lParam) => - { - GetWindowThreadProcessId(hWnd, out var procId); - - if (procId == pid) + EnumWindows( + (hWnd, lParam) => { - var windowTitleLen = GetWindowTextLength(hWnd); + GetWindowThreadProcessId(hWnd, out var procId); - if (windowTitleLen > 0) + if (procId == pid) { - var windowTitle = new StringBuilder(windowTitleLen + 1); - GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + var windowTitleLen = GetWindowTextLength(hWnd); - if (windowTitle.ToString().Contains(searchWindowTitle)) + if (windowTitleLen > 0) { - foundHwnd = hWnd; - return false; + var windowTitle = new StringBuilder(windowTitleLen + 1); + GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + + if (windowTitle.ToString().Contains(searchWindowTitle)) + { + foundHwnd = hWnd; + return false; + } } } - } - return true; - }, IntPtr.Zero); + return true; + }, + IntPtr.Zero); return foundHwnd; } diff --git a/OpenTween/nicoms.cs b/OpenTween/Nicoms.cs similarity index 94% rename from OpenTween/nicoms.cs rename to OpenTween/Nicoms.cs index 6a23a2ae5..3cac42b13 100644 --- a/OpenTween/nicoms.cs +++ b/OpenTween/Nicoms.cs @@ -30,9 +30,9 @@ namespace OpenTween { - public static class nicoms + public static class Nicoms { - private static readonly string[] _nicovideo = + private static readonly string[] Nicovideo = { "www.nicovideo.jp/watch/", "live.nicovideo.jp/watch/", @@ -50,7 +50,7 @@ public static class nicoms public static string Shorten(string url) { - //整形(http(s)://を削除) + // 整形(http(s)://を削除) if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) { url = url.Substring(7); @@ -64,7 +64,7 @@ public static string Shorten(string url) return url; } - foreach (var nv in _nicovideo) + foreach (var nv in Nicovideo) { if (url.StartsWith(nv, StringComparison.Ordinal)) return string.Format("{0}{1}", "https://nico.ms/", url.Substring(nv.Length)); diff --git a/OpenTween/NullableAttributes.cs b/OpenTween/NullableAttributes.cs index da108132a..f99ce63f4 100644 --- a/OpenTween/NullableAttributes.cs +++ b/OpenTween/NullableAttributes.cs @@ -20,6 +20,7 @@ // Boston, MA 02110-1301, USA. #nullable enable +#pragma warning disable SA1649 namespace System.Diagnostics.CodeAnalysis { @@ -40,4 +41,13 @@ internal sealed class MaybeNullWhenAttribute : Attribute public MaybeNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue; } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] + internal sealed class MemberNotNullAttribute : Attribute + { + public string[] Members { get; } + + public MemberNotNullAttribute(params string[] members) + => this.Members = members; + } } diff --git a/OpenTween/OTBaseForm.cs b/OpenTween/OTBaseForm.cs index 3f6575268..982057193 100644 --- a/OpenTween/OTBaseForm.cs +++ b/OpenTween/OTBaseForm.cs @@ -54,12 +54,9 @@ public class OTBaseForm : Form /// public SizeF CurrentScaleFactor { get; private set; } - private readonly SynchronizationContext synchronizationContext; - protected OTBaseForm() { this.CurrentScaleFactor = new SizeF(1.0f, 1.0f); - this.synchronizationContext = SynchronizationContext.Current; this.Load += (o, e) => { @@ -69,37 +66,6 @@ protected OTBaseForm() }; } - public Task InvokeAsync(Action x) - => this.InvokeAsync(new Func(() => { x(); return 0; })); - - public Task InvokeAsync(Func x) - => this.InvokeAsync(x).Unwrap(); - - public Task InvokeAsync(Func> x) - => this.InvokeAsync>(x).Unwrap(); - - /// - /// メソッドのTask版みたいなやつ - /// - public Task InvokeAsync(Func x) - { - var tcs = new TaskCompletionSource(); - this.synchronizationContext.Post(_ => - { - try - { - var ret = x(); - tcs.SetResult(ret); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - }, null); - - return tcs.Task; - } - /// /// source で指定されたフォントのスタイルを維持しつつ GlobalFont に置き換えた Font を返します /// diff --git a/OpenTween/OTPictureBox.cs b/OpenTween/OTPictureBox.cs index de7283933..5ebb65140 100644 --- a/OpenTween/OTPictureBox.cs +++ b/OpenTween/OTPictureBox.cs @@ -23,17 +23,17 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows.Forms; using System.ComponentModel; using System.Drawing; -using System.Threading.Tasks; -using System.Threading; +using System.IO; +using System.Linq; using System.Net; using System.Net.Http; -using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; using OpenTween.Thumbnail; namespace OpenTween @@ -53,6 +53,7 @@ public class OTPictureBox : PictureBox this.RestoreSizeMode(); } } + private MemoryImage? memoryImage; [Localizable(true)] @@ -102,13 +103,14 @@ private void RestoreSizeMode() /// private int currentImageTaskId = 0; - public async Task SetImageFromTask(Func> imageTask) + public async Task SetImageFromTask(Func> imageTask, bool useStatusImage = true) { var id = Interlocked.Increment(ref this.currentImageTaskId); try { - this.ShowInitialImage(); + if (useStatusImage) + this.ShowInitialImage(); var image = await imageTask(); @@ -117,18 +119,30 @@ public async Task SetImageFromTask(Func> imageTask) } catch (Exception) { - if (id == this.currentImageTaskId) + if (id == this.currentImageTaskId && useStatusImage) this.ShowErrorImage(); try { throw; } - catch (HttpRequestException) { } - catch (InvalidImageException) { } - catch (OperationCanceledException) { } - catch (ObjectDisposedException) { } - catch (WebException) { } - catch (IOException) { } + catch (HttpRequestException) + { + } + catch (InvalidImageException) + { + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (WebException) + { + } + catch (IOException) + { + } } } @@ -139,7 +153,7 @@ protected override void OnPaint(PaintEventArgs pe) base.OnPaint(pe); // 動画なら再生ボタンを上から描画 - DrawPlayableMark(pe); + this.DrawPlayableMark(pe); } catch (ExternalException) { diff --git a/OpenTween/OTSplitContainer.cs b/OpenTween/OTSplitContainer.cs index dd9621561..60b514cf6 100644 --- a/OpenTween/OTSplitContainer.cs +++ b/OpenTween/OTSplitContainer.cs @@ -22,10 +22,10 @@ #nullable enable using System; -using System.Windows.Forms; using System.Collections.Generic; -using System.Linq; using System.ComponentModel; +using System.Linq; +using System.Windows.Forms; namespace OpenTween { @@ -39,12 +39,12 @@ public class OTSplitContainer : SplitContainer [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsPanelInverted { - get => this._isPanelInverted; + get => this.isPanelInverted; set { - if (_isPanelInverted == value) + if (this.isPanelInverted == value) return; - _isPanelInverted = value; + this.isPanelInverted = value; // Panel1 と Panel2 の中身を入れ替え using (ControlTransaction.Layout(this, false)) @@ -60,22 +60,18 @@ public bool IsPanelInverted if (base.FixedPanel != FixedPanel.None) base.FixedPanel = (base.FixedPanel == FixedPanel.Panel1) ? FixedPanel.Panel2 : FixedPanel.Panel1; - base.SplitterDistance = SplitterTotalWidth - (base.SplitterDistance + SplitterWidth); + base.SplitterDistance = this.SplitterTotalWidth - (base.SplitterDistance + this.SplitterWidth); - var tmpMinSize = base.Panel1MinSize; - base.Panel1MinSize = base.Panel2MinSize; - base.Panel2MinSize = tmpMinSize; - - var tmpCollapsed = base.Panel1Collapsed; - base.Panel1Collapsed = base.Panel2Collapsed; - base.Panel2Collapsed = tmpCollapsed; + (base.Panel2MinSize, base.Panel1MinSize) = (base.Panel1MinSize, base.Panel2MinSize); + (base.Panel2Collapsed, base.Panel1Collapsed) = (base.Panel1Collapsed, base.Panel2Collapsed); base.Panel1.Controls.AddRange(cont2.ToArray()); base.Panel2.Controls.AddRange(cont1.ToArray()); } } } - private bool _isPanelInverted = false; + + private bool isPanelInverted = false; /// /// SplitContainer.Orientation プロパティの設定に応じて、スプリッタが移動する方向の幅を返す。 @@ -87,13 +83,13 @@ private int SplitterTotalWidth /// IsPanelInverted プロパティの設定に応じて、SplitContainer.Panel1 または SplitContainer.Panel2 を返す。 /// public new SplitterPanel Panel1 - => IsPanelInverted ? base.Panel2 : base.Panel1; + => this.IsPanelInverted ? base.Panel2 : base.Panel1; /// /// IsPanelInverted プロパティの設定に応じて、SplitContainer.Panel1 または SplitContainer.Panel2 を返す。 /// public new SplitterPanel Panel2 - => IsPanelInverted ? base.Panel1 : base.Panel2; + => this.IsPanelInverted ? base.Panel1 : base.Panel2; /// /// IsPanelInverted プロパティの設定に応じて、SplitContainer.FixedPanel を返す。 @@ -102,14 +98,15 @@ private int SplitterTotalWidth { get { - if (base.FixedPanel != FixedPanel.None && IsPanelInverted) + if (base.FixedPanel != FixedPanel.None && this.IsPanelInverted) return (base.FixedPanel == FixedPanel.Panel1) ? FixedPanel.Panel2 : FixedPanel.Panel1; else return base.FixedPanel; } + set { - if (value != FixedPanel.None && IsPanelInverted) + if (value != FixedPanel.None && this.IsPanelInverted) base.FixedPanel = (value == FixedPanel.Panel1) ? FixedPanel.Panel2 : FixedPanel.Panel1; else base.FixedPanel = value; @@ -123,15 +120,16 @@ private int SplitterTotalWidth { get { - if (IsPanelInverted) - return SplitterTotalWidth - (base.SplitterDistance + SplitterWidth); + if (this.IsPanelInverted) + return this.SplitterTotalWidth - (base.SplitterDistance + this.SplitterWidth); else return base.SplitterDistance; } + set { - if (IsPanelInverted) - base.SplitterDistance = SplitterTotalWidth - (value + SplitterWidth); + if (this.IsPanelInverted) + base.SplitterDistance = this.SplitterTotalWidth - (value + this.SplitterWidth); else base.SplitterDistance = value; } @@ -142,10 +140,10 @@ private int SplitterTotalWidth /// public new int Panel1MinSize { - get => IsPanelInverted ? base.Panel2MinSize : base.Panel1MinSize; + get => this.IsPanelInverted ? base.Panel2MinSize : base.Panel1MinSize; set { - if (IsPanelInverted) + if (this.IsPanelInverted) base.Panel2MinSize = value; else base.Panel1MinSize = value; @@ -157,10 +155,10 @@ private int SplitterTotalWidth /// public new int Panel2MinSize { - get => IsPanelInverted ? base.Panel1MinSize : base.Panel2MinSize; + get => this.IsPanelInverted ? base.Panel1MinSize : base.Panel2MinSize; set { - if (IsPanelInverted) + if (this.IsPanelInverted) base.Panel1MinSize = value; else base.Panel2MinSize = value; @@ -172,10 +170,10 @@ private int SplitterTotalWidth /// public new bool Panel1Collapsed { - get => IsPanelInverted ? base.Panel2Collapsed : base.Panel1Collapsed; + get => this.IsPanelInverted ? base.Panel2Collapsed : base.Panel1Collapsed; set { - if (IsPanelInverted) + if (this.IsPanelInverted) base.Panel2Collapsed = value; else base.Panel1Collapsed = value; @@ -187,10 +185,10 @@ private int SplitterTotalWidth /// public new bool Panel2Collapsed { - get => IsPanelInverted ? base.Panel1Collapsed : base.Panel2Collapsed; + get => this.IsPanelInverted ? base.Panel1Collapsed : base.Panel2Collapsed; set { - if (IsPanelInverted) + if (this.IsPanelInverted) base.Panel1Collapsed = value; else base.Panel2Collapsed = value; diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj index b0ec82a31..cb4691fdf 100644 --- a/OpenTween/OpenTween.csproj +++ b/OpenTween/OpenTween.csproj @@ -9,7 +9,7 @@ OpenTween OpenTween v4.7.2 - 8.0 + 10.0 512 true $(MSBuildProjectDirectory)=. @@ -38,7 +38,7 @@ false - OpenTween.MyApplication + OpenTween.ApplicationEvents Resources\4b.ico @@ -73,11 +73,13 @@ + + Code @@ -90,6 +92,7 @@ + @@ -98,10 +101,18 @@ + + + Code + + + + + Form @@ -122,8 +133,13 @@ AuthDialog.cs + + + + + @@ -190,6 +206,9 @@ + + + UserControl @@ -386,7 +405,7 @@ UpdateDialog.cs - + Form @@ -398,7 +417,6 @@ - Form @@ -406,7 +424,7 @@ MyLists.cs - + Form @@ -458,7 +476,7 @@ - + @@ -724,6 +742,9 @@ LICENSE.LGPL-3 Always + + stylecop.json + Designer @@ -746,6 +767,21 @@ + + + + + + + + + + + + + + + @@ -757,6 +793,11 @@ 1.0.1 + + 1.2.0-beta.406 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + " - + "
";
-        private const string detailHtmlFormatFooterMono = "
"; - private const string detailHtmlFormatHeaderColor = - "" + private const string DetailHtmlFormatHead = + "" + "" - + "

"; - private const string detailHtmlFormatFooterColor = "

"; - private string detailHtmlFormatHeader = null!; - private string detailHtmlFormatFooter = null!; + + ""; + + private const string DetailHtmlFormatTemplateMono = + $"{DetailHtmlFormatHead}
%CONTENT_HTML%
"; - private bool _myStatusError = false; - private bool _myStatusOnline = false; + private const string DetailHtmlFormatTemplateNormal = + $"{DetailHtmlFormatHead}

%CONTENT_HTML%

"; + + private string detailHtmlFormatPreparedTemplate = null!; + + private bool myStatusError = false; + private bool myStatusOnline = false; private bool soundfileListup = false; - private FormWindowState _formWindowState = FormWindowState.Normal; // フォームの状態保存用 通知領域からアイコンをクリックして復帰した際に使用する + private FormWindowState formWindowState = FormWindowState.Normal; // フォームの状態保存用 通知領域からアイコンをクリックして復帰した際に使用する - //twitter解析部 - private readonly TwitterApi twitterApi = new TwitterApi(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret); - private Twitter tw = null!; + // 設定ファイル + private readonly SettingManager settings; - //Growl呼び出し部 - private readonly GrowlHelper gh = new GrowlHelper(ApplicationSettings.ApplicationName); + // twitter解析部 + private readonly Twitter tw; - //サブ画面インスタンス + // Growl呼び出し部 + private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName); + + // サブ画面インスタンス /// 検索画面インスタンス - internal SearchWordDialog SearchDialog = new SearchWordDialog(); + internal SearchWordDialog SearchDialog = new(); - private readonly OpenURL UrlDialog = new OpenURL(); + private readonly OpenURL urlDialog = new(); /// @id補助 public AtIdSupplement AtIdSupl = null!; @@ -161,254 +148,96 @@ public partial class TweenMain : OTBaseForm public HashtagManage HashMgr = null!; - //表示フォント、色、アイコン - - /// 未読用フォント - private Font _fntUnread = null!; - - /// 未読用文字色 - private Color _clUnread; - - /// 既読用フォント - private Font _fntReaded = null!; - - /// 既読用文字色 - private Color _clReaded; - - /// Fav用文字色 - private Color _clFav; - - /// 片思い用文字色 - private Color _clOWL; - - /// Retweet用文字色 - private Color _clRetweet; - - /// 選択中の行用文字色 - private readonly Color _clHighLight = Color.FromKnownColor(KnownColor.HighlightText); - - /// 発言詳細部用フォント - private Font _fntDetail = null!; - - /// 発言詳細部用色 - private Color _clDetail; - - /// 発言詳細部用リンク文字色 - private Color _clDetailLink; - - /// 発言詳細部用背景色 - private Color _clDetailBackcolor; - - /// 自分の発言用背景色 - private Color _clSelf; - - /// 自分宛返信用背景色 - private Color _clAtSelf; - - /// 選択発言者の他の発言用背景色 - private Color _clTarget; - - /// 選択発言中の返信先用背景色 - private Color _clAtTarget; - - /// 選択発言者への返信発言用背景色 - private Color _clAtFromTarget; - - /// 選択発言の唯一@先 - private Color _clAtTo; - - /// リスト部通常発言背景色 - private Color _clListBackcolor; - - /// 入力欄背景色 - private Color _clInputBackcolor; - - /// 入力欄文字色 - private Color _clInputFont; - - /// 入力欄フォント - private Font _fntInputFont = null!; + // 表示フォント、色、アイコン + private ThemeManager themeManager; /// アイコン画像リスト - private ImageCache IconCache = null!; - - /// タスクトレイアイコン:通常時 (At.ico) - private Icon NIconAt = null!; - - /// タスクトレイアイコン:通信エラー時 (AtRed.ico) - private Icon NIconAtRed = null!; + private readonly ImageCache iconCache; - /// タスクトレイアイコン:オフライン時 (AtSmoke.ico) - private Icon NIconAtSmoke = null!; + private readonly IconAssetsManager iconAssets; - /// タスクトレイアイコン:更新中 (Refresh.ico) - private Icon[] NIconRefresh = new Icon[4]; - - /// 未読のあるタブ用アイコン (Tab.ico) - private Icon TabIcon = null!; - - /// 画面左上のアイコン (Main.ico) - private Icon MainIcon = null!; - - private Icon ReplyIcon = null!; - private Icon ReplyIconBlink = null!; - - private readonly ImageList _listViewImageList = new ImageList(); //ListViewItemの高さ変更用 - - private PostClass? _anchorPost; - private bool _anchorFlag; //true:関連発言移動中(関連移動以外のオペレーションをするとfalseへ。trueだとリスト背景色をアンカー発言選択中として描画) + private readonly ThumbnailGenerator thumbGenerator; /// 発言履歴 - private readonly List _history = new List(); + private readonly List history = new(); /// 発言履歴カレントインデックス - private int _hisIdx; + private int hisIdx; - //発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない) + // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない) /// リプライ先のステータスID・スクリーン名 private (long StatusId, string ScreenName)? inReplyTo = null; - //時速表示用 - private readonly List _postTimestamps = new List(); - private readonly List _favTimestamps = new List(); + // 時速表示用 + private readonly List postTimestamps = new(); + private readonly List favTimestamps = new(); // 以下DrawItem関連 - private readonly SolidBrush _brsHighLight = new SolidBrush(Color.FromKnownColor(KnownColor.Highlight)); - private SolidBrush _brsBackColorMine = null!; - private SolidBrush _brsBackColorAt = null!; - private SolidBrush _brsBackColorYou = null!; - private SolidBrush _brsBackColorAtYou = null!; - private SolidBrush _brsBackColorAtFromTarget = null!; - private SolidBrush _brsBackColorAtTo = null!; - private SolidBrush _brsBackColorNone = null!; - - /// Listにフォーカスないときの選択行の背景色 - private readonly SolidBrush _brsDeactiveSelection = new SolidBrush(Color.FromKnownColor(KnownColor.ButtonFace)); - - private readonly StringFormat sfTab = new StringFormat(); + private readonly StringFormat sfTab = new(); ////////////////////////////////////////////////////////////////////////////////////////////////////////// - private TabInformations _statuses = null!; - /// - /// 現在表示している発言一覧の に対するキャッシュ - /// - /// - /// キャッシュクリアのために null が代入されることがあるため、 - /// 使用する場合には に対して直接メソッド等を呼び出さずに - /// 一旦ローカル変数に代入してから参照すること。 - /// - private ListViewItemCache? _listItemCache = null; - - internal class ListViewItemCache - { - /// アイテムをキャッシュする対象の - public ListView TargetList { get; set; } = null!; - - /// キャッシュする範囲の開始インデックス - public int StartIndex { get; set; } - - /// キャッシュする範囲の終了インデックス - public int EndIndex { get; set; } + /// 発言保持クラス + private readonly TabInformations statuses; - /// キャッシュされた範囲に対応する の組 - public (ListViewItem, PostClass)[] Cache { get; set; } = null!; + private TimelineListViewCache? listCache; + private TimelineListViewDrawer? listDrawer; + private readonly Dictionary listViewState = new(); - /// キャッシュされたアイテムの件数 - public int Count - => this.EndIndex - this.StartIndex + 1; + private bool isColumnChanged = false; - /// 指定されたインデックスがキャッシュの範囲内であるか判定します - /// がキャッシュの範囲内であれば true、それ以外は false - public bool Contains(int index) - => index >= this.StartIndex && index <= this.EndIndex; - - /// 指定されたインデックスの範囲が全てキャッシュの範囲内であるか判定します - /// から の範囲が全てキャッシュの範囲内であれば true、それ以外は false - public bool IsSupersetOf(int rangeStart, int rangeEnd) - => rangeStart >= this.StartIndex && rangeEnd <= this.EndIndex; - - /// 指定されたインデックスの をキャッシュから取得することを試みます - /// 取得に成功すれば true、それ以外は false - public bool TryGetValue(int index, [NotNullWhen(true)] out ListViewItem? item, [NotNullWhen(true)] out PostClass? post) - { - if (this.Contains(index)) - { - (item, post) = this.Cache[index - this.StartIndex]; - return true; - } - else - { - item = null; - post = null; - return false; - } - } - } - - private bool _isColumnChanged = false; - - private const int MAX_WORKER_THREADS = 20; - private readonly SemaphoreSlim workerSemaphore = new SemaphoreSlim(MAX_WORKER_THREADS); - private readonly CancellationTokenSource workerCts = new CancellationTokenSource(); + private const int MaxWorderThreads = 20; + private readonly SemaphoreSlim workerSemaphore = new(MaxWorderThreads); + private readonly CancellationTokenSource workerCts = new(); private readonly IProgress workerProgress = null!; - private int UnreadCounter = -1; - private int UnreadAtCounter = -1; + private int unreadCounter = -1; + private int unreadAtCounter = -1; - private readonly string[] ColumnOrgText = new string[9]; - private readonly string[] ColumnText = new string[9]; + private readonly string[] columnOrgText = new string[9]; + private readonly string[] columnText = new string[9]; - private bool _DoFavRetweetFlags = false; + private bool doFavRetweetFlags = false; ////////////////////////////////////////////////////////////////////////////////////////////////////////// - private readonly TimelineScheduler timelineScheduler = new TimelineScheduler(); - private DebounceTimer selectionDebouncer = null!; - private DebounceTimer saveConfigDebouncer = null!; + private readonly TimelineScheduler timelineScheduler = new(); + private readonly DebounceTimer selectionDebouncer; + private readonly DebounceTimer saveConfigDebouncer; - private string recommendedStatusFooter = null!; + private readonly string recommendedStatusFooter; private bool urlMultibyteSplit = false; private bool preventSmsCommand = true; // URL短縮のUndo用 - private struct urlUndo - { - public string Before; - public string After; - } + private readonly record struct UrlUndo( + string Before, + string After + ); - private List? urlUndoBuffer = null; + private List? urlUndoBuffer = null; - private readonly struct ReplyChain - { - public readonly long OriginalId; - public readonly long InReplyToId; - public readonly TabModel OriginalTab; - - public ReplyChain(long originalId, long inReplyToId, TabModel originalTab) - { - this.OriginalId = originalId; - this.InReplyToId = inReplyToId; - this.OriginalTab = originalTab; - } - } + private readonly record struct ReplyChain( + long OriginalId, + long InReplyToId, + TabModel OriginalTab + ); /// [, ]でのリプライ移動の履歴 private Stack? replyChains; /// ポスト選択履歴 - private readonly Stack<(TabModel, PostClass?)> selectPostChains = new Stack<(TabModel, PostClass?)>(); + private readonly Stack<(TabModel, PostClass?)> selectPostChains = new(); public TabModel CurrentTab - => this._statuses.SelectedTab; + => this.statuses.SelectedTab; public string CurrentTabName - => this._statuses.SelectedTabName; + => this.statuses.SelectedTabName; public TabPage CurrentTabPage - => this.ListTab.TabPages[this._statuses.Tabs.IndexOf(this.CurrentTabName)]; + => this.ListTab.TabPages[this.statuses.Tabs.IndexOf(this.CurrentTabName)]; public DetailsListView CurrentListView => (DetailsListView)this.CurrentTabPage.Tag; @@ -416,6 +245,9 @@ public DetailsListView CurrentListView public PostClass? CurrentPost => this.CurrentTab.SelectedPost; + public bool Use2ColumnsMode + => this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2; + /// 検索処理タイプ internal enum SEARCHTYPE { @@ -424,29 +256,17 @@ internal enum SEARCHTYPE PrevSearch, } - private class StatusTextHistory - { - public string status = ""; - public (long StatusId, string ScreenName)? inReplyTo = null; - - /// 画像投稿サービス名 - public string imageService = ""; + private readonly record struct StatusTextHistory( + string Status, + (long StatusId, string ScreenName)? InReplyTo = null + ); - public IMediaItem[]? mediaItems = null; - public StatusTextHistory() - { - } - public StatusTextHistory(string status, (long StatusId, string ScreenName)? inReplyTo) - { - this.status = status; - this.inReplyTo = inReplyTo; - } - } + private readonly HookGlobalHotkey hookGlobalHotkey; private void TweenMain_Activated(object sender, EventArgs e) { - //画面がアクティブになったら、発言欄の背景色戻す - if (StatusText.Focused) + // 画面がアクティブになったら、発言欄の背景色戻す + if (this.StatusText.Focused) { this.StatusText_Enter(this.StatusText, System.EventArgs.Empty); } @@ -469,145 +289,27 @@ protected override void Dispose(bool disposing) { this.components?.Dispose(); - //後始末 - SearchDialog.Dispose(); - UrlDialog.Dispose(); - NIconAt?.Dispose(); - NIconAtRed?.Dispose(); - NIconAtSmoke?.Dispose(); - foreach (var iconRefresh in this.NIconRefresh) - { - iconRefresh?.Dispose(); - } - TabIcon?.Dispose(); - MainIcon?.Dispose(); - ReplyIcon?.Dispose(); - ReplyIconBlink?.Dispose(); - _listViewImageList.Dispose(); - _brsHighLight.Dispose(); - _brsBackColorMine?.Dispose(); - _brsBackColorAt?.Dispose(); - _brsBackColorYou?.Dispose(); - _brsBackColorAtYou?.Dispose(); - _brsBackColorAtFromTarget?.Dispose(); - _brsBackColorAtTo?.Dispose(); - _brsBackColorNone?.Dispose(); - _brsDeactiveSelection?.Dispose(); - //sf.Dispose(); - sfTab.Dispose(); + // 後始末 + this.SearchDialog.Dispose(); + this.urlDialog.Dispose(); + this.themeManager.Dispose(); + this.sfTab.Dispose(); this.timelineScheduler.Dispose(); this.workerCts.Cancel(); - - if (IconCache != null) - { - this.IconCache.CancelAsync(); - this.IconCache.Dispose(); - } - this.thumbnailTokenSource?.Dispose(); - this.twitterApi.Dispose(); - this._hookGlobalHotkey.Dispose(); + this.hookGlobalHotkey.Dispose(); } // 終了時にRemoveHandlerしておかないとメモリリークする // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx - Microsoft.Win32.SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; - Microsoft.Win32.SystemEvents.TimeChanged -= SystemEvents_TimeChanged; + Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged; + Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged; this.disposed = true; } - private void LoadIcons() - { - // Icons フォルダ以下のアイコンを読み込み(着せ替えアイコン対応) - var iconsDir = Path.Combine(Application.StartupPath, "Icons"); - - // ウィンドウ左上のアイコン - var iconMain = this.LoadIcon(Path.Combine(iconsDir, "MIcon.ico")); - - // タブ見出し未読表示アイコン - var iconTab = this.LoadIcon(Path.Combine(iconsDir, "Tab.ico")); - - // タスクトレイ: 通常時アイコン - var iconAt = this.LoadIcon(Path.Combine(iconsDir, "At.ico")); - - // タスクトレイ: エラー時アイコン - var iconAtRed = this.LoadIcon(Path.Combine(iconsDir, "AtRed.ico")); - - // タスクトレイ: オフライン時アイコン - var iconAtSmoke = this.LoadIcon(Path.Combine(iconsDir, "AtSmoke.ico")); - - // タスクトレイ: Reply通知アイコン (最大2枚でアニメーション可能) - var iconReply = this.LoadIcon(Path.Combine(iconsDir, "Reply.ico")); - var iconReplyBlink = this.LoadIcon(Path.Combine(iconsDir, "ReplyBlink.ico")); - - // タスクトレイ: 更新中アイコン (最大4枚でアニメーション可能) - var iconRefresh1 = this.LoadIcon(Path.Combine(iconsDir, "Refresh.ico")); - var iconRefresh2 = this.LoadIcon(Path.Combine(iconsDir, "Refresh2.ico")); - var iconRefresh3 = this.LoadIcon(Path.Combine(iconsDir, "Refresh3.ico")); - var iconRefresh4 = this.LoadIcon(Path.Combine(iconsDir, "Refresh4.ico")); - - // 読み込んだアイコンを設定 (不足するアイコンはデフォルトのものを設定) - - this.MainIcon = iconMain ?? Properties.Resources.MIcon; - this.TabIcon = iconTab ?? Properties.Resources.TabIcon; - this.NIconAt = iconAt ?? iconMain ?? Properties.Resources.At; - this.NIconAtRed = iconAtRed ?? Properties.Resources.AtRed; - this.NIconAtSmoke = iconAtSmoke ?? Properties.Resources.AtSmoke; - - if (iconReply != null && iconReplyBlink != null) - { - this.ReplyIcon = iconReply; - this.ReplyIconBlink = iconReplyBlink; - } - else - { - this.ReplyIcon = iconReply ?? iconReplyBlink ?? Properties.Resources.Reply; - this.ReplyIconBlink = this.NIconAt; - } - - if (iconRefresh1 == null) - { - this.NIconRefresh = new[] { - Properties.Resources.Refresh, Properties.Resources.Refresh2, - Properties.Resources.Refresh3, Properties.Resources.Refresh4, - }; - } - else if (iconRefresh2 == null) - { - this.NIconRefresh = new[] { iconRefresh1 }; - } - else if (iconRefresh3 == null) - { - this.NIconRefresh = new[] { iconRefresh1, iconRefresh2 }; - } - else if (iconRefresh4 == null) - { - this.NIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3 }; - } - else // iconRefresh1 から iconRefresh4 まで全て揃っている - { - this.NIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3, iconRefresh4 }; - } - } - - private Icon? LoadIcon(string filePath) - { - if (!File.Exists(filePath)) - return null; - - try - { - return new Icon(filePath); - } - catch (Exception) - { - return null; - } - } - private void InitColumns(ListView list, bool startup) { this.InitColumnText(); @@ -615,7 +317,7 @@ private void InitColumns(ListView list, bool startup) ColumnHeader[]? columns = null; try { - if (this._iconCol) + if (this.Use2ColumnsMode) { columns = new[] { @@ -623,15 +325,15 @@ private void InitColumns(ListView list, bool startup) new ColumnHeader(), // 本文 }; - columns[0].Text = this.ColumnText[0]; - columns[1].Text = this.ColumnText[2]; + columns[0].Text = this.columnText[0]; + columns[1].Text = this.columnText[2]; if (startup) { - var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / SettingManager.Local.ScaleDimension.Width; + var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; - columns[0].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width1); - columns[1].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width3); + columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]); + columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]); columns[0].DisplayIndex = 0; columns[1].DisplayIndex = 1; } @@ -661,31 +363,16 @@ private void InitColumns(ListView list, bool startup) }; foreach (var i in Enumerable.Range(0, columns.Length)) - columns[i].Text = this.ColumnText[i]; + columns[i].Text = this.columnText[i]; if (startup) { - var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / SettingManager.Local.ScaleDimension.Width; - - columns[0].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width1); - columns[1].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width2); - columns[2].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width3); - columns[3].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width4); - columns[4].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width5); - columns[5].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width6); - columns[6].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width7); - columns[7].Width = ScaleBy(widthScaleFactor, SettingManager.Local.Width8); - - var displayIndex = new[] { - SettingManager.Local.DisplayIndex1, SettingManager.Local.DisplayIndex2, - SettingManager.Local.DisplayIndex3, SettingManager.Local.DisplayIndex4, - SettingManager.Local.DisplayIndex5, SettingManager.Local.DisplayIndex6, - SettingManager.Local.DisplayIndex7, SettingManager.Local.DisplayIndex8 - }; + var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; - foreach (var i in Enumerable.Range(0, displayIndex.Length)) + foreach (var (column, index) in columns.WithIndex()) { - columns[i].DisplayIndex = displayIndex[i]; + column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]); + column.DisplayIndex = this.settings.Local.ColumnsOrder[index]; } } else @@ -716,25 +403,25 @@ private void InitColumns(ListView list, bool startup) private void InitColumnText() { - ColumnText[0] = ""; - ColumnText[1] = Properties.Resources.AddNewTabText2; - ColumnText[2] = Properties.Resources.AddNewTabText3; - ColumnText[3] = Properties.Resources.AddNewTabText4_2; - ColumnText[4] = Properties.Resources.AddNewTabText5; - ColumnText[5] = ""; - ColumnText[6] = ""; - ColumnText[7] = "Source"; - - ColumnOrgText[0] = ""; - ColumnOrgText[1] = Properties.Resources.AddNewTabText2; - ColumnOrgText[2] = Properties.Resources.AddNewTabText3; - ColumnOrgText[3] = Properties.Resources.AddNewTabText4_2; - ColumnOrgText[4] = Properties.Resources.AddNewTabText5; - ColumnOrgText[5] = ""; - ColumnOrgText[6] = ""; - ColumnOrgText[7] = "Source"; - - var c = this._statuses.SortMode switch + this.columnText[0] = ""; + this.columnText[1] = Properties.Resources.AddNewTabText2; + this.columnText[2] = Properties.Resources.AddNewTabText3; + this.columnText[3] = Properties.Resources.AddNewTabText4_2; + this.columnText[4] = Properties.Resources.AddNewTabText5; + this.columnText[5] = ""; + this.columnText[6] = ""; + this.columnText[7] = "Source"; + + this.columnOrgText[0] = ""; + this.columnOrgText[1] = Properties.Resources.AddNewTabText2; + this.columnOrgText[2] = Properties.Resources.AddNewTabText3; + this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2; + this.columnOrgText[4] = Properties.Resources.AddNewTabText5; + this.columnOrgText[5] = ""; + this.columnOrgText[6] = ""; + this.columnOrgText[7] = "Source"; + + var c = this.statuses.SortMode switch { ComparerMode.Nickname => 1, // ニックネーム ComparerMode.Data => 2, // 本文 @@ -744,174 +431,103 @@ private void InitColumnText() _ => 0, }; - if (_iconCol) + if (this.Use2ColumnsMode) { - if (_statuses.SortOrder == SortOrder.Descending) + if (this.statuses.SortOrder == SortOrder.Descending) { // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE - ColumnText[2] = ColumnOrgText[2] + "▾"; + this.columnText[2] = this.columnOrgText[2] + "▾"; } else { // U+25B4 BLACK UP-POINTING SMALL TRIANGLE - ColumnText[2] = ColumnOrgText[2] + "▴"; + this.columnText[2] = this.columnOrgText[2] + "▴"; } } else { - if (_statuses.SortOrder == SortOrder.Descending) + if (this.statuses.SortOrder == SortOrder.Descending) { // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE - ColumnText[c] = ColumnOrgText[c] + "▾"; + this.columnText[c] = this.columnOrgText[c] + "▾"; } else { // U+25B4 BLACK UP-POINTING SMALL TRIANGLE - ColumnText[c] = ColumnOrgText[c] + "▴"; + this.columnText[c] = this.columnOrgText[c] + "▴"; } } } - private void InitializeTraceFrag() - { -#if DEBUG - TraceOutToolStripMenuItem.Checked = true; - MyCommon.TraceFlag = true; -#endif - if (!MyCommon.FileVersion.EndsWith("0", StringComparison.Ordinal)) - { - TraceOutToolStripMenuItem.Checked = true; - MyCommon.TraceFlag = true; - } - } - - private void TweenMain_Load(object sender, EventArgs e) + public TweenMain( + SettingManager settingManager, + TabInformations tabInfo, + Twitter twitter, + ImageCache imageCache, + IconAssetsManager iconAssets, + ThumbnailGenerator thumbGenerator + ) { - _ignoreConfigSave = true; - this.Visible = false; - - if (MyApplication.StartupOptions.ContainsKey("d")) - MyCommon.TraceFlag = true; - - InitializeTraceFrag(); - - Microsoft.Win32.SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; - - Regex.CacheSize = 100; - - //発言保持クラス - _statuses = TabInformations.GetInstance(); + this.settings = settingManager; + this.statuses = tabInfo; + this.tw = twitter; + this.iconCache = imageCache; + this.iconAssets = iconAssets; + this.thumbGenerator = thumbGenerator; - //アイコン設定 - LoadIcons(); - this.Icon = MainIcon; //メインフォーム(TweenMain) - NotifyIcon1.Icon = NIconAt; //タスクトレイ - TabImage.Images.Add(TabIcon); //タブ見出し + this.InitializeComponent(); - //<<<<<<<<<設定関連>>>>>>>>> - ////設定読み出し - LoadConfig(); - - // 現在の DPI と設定保存時の DPI との比を取得する - var configScaleFactor = SettingManager.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); - - // UIフォント設定 - var fontUIGlobal = SettingManager.Local.FontUIGlobal; - if (fontUIGlobal != null) + if (!this.DesignMode) { - OTBaseForm.GlobalFont = fontUIGlobal; - this.Font = fontUIGlobal; + // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策 + this.StatusText.Dock = DockStyle.Fill; } - //不正値チェック - if (!MyApplication.StartupOptions.ContainsKey("nolimit")) - { - if (SettingManager.Common.TimelinePeriod < 15 && SettingManager.Common.TimelinePeriod > 0) - SettingManager.Common.TimelinePeriod = 15; - - if (SettingManager.Common.ReplyPeriod < 15 && SettingManager.Common.ReplyPeriod > 0) - SettingManager.Common.ReplyPeriod = 15; - - if (SettingManager.Common.DMPeriod < 15 && SettingManager.Common.DMPeriod > 0) - SettingManager.Common.DMPeriod = 15; + this.hookGlobalHotkey = new HookGlobalHotkey(this); - if (SettingManager.Common.PubSearchPeriod < 30 && SettingManager.Common.PubSearchPeriod > 0) - SettingManager.Common.PubSearchPeriod = 30; + this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed; + this.gh.NotifyClicked += this.GrowlHelper_Callback; - if (SettingManager.Common.UserTimelinePeriod < 15 && SettingManager.Common.UserTimelinePeriod > 0) - SettingManager.Common.UserTimelinePeriod = 15; - - if (SettingManager.Common.ListsPeriod < 15 && SettingManager.Common.ListsPeriod > 0) - SettingManager.Common.ListsPeriod = 15; - } + // メイリオフォント指定時にタブの最小幅が広くなる問題の対策 + this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40); - if (!Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Timeline, SettingManager.Common.CountApi)) - SettingManager.Common.CountApi = 60; - if (!Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Reply, SettingManager.Common.CountApiReply)) - SettingManager.Common.CountApiReply = 40; + this.ImageSelector.Visible = false; + this.ImageSelector.Enabled = false; + this.ImageSelector.FilePickDialog = this.OpenFileDialog1; - if (SettingManager.Common.MoreCountApi != 0 && !Twitter.VerifyMoreApiResultCount(SettingManager.Common.MoreCountApi)) - SettingManager.Common.MoreCountApi = 200; - if (SettingManager.Common.FirstCountApi != 0 && !Twitter.VerifyFirstApiResultCount(SettingManager.Common.FirstCountApi)) - SettingManager.Common.FirstCountApi = 100; + this.workerProgress = new Progress(x => this.StatusLabel.Text = x); - if (SettingManager.Common.FavoritesCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.Favorites, SettingManager.Common.FavoritesCountApi)) - SettingManager.Common.FavoritesCountApi = 40; - if (SettingManager.Common.ListCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.List, SettingManager.Common.ListCountApi)) - SettingManager.Common.ListCountApi = 100; - if (SettingManager.Common.SearchCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.PublicSearch, SettingManager.Common.SearchCountApi)) - SettingManager.Common.SearchCountApi = 100; - if (SettingManager.Common.UserTimelineCountApi != 0 && !Twitter.VerifyApiResultCount(MyCommon.WORKERTYPE.UserTimeline, SettingManager.Common.UserTimelineCountApi)) - SettingManager.Common.UserTimelineCountApi = 20; + this.ReplaceAppName(); + this.InitializeShortcuts(); - //廃止サービスが選択されていた場合ux.nuへ読み替え - if (SettingManager.Common.AutoShortUrlFirst < 0) - SettingManager.Common.AutoShortUrlFirst = MyCommon.UrlConverter.Uxnu; + this.ignoreConfigSave = true; + this.Visible = false; - TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost; - this.tw = new Twitter(this.twitterApi); + this.TraceOutToolStripMenuItem.Checked = MyCommon.TraceFlag; - //認証関連 - if (MyCommon.IsNullOrEmpty(SettingManager.Common.Token)) SettingManager.Common.UserName = ""; - tw.Initialize(SettingManager.Common.Token, SettingManager.Common.TokenSecret, SettingManager.Common.UserName, SettingManager.Common.UserId); + Microsoft.Win32.SystemEvents.PowerModeChanged += this.SystemEvents_PowerModeChanged; - _initial = true; + Regex.CacheSize = 100; - Networking.Initialize(); + // アイコン設定 + this.Icon = this.iconAssets.IconMain; // メインフォーム(TweenMain) + this.NotifyIcon1.Icon = this.iconAssets.IconTray; // タスクトレイ + this.TabImage.Images.Add(this.iconAssets.IconTab); // タブ見出し - var saveRequired = false; - var firstRun = false; + // <<<<<<<<<設定関連>>>>>>>>> + // 設定読み出し + this.LoadConfig(); - //ユーザー名、パスワードが未設定なら設定画面を表示(初回起動時など) - if (MyCommon.IsNullOrEmpty(tw.Username)) - { - saveRequired = true; - firstRun = true; + // 現在の DPI と設定保存時の DPI との比を取得する + var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); - //設定せずにキャンセルされたか、設定されたが依然ユーザー名が未設定ならプログラム終了 - if (ShowSettingDialog(showTaskbarIcon: true) != DialogResult.OK || - MyCommon.IsNullOrEmpty(tw.Username)) - { - Application.Exit(); //強制終了 - return; - } - } + // 認証関連 + this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId); - //Twitter用通信クラス初期化 - Networking.DefaultTimeout = TimeSpan.FromSeconds(SettingManager.Common.DefaultTimeOut); - Networking.UploadImageTimeout = TimeSpan.FromSeconds(SettingManager.Common.UploadImageTimeout); - Networking.SetWebProxy(SettingManager.Local.ProxyType, - SettingManager.Local.ProxyAddress, SettingManager.Local.ProxyPort, - SettingManager.Local.ProxyUser, SettingManager.Local.ProxyPassword); - Networking.ForceIPv4 = SettingManager.Common.ForceIPv4; + this.initial = true; - TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost; - tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck; - tw.ReadOwnPost = SettingManager.Common.ReadOwnPost; - ShortUrl.Instance.DisableExpanding = !SettingManager.Common.TinyUrlResolve; - ShortUrl.Instance.BitlyAccessToken = SettingManager.Common.BitlyAccessToken; - ShortUrl.Instance.BitlyId = SettingManager.Common.BilyUser; - ShortUrl.Instance.BitlyKey = SettingManager.Common.BitlyPwd; + this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; + this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; // アクセストークンが有効であるか確認する // ここが Twitter API への最初のアクセスになるようにすること @@ -921,106 +537,81 @@ private void TweenMain_Load(object sender, EventArgs e) } catch (WebApiException ex) { - MessageBox.Show(this, string.Format(Properties.Resources.StartupAuthError_Text, ex.Message), - ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - - //サムネイル関連の初期化 - //プロキシ設定等の通信まわりの初期化が済んでから処理する - ThumbnailGenerator.InitializeGenerator(); - - var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance; - imgazyobizinet.Enabled = SettingManager.Common.EnableImgAzyobuziNet; - imgazyobizinet.DisabledInDM = SettingManager.Common.ImgAzyobuziNetDisabledInDM; - - Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.twitterApi.Connection; - - //画像投稿サービス - ImageSelector.Initialize(tw, this.tw.Configuration, SettingManager.Common.UseImageServiceName, SettingManager.Common.UseImageService); - - //ハッシュタグ/@id関連 - AtIdSupl = new AtIdSupplement(SettingManager.AtIdList.AtIdList, "@"); - HashSupl = new AtIdSupplement(SettingManager.Common.HashTags, "#"); - HashMgr = new HashtagManage(HashSupl, - SettingManager.Common.HashTags.ToArray(), - SettingManager.Common.HashSelected, - SettingManager.Common.HashIsPermanent, - SettingManager.Common.HashIsHead, - SettingManager.Common.HashIsNotAddToAtReply); - if (!MyCommon.IsNullOrEmpty(HashMgr.UseHash) && HashMgr.IsPermanent) HashStripSplitButton.Text = HashMgr.UseHash; - - //アイコンリスト作成 - this.IconCache = new ImageCache(); - this.tweetDetailsView.IconCache = this.IconCache; - - //フォント&文字色&背景色保持 - _fntUnread = SettingManager.Local.FontUnread; - _clUnread = SettingManager.Local.ColorUnread; - _fntReaded = SettingManager.Local.FontRead; - _clReaded = SettingManager.Local.ColorRead; - _clFav = SettingManager.Local.ColorFav; - _clOWL = SettingManager.Local.ColorOWL; - _clRetweet = SettingManager.Local.ColorRetweet; - _fntDetail = SettingManager.Local.FontDetail; - _clDetail = SettingManager.Local.ColorDetail; - _clDetailLink = SettingManager.Local.ColorDetailLink; - _clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor; - _clSelf = SettingManager.Local.ColorSelf; - _clAtSelf = SettingManager.Local.ColorAtSelf; - _clTarget = SettingManager.Local.ColorTarget; - _clAtTarget = SettingManager.Local.ColorAtTarget; - _clAtFromTarget = SettingManager.Local.ColorAtFromTarget; - _clAtTo = SettingManager.Local.ColorAtTo; - _clListBackcolor = SettingManager.Local.ColorListBackcolor; - _clInputBackcolor = SettingManager.Local.ColorInputBackcolor; - _clInputFont = SettingManager.Local.ColorInputFont; - _fntInputFont = SettingManager.Local.FontInputFont; - - _brsBackColorMine = new SolidBrush(_clSelf); - _brsBackColorAt = new SolidBrush(_clAtSelf); - _brsBackColorYou = new SolidBrush(_clTarget); - _brsBackColorAtYou = new SolidBrush(_clAtTarget); - _brsBackColorAtFromTarget = new SolidBrush(_clAtFromTarget); - _brsBackColorAtTo = new SolidBrush(_clAtTo); - _brsBackColorNone = new SolidBrush(_clListBackcolor); + MessageBox.Show( + this, + string.Format(Properties.Resources.StartupAuthError_Text, ex.Message), + ApplicationSettings.ApplicationName, + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + + // サムネイル関連の初期化 + // プロキシ設定等の通信まわりの初期化が済んでから処理する + var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet; + imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet; + imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM; + imgazyobizinet.AutoUpdate = true; + + Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection; + + // 画像投稿サービス + this.ImageSelector.Initialize(this.tw, this.tw.Configuration, this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService); + + this.tweetThumbnail1.Initialize(this.thumbGenerator); + + // ハッシュタグ/@id関連 + this.AtIdSupl = new AtIdSupplement(this.settings.AtIdList.AtIdList, "@"); + this.HashSupl = new AtIdSupplement(this.settings.Common.HashTags, "#"); + this.HashMgr = new HashtagManage(this.HashSupl, + this.settings.Common.HashTags.ToArray(), + this.settings.Common.HashSelected, + this.settings.Common.HashIsPermanent, + this.settings.Common.HashIsHead, + this.settings.Common.HashIsNotAddToAtReply); + if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && this.HashMgr.IsPermanent) this.HashStripSplitButton.Text = this.HashMgr.UseHash; + + // フォント&文字色&背景色保持 + this.themeManager = new(this.settings.Local); + this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager); // StringFormatオブジェクトへの事前設定 - sfTab.Alignment = StringAlignment.Center; - sfTab.LineAlignment = StringAlignment.Center; + this.sfTab.Alignment = StringAlignment.Center; + this.sfTab.LineAlignment = StringAlignment.Center; - InitDetailHtmlFormat(); + this.InitDetailHtmlFormat(); + this.tweetDetailsView.ClearPostBrowser(); this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]"; - _history.Add(new StatusTextHistory()); - _hisIdx = 0; + this.history.Add(new StatusTextHistory("")); + this.hisIdx = 0; this.inReplyTo = null; - //各種ダイアログ設定 - SearchDialog.Owner = this; - UrlDialog.Owner = this; - - //新着バルーン通知のチェック状態設定 - NewPostPopMenuItem.Checked = SettingManager.Common.NewAllPop; - this.NotifyFileMenuItem.Checked = NewPostPopMenuItem.Checked; - - //新着取得時のリストスクロールをするか。trueならスクロールしない - ListLockMenuItem.Checked = SettingManager.Common.ListLock; - this.LockListFileMenuItem.Checked = SettingManager.Common.ListLock; - //サウンド再生(タブ別設定より優先) - this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound; - this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound; - - //ウィンドウ設定 - this.ClientSize = ScaleBy(configScaleFactor, SettingManager.Local.FormSize); - _mySize = this.ClientSize; // サイズ保持(最小化・最大化されたまま終了した場合の対応用) - _myLoc = SettingManager.Local.FormLocation; - //タイトルバー領域 + // 各種ダイアログ設定 + this.SearchDialog.Owner = this; + this.urlDialog.Owner = this; + + // 新着バルーン通知のチェック状態設定 + this.NewPostPopMenuItem.Checked = this.settings.Common.NewAllPop; + this.NotifyFileMenuItem.Checked = this.NewPostPopMenuItem.Checked; + + // 新着取得時のリストスクロールをするか。trueならスクロールしない + this.ListLockMenuItem.Checked = this.settings.Common.ListLock; + this.LockListFileMenuItem.Checked = this.settings.Common.ListLock; + // サウンド再生(タブ別設定より優先) + this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound; + this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound; + + // ウィンドウ設定 + this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize); + this.mySize = this.ClientSize; // サイズ保持(最小化・最大化されたまま終了した場合の対応用) + this.myLoc = this.settings.Local.FormLocation; + // タイトルバー領域 if (this.WindowState != FormWindowState.Minimized) { - var tbarRect = new Rectangle(this._myLoc, new Size(_mySize.Width, SystemInformation.CaptionHeight)); + var tbarRect = new Rectangle(this.myLoc, new Size(this.mySize.Width, SystemInformation.CaptionHeight)); var outOfScreen = true; - if (Screen.AllScreens.Length == 1) //ハングするとの報告 + if (Screen.AllScreens.Length == 1) // ハングするとの報告 { foreach (var scr in Screen.AllScreens) { @@ -1032,56 +623,48 @@ private void TweenMain_Load(object sender, EventArgs e) } if (outOfScreen) - this._myLoc = new Point(0, 0); + this.myLoc = new Point(0, 0); } - this.DesktopLocation = this._myLoc; - } - this.TopMost = SettingManager.Common.AlwaysTop; - _mySpDis = ScaleBy(configScaleFactor.Height, SettingManager.Local.SplitterDistance); - _mySpDis2 = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight); - if (SettingManager.Local.PreviewDistance == -1) - { - _mySpDis3 = _mySize.Width - ScaleBy(this.CurrentScaleFactor.Width, 150); - if (_mySpDis3 < 1) _mySpDis3 = ScaleBy(this.CurrentScaleFactor.Width, 50); - SettingManager.Local.PreviewDistance = _mySpDis3; - } - else - { - _mySpDis3 = ScaleBy(configScaleFactor.Width, SettingManager.Local.PreviewDistance); + this.DesktopLocation = this.myLoc; } - this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound; - this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound; - //入力欄 - StatusText.Font = _fntInputFont; - StatusText.ForeColor = _clInputFont; + this.TopMost = this.settings.Common.AlwaysTop; + this.mySpDis = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance); + this.mySpDis2 = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight); + this.mySpDis3 = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance); + + this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound; + this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound; + // 入力欄 + this.StatusText.Font = this.themeManager.FontInputFont; + this.StatusText.ForeColor = this.themeManager.ColorInputFont; // SplitContainer2.Panel2MinSize を一行表示の入力欄の高さに合わせる (MS UI Gothic 12pt (96dpi) の場合は 19px) - this.StatusText.Multiline = false; // SettingManager.Local.StatusMultiline の設定は後で反映される + this.StatusText.Multiline = false; // this.settings.Local.StatusMultiline の設定は後で反映される this.SplitContainer2.Panel2MinSize = this.StatusText.Height; // 必要であれば、発言一覧と発言詳細部・入力欄の上下を入れ替える - SplitContainer1.IsPanelInverted = !SettingManager.Common.StatusAreaAtBottom; + this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom; - //全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定) - if (SettingManager.Common.UnreadManage == false) + // 全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定) + if (this.settings.Common.UnreadManage == false) { - ReadedStripMenuItem.Enabled = false; - UnreadStripMenuItem.Enabled = false; + this.ReadedStripMenuItem.Enabled = false; + this.UnreadStripMenuItem.Enabled = false; } - //リンク先URL表示部の初期化(画面左下) - StatusLabelUrl.Text = ""; - //状態表示部の初期化(画面右下) - StatusLabel.Text = ""; - StatusLabel.AutoToolTip = false; - StatusLabel.ToolTipText = ""; - //文字カウンタ初期化 - lblLen.Text = this.GetRestStatusCount(this.FormatStatusTextExtended("")).ToString(); + // リンク先URL表示部の初期化(画面左下) + this.StatusLabelUrl.Text = ""; + // 状態表示部の初期化(画面右下) + this.StatusLabel.Text = ""; + this.StatusLabel.AutoToolTip = false; + this.StatusLabel.ToolTipText = ""; + // 文字カウンタ初期化 + this.lblLen.Text = this.GetRestStatusCount(this.FormatStatusTextExtended("")).ToString(); this.JumpReadOpMenuItem.ShortcutKeyDisplayString = "Space"; - CopySTOTMenuItem.ShortcutKeyDisplayString = "Ctrl+C"; - CopyURLMenuItem.ShortcutKeyDisplayString = "Ctrl+Shift+C"; - CopyUserIdStripMenuItem.ShortcutKeyDisplayString = "Shift+Alt+C"; + this.CopySTOTMenuItem.ShortcutKeyDisplayString = "Ctrl+C"; + this.CopyURLMenuItem.ShortcutKeyDisplayString = "Ctrl+Shift+C"; + this.CopyUserIdStripMenuItem.ShortcutKeyDisplayString = "Shift+Alt+C"; // SourceLinkLabel のテキストが SplitContainer2.Panel2.AccessibleName にセットされるのを防ぐ // (タブオーダー順で SourceLinkLabel の次にある PostBrowser が TabStop = false となっているため、 @@ -1089,93 +672,79 @@ private void TweenMain_Load(object sender, EventArgs e) this.SplitContainer2.Panel2.AccessibleName = ""; //////////////////////////////////////////////////////////////////////////////// - var sortOrder = (SortOrder)SettingManager.Common.SortOrder; - var mode = ComparerMode.Id; - switch (SettingManager.Common.SortColumn) - { - case 0: //0:アイコン,5:未読マーク,6:プロテクト・フィルターマーク - case 5: - case 6: - //ソートしない - mode = ComparerMode.Id; //Idソートに読み替え - break; - case 1: //ニックネーム - mode = ComparerMode.Nickname; - break; - case 2: //本文 - mode = ComparerMode.Data; - break; - case 3: //時刻=発言Id - mode = ComparerMode.Id; - break; - case 4: //名前 - mode = ComparerMode.Name; - break; - case 7: //Source - mode = ComparerMode.Source; - break; - } - _statuses.SetSortMode(mode, sortOrder); + var sortOrder = (SortOrder)this.settings.Common.SortOrder; + var mode = this.settings.Common.SortColumn switch + { + // 0:アイコン,5:未読マーク,6:プロテクト・フィルターマーク + 0 or 5 or 6 => ComparerMode.Id, // Idソートに読み替え + 1 => ComparerMode.Nickname, // ニックネーム + 2 => ComparerMode.Data, // 本文 + 3 => ComparerMode.Id, // 時刻=発言Id + 4 => ComparerMode.Name, // 名前 + 7 => ComparerMode.Source, // Source + _ => ComparerMode.Id, + }; + this.statuses.SetSortMode(mode, sortOrder); //////////////////////////////////////////////////////////////////////////////// - ApplyListViewIconSize(SettingManager.Common.IconSize); + this.ApplyListViewIconSize(this.settings.Common.IconSize); - //<<<<<<<<タブ関連>>>>>>> - foreach (var tab in _statuses.Tabs) + // <<<<<<<<タブ関連>>>>>>> + foreach (var tab in this.statuses.Tabs) { - if (!AddNewTab(tab, startup: true)) + if (!this.AddNewTab(tab, startup: true)) throw new TabException(Properties.Resources.TweenMain_LoadText1); } - this._statuses.SelectTab(this.ListTab.SelectedTab.Text); + this.ListTabSelect(this.ListTab.SelectedTab); // タブの位置を調整する - SetTabAlignment(); + this.SetTabAlignment(); - MyCommon.TwitterApiInfo.AccessLimitUpdated += TwitterApiStatus_AccessLimitUpdated; - Microsoft.Win32.SystemEvents.TimeChanged += SystemEvents_TimeChanged; + MyCommon.TwitterApiInfo.AccessLimitUpdated += this.TwitterApiStatus_AccessLimitUpdated; + Microsoft.Win32.SystemEvents.TimeChanged += this.SystemEvents_TimeChanged; - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - ListTab.DrawMode = TabDrawMode.Normal; + this.ListTab.DrawMode = TabDrawMode.Normal; } else { - ListTab.DrawMode = TabDrawMode.OwnerDrawFixed; - ListTab.DrawItem += ListTab_DrawItem; - ListTab.ImageList = null; + this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed; + this.ListTab.DrawItem += this.ListTab_DrawItem; + this.ListTab.ImageList = null; } - if (SettingManager.Common.HotkeyEnabled) + if (this.settings.Common.HotkeyEnabled) { - //////グローバルホットキーの登録 + // グローバルホットキーの登録 var modKey = HookGlobalHotkey.ModKeys.None; - if ((SettingManager.Common.HotkeyModifier & Keys.Alt) == Keys.Alt) + if ((this.settings.Common.HotkeyModifier & Keys.Alt) == Keys.Alt) modKey |= HookGlobalHotkey.ModKeys.Alt; - if ((SettingManager.Common.HotkeyModifier & Keys.Control) == Keys.Control) + if ((this.settings.Common.HotkeyModifier & Keys.Control) == Keys.Control) modKey |= HookGlobalHotkey.ModKeys.Ctrl; - if ((SettingManager.Common.HotkeyModifier & Keys.Shift) == Keys.Shift) + if ((this.settings.Common.HotkeyModifier & Keys.Shift) == Keys.Shift) modKey |= HookGlobalHotkey.ModKeys.Shift; - if ((SettingManager.Common.HotkeyModifier & Keys.LWin) == Keys.LWin) + if ((this.settings.Common.HotkeyModifier & Keys.LWin) == Keys.LWin) modKey |= HookGlobalHotkey.ModKeys.Win; - _hookGlobalHotkey.RegisterOriginalHotkey(SettingManager.Common.HotkeyKey, SettingManager.Common.HotkeyValue, modKey); + this.hookGlobalHotkey.RegisterOriginalHotkey(this.settings.Common.HotkeyKey, this.settings.Common.HotkeyValue, modKey); } - if (SettingManager.Common.IsUseNotifyGrowl) - gh.RegisterGrowl(); + if (this.settings.Common.IsUseNotifyGrowl) + this.gh.RegisterGrowl(); - StatusLabel.Text = Properties.Resources.Form1_LoadText1; //画面右下の状態表示を変更 + this.StatusLabel.Text = Properties.Resources.Form1_LoadText1; // 画面右下の状態表示を変更 - SetMainWindowTitle(); - SetNotifyIconText(); + this.SetMainWindowTitle(); + this.SetNotifyIconText(); - if (!SettingManager.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized) + if (!this.settings.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized) { this.Visible = true; } - //タイマー設定 + // タイマー設定 this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Mention] = () => this.InvokeAsync(() => this.RefreshTabAsync()); @@ -1185,7 +754,7 @@ private void TweenMain_Load(object sender, EventArgs e) this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.List] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Config] = () => this.InvokeAsync(() => Task.WhenAll(new[] { - this.doGetFollowersMenu(), + this.DoGetFollowersMenu(), this.RefreshBlockIdsAsync(), this.RefreshMuteUserIdsAsync(), this.RefreshNoRetweetIdsAsync(), @@ -1196,50 +765,34 @@ private void TweenMain_Load(object sender, EventArgs e) this.selectionDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true); this.saveConfigDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1)); - //更新中アイコンアニメーション間隔 - TimerRefreshIcon.Interval = 200; - TimerRefreshIcon.Enabled = false; + // 更新中アイコンアニメーション間隔 + this.TimerRefreshIcon.Interval = 200; + this.TimerRefreshIcon.Enabled = false; - _ignoreConfigSave = false; + this.ignoreConfigSave = false; this.TweenMain_Resize(this, EventArgs.Empty); - if (saveRequired) SaveConfigsAll(false); - - foreach (var ua in SettingManager.Common.UserAccounts) - { - if (ua.UserId == 0 && ua.Username.Equals(tw.Username, StringComparison.InvariantCultureIgnoreCase)) - { - ua.UserId = tw.UserId; - break; - } - } - if (firstRun) + if (this.settings.IsFirstRun) { // 初回起動時だけ右下のメニューを目立たせる - HashStripSplitButton.ShowDropDown(); + this.HashStripSplitButton.ShowDropDown(); } } private void InitDetailHtmlFormat() { - if (SettingManager.Common.IsMonospace) - { - detailHtmlFormatHeader = detailHtmlFormatHeaderMono; - detailHtmlFormatFooter = detailHtmlFormatFooterMono; - } - else - { - detailHtmlFormatHeader = detailHtmlFormatHeaderColor; - detailHtmlFormatFooter = detailHtmlFormatFooterColor; - } + var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal; + + static string ColorToRGBString(Color color) + => $"{color.R},{color.G},{color.B}"; - detailHtmlFormatHeader = detailHtmlFormatHeader - .Replace("%FONT_FAMILY%", _fntDetail.Name) - .Replace("%FONT_SIZE%", _fntDetail.Size.ToString()) - .Replace("%FONT_COLOR%", $"{_clDetail.R},{_clDetail.G},{_clDetail.B}") - .Replace("%LINK_COLOR%", $"{_clDetailLink.R},{_clDetailLink.G},{_clDetailLink.B}") - .Replace("%BG_COLOR%", $"{_clDetailBackcolor.R},{_clDetailBackcolor.G},{_clDetailBackcolor.B}") - .Replace("%BG_REPLY_COLOR%", $"{_clAtTo.R}, {_clAtTo.G}, {_clAtTo.B}"); + this.detailHtmlFormatPreparedTemplate = htmlTemplate + .Replace("%FONT_FAMILY%", this.themeManager.FontDetail.Name) + .Replace("%FONT_SIZE%", this.themeManager.FontDetail.Size.ToString()) + .Replace("%FONT_COLOR%", ColorToRGBString(this.themeManager.ColorDetail)) + .Replace("%LINK_COLOR%", ColorToRGBString(this.themeManager.ColorDetailLink)) + .Replace("%BG_COLOR%", ColorToRGBString(this.themeManager.ColorDetailBackcolor)) + .Replace("%BG_REPLY_COLOR%", ColorToRGBString(this.themeManager.ColorAtTo)); } private void ListTab_DrawItem(object sender, DrawItemEventArgs e) @@ -1247,7 +800,7 @@ private void ListTab_DrawItem(object sender, DrawItemEventArgs e) string txt; try { - txt = this._statuses.Tabs[e.Index].TabName; + txt = this.statuses.Tabs[e.Index].TabName; } catch (Exception) { @@ -1262,7 +815,7 @@ private void ListTab_DrawItem(object sender, DrawItemEventArgs e) Brush fore; try { - if (_statuses.Tabs[txt].UnreadCount > 0) + if (this.statuses.Tabs[txt].UnreadCount > 0) fore = Brushes.Red; else fore = System.Drawing.SystemBrushes.ControlText; @@ -1271,77 +824,13 @@ private void ListTab_DrawItem(object sender, DrawItemEventArgs e) { fore = System.Drawing.SystemBrushes.ControlText; } - e.Graphics.DrawString(txt, e.Font, fore, e.Bounds, sfTab); + e.Graphics.DrawString(txt, e.Font, fore, e.Bounds, this.sfTab); } private void LoadConfig() { - SettingManager.Local = SettingManager.Local; - - // v1.2.4 以前の設定には ScaleDimension の項目がないため、現在の DPI と同じとして扱う - if (SettingManager.Local.ScaleDimension.IsEmpty) - SettingManager.Local.ScaleDimension = this.CurrentAutoScaleDimensions; - - var tabSettings = SettingManager.Tabs; - foreach (var tabSetting in tabSettings.Tabs) - { - TabModel tab; - switch (tabSetting.TabType) - { - case MyCommon.TabUsageType.Home: - tab = new HomeTabModel(tabSetting.TabName); - break; - case MyCommon.TabUsageType.Mentions: - tab = new MentionsTabModel(tabSetting.TabName); - break; - case MyCommon.TabUsageType.DirectMessage: - tab = new DirectMessagesTabModel(tabSetting.TabName); - break; - case MyCommon.TabUsageType.Favorites: - tab = new FavoritesTabModel(tabSetting.TabName); - break; - case MyCommon.TabUsageType.UserDefined: - tab = new FilterTabModel(tabSetting.TabName); - break; - case MyCommon.TabUsageType.UserTimeline: - tab = new UserTimelineTabModel(tabSetting.TabName, tabSetting.User!); - break; - case MyCommon.TabUsageType.PublicSearch: - tab = new PublicSearchTabModel(tabSetting.TabName) - { - SearchWords = tabSetting.SearchWords, - SearchLang = tabSetting.SearchLang, - }; - break; - case MyCommon.TabUsageType.Lists: - tab = new ListTimelineTabModel(tabSetting.TabName, tabSetting.ListInfo!); - break; - case MyCommon.TabUsageType.Mute: - tab = new MuteTabModel(tabSetting.TabName); - break; - default: - continue; - } - - tab.UnreadManage = tabSetting.UnreadManage; - tab.Protected = tabSetting.Protected; - tab.Notify = tabSetting.Notify; - tab.SoundFile = tabSetting.SoundFile; - - if (tab.IsDistributableTabType) - { - var filterTab = (FilterTabModel)tab; - filterTab.FilterArray = tabSetting.FilterArray; - filterTab.FilterModified = false; - } - - if (this._statuses.ContainsTab(tab.TabName)) - tab.TabName = this._statuses.MakeTabName("MyTab"); - - this._statuses.AddTab(tab); - } - - this._statuses.AddDefaultTabs(); + this.statuses.LoadTabsFromSettings(this.settings.Tabs); + this.statuses.AddDefaultTabs(); } private void TimerInterval_Changed(object sender, IntervalChangedEventArgs e) @@ -1351,15 +840,15 @@ private void TimerInterval_Changed(object sender, IntervalChangedEventArgs e) private void RefreshTimelineScheduler() { - static TimeSpan intervalSecondsOrDisabled(int seconds) + static TimeSpan IntervalSecondsOrDisabled(int seconds) => seconds == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(seconds); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Home] = intervalSecondsOrDisabled(SettingManager.Common.TimelinePeriod); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Mention] = intervalSecondsOrDisabled(SettingManager.Common.ReplyPeriod); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Dm] = intervalSecondsOrDisabled(SettingManager.Common.DMPeriod); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.PublicSearch] = intervalSecondsOrDisabled(SettingManager.Common.PubSearchPeriod); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.User] = intervalSecondsOrDisabled(SettingManager.Common.UserTimelinePeriod); - this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.List] = intervalSecondsOrDisabled(SettingManager.Common.ListsPeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Home] = IntervalSecondsOrDisabled(this.settings.Common.TimelinePeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Mention] = IntervalSecondsOrDisabled(this.settings.Common.ReplyPeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Dm] = IntervalSecondsOrDisabled(this.settings.Common.DMPeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.PublicSearch] = IntervalSecondsOrDisabled(this.settings.Common.PubSearchPeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.User] = IntervalSecondsOrDisabled(this.settings.Common.UserTimelinePeriod); + this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.List] = IntervalSecondsOrDisabled(this.settings.Common.ListsPeriod); this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Config] = TimeSpan.FromHours(6); this.timelineScheduler.UpdateAfterSystemResume = TimeSpan.FromSeconds(30); @@ -1395,58 +884,40 @@ internal void MarkSettingAtIdModified() private void RefreshTimeline() { - var curTabModel = this.CurrentTab; var curListView = this.CurrentListView; // 現在表示中のタブのスクロール位置を退避 - var curListScroll = this.SaveListViewScroll(curListView, curTabModel); - - // 各タブのリスト上の選択位置などを退避 - var listSelections = this.SaveListViewSelection(); + var currentListViewState = this.listViewState[this.CurrentTabName]; + currentListViewState.Save(this.ListLockMenuItem.Checked); - //更新確定 + // 更新確定 int addCount; - addCount = _statuses.SubmitUpdate(out var soundFile, out var notifyPosts, - out var newMentionOrDm, out var isDelete); + addCount = this.statuses.SubmitUpdate( + out var soundFile, + out var notifyPosts, + out var newMentionOrDm, + out var isDelete); - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; // リストに反映&選択状態復元 - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + if (this.listCache != null && (this.listCache.IsListSizeMismatched || isDelete)) { - var tabPage = this.ListTab.TabPages[index]; - var listView = (DetailsListView)tabPage.Tag; - - if (listView.VirtualListSize != tab.AllCount || isDelete) + using (ControlTransaction.Update(curListView)) { - using (ControlTransaction.Update(listView)) - { - if (listView == curListView) - this.PurgeListViewItemCache(); + this.listCache.PurgeCache(); + this.listCache.UpdateListSize(); - try - { - // リスト件数更新 - listView.VirtualListSize = tab.AllCount; - } - catch (NullReferenceException ex) - { - // WinForms 内部で ListView.set_TopItem が発生させている例外 - // https://ja.osdn.net/ticket/browse.php?group_id=6526&tid=36588 - MyCommon.TraceOut(ex, $"TabType: {tab.TabType}, Count: {tab.AllCount}, ListSize: {listView.VirtualListSize}"); - } - - // 選択位置などを復元 - this.RestoreListViewSelection(listView, tab, listSelections[tab.TabName]); - } + // 選択位置などを復元 + currentListViewState.RestoreSelection(); } } if (addCount > 0) { - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { var tabPage = this.ListTab.TabPages[index]; if (tab.UnreadCount > 0 && tabPage.ImageIndex != 0) @@ -1460,220 +931,20 @@ private void RefreshTimeline() } // スクロール位置を復元 - this.RestoreListViewScroll(curListView, curTabModel, curListScroll); - - //新着通知 - NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm); - - SetMainWindowTitle(); - if (!StatusLabelUrl.Text.StartsWith("http", StringComparison.Ordinal)) SetStatusLabelUrl(); - - HashSupl.AddRangeItem(tw.GetHashList()); - - } - - internal struct ListViewScroll - { - public ScrollLockMode ScrollLockMode { get; set; } - public long? TopItemStatusId { get; set; } - } - - internal enum ScrollLockMode - { - /// 固定しない - None, - - /// 最上部に固定する - FixedToTop, - - /// 最下部に固定する - FixedToBottom, - - /// の位置に固定する - FixedToItem, - } - - /// - /// のスクロール位置に関する情報を として返します - /// - private ListViewScroll SaveListViewScroll(DetailsListView listView, TabModel tab) - { - var listScroll = new ListViewScroll - { - ScrollLockMode = this.GetScrollLockMode(listView), - }; - - if (listScroll.ScrollLockMode == ScrollLockMode.FixedToItem) - { - var topItemIndex = listView.TopItem?.Index ?? -1; - if (topItemIndex != -1 && topItemIndex < tab.AllCount) - listScroll.TopItemStatusId = tab.GetStatusIdAt(topItemIndex); - } - - return listScroll; - } - - private ScrollLockMode GetScrollLockMode(DetailsListView listView) - { - if (this._statuses.SortMode == ComparerMode.Id) - { - if (this._statuses.SortOrder == SortOrder.Ascending) - { - // Id昇順 - if (this.ListLockMenuItem.Checked) - return ScrollLockMode.None; - - // 最下行が表示されていたら、最下行へ強制スクロール。最下行が表示されていなかったら制御しない - - // 一番下に表示されているアイテム - var bottomItem = listView.GetItemAt(0, listView.ClientSize.Height - 1); - if (bottomItem == null || bottomItem.Index == listView.VirtualListSize - 1) - return ScrollLockMode.FixedToBottom; - else - return ScrollLockMode.None; - } - else - { - // Id降順 - if (this.ListLockMenuItem.Checked) - return ScrollLockMode.FixedToItem; - - // 最上行が表示されていたら、制御しない。最上行が表示されていなかったら、現在表示位置へ強制スクロール - var topItem = listView.TopItem; - if (topItem == null || topItem.Index == 0) - return ScrollLockMode.FixedToTop; - else - return ScrollLockMode.FixedToItem; - } - } - else - { - return ScrollLockMode.FixedToItem; - } - } - - internal struct ListViewSelection - { - public long[]? SelectedStatusIds { get; set; } - public long? SelectionMarkStatusId { get; set; } - public long? FocusedStatusId { get; set; } - } - - /// - /// の選択状態を として返します - /// - private IReadOnlyDictionary SaveListViewSelection() - { - var listsDict = new Dictionary(); - - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) - { - var listView = (DetailsListView)this.ListTab.TabPages[index].Tag; - listsDict[tab.TabName] = this.SaveListViewSelection(listView, tab); - } - - return listsDict; - } - - /// - /// の選択状態を として返します - /// - private ListViewSelection SaveListViewSelection(DetailsListView listView, TabModel tab) - { - if (listView.VirtualListSize == 0) - { - return new ListViewSelection - { - SelectedStatusIds = Array.Empty(), - SelectionMarkStatusId = null, - FocusedStatusId = null, - }; - } - - return new ListViewSelection - { - SelectedStatusIds = tab.SelectedStatusIds, - FocusedStatusId = this.GetFocusedStatusId(listView, tab), - SelectionMarkStatusId = this.GetSelectionMarkStatusId(listView, tab), - }; - } - - private long? GetFocusedStatusId(DetailsListView listView, TabModel tab) - { - var index = listView.FocusedItem?.Index ?? -1; - - return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null; - } - - private long? GetSelectionMarkStatusId(DetailsListView listView, TabModel tab) - { - var index = listView.SelectionMark; - - return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null; - } - - /// - /// によって保存されたスクロール位置を復元します - /// - private void RestoreListViewScroll(DetailsListView listView, TabModel tab, ListViewScroll listScroll) - { - if (listView.VirtualListSize == 0) - return; - - switch (listScroll.ScrollLockMode) - { - case ScrollLockMode.FixedToTop: - listView.EnsureVisible(0); - break; - case ScrollLockMode.FixedToBottom: - listView.EnsureVisible(listView.VirtualListSize - 1); - break; - case ScrollLockMode.FixedToItem: - var topIndex = listScroll.TopItemStatusId != null ? tab.IndexOf(listScroll.TopItemStatusId.Value) : -1; - if (topIndex != -1) - { - var topItem = listView.Items[topIndex]; - try - { - listView.TopItem = topItem; - } - catch (NullReferenceException) - { - listView.EnsureVisible(listView.VirtualListSize - 1); - listView.EnsureVisible(topIndex); - } - } - break; - case ScrollLockMode.None: - default: - break; - } - } - - /// - /// によって保存された選択状態を復元します - /// - private void RestoreListViewSelection(DetailsListView listView, TabModel tab, ListViewSelection listSelection) - { - // status_id から ListView 上のインデックスに変換 - int[]? selectedIndices = null; - if (listSelection.SelectedStatusIds != null) - selectedIndices = tab.IndexOf(listSelection.SelectedStatusIds).Where(x => x != -1).ToArray(); + currentListViewState.RestoreScroll(); - var focusedIndex = -1; - if (listSelection.FocusedStatusId != null) - focusedIndex = tab.IndexOf(listSelection.FocusedStatusId.Value); + // 新着通知 + this.NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm); - var selectionMarkIndex = -1; - if (listSelection.SelectionMarkStatusId != null) - selectionMarkIndex = tab.IndexOf(listSelection.SelectionMarkStatusId.Value); + this.SetMainWindowTitle(); + if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.Ordinal)) this.SetStatusLabelUrl(); - this.SelectListItem(listView, selectedIndices, focusedIndex, selectionMarkIndex); + this.HashSupl.AddRangeItem(this.tw.GetHashList()); } private bool BalloonRequired() { - if (this._initial) + if (this.initial) return false; if (NativeMethods.IsScreenSaverRunning()) @@ -1684,7 +955,7 @@ private bool BalloonRequired() return false; // 「画面最小化・アイコン時のみバルーンを表示する」が有効 - if (SettingManager.Common.LimitBalloon) + if (this.settings.Common.LimitBalloon) { if (this.WindowState != FormWindowState.Minimized && this.Visible && Form.ActiveForm != null) return false; @@ -1695,19 +966,19 @@ private bool BalloonRequired() private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCount, bool newMentions) { - if (SettingManager.Common.ReadOwnPost) + if (this.settings.Common.ReadOwnPost) { - if (notifyPosts != null && notifyPosts.Length > 0 && notifyPosts.All(x => x.UserId == tw.UserId)) + if (notifyPosts != null && notifyPosts.Length > 0 && notifyPosts.All(x => x.UserId == this.tw.UserId)) return; } - //新着通知 - if (BalloonRequired()) + // 新着通知 + if (this.BalloonRequired()) { if (notifyPosts != null && notifyPosts.Length > 0) { - //Growlは一個ずつばらして通知。ただし、3ポスト以上あるときはまとめる - if (SettingManager.Common.IsUseNotifyGrowl) + // Growlは一個ずつばらして通知。ただし、3ポスト以上あるときはまとめる + if (this.settings.Common.IsUseNotifyGrowl) { var sb = new StringBuilder(); var reply = false; @@ -1724,7 +995,7 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo if (post.IsReply && !post.IsExcludeReply) reply = true; if (post.IsDm) dm = true; if (sb.Length > 0) sb.Append(System.Environment.NewLine); - switch (SettingManager.Common.NameBalloon) + switch (this.settings.Common.NameBalloon) { case MyCommon.NameBalloonEnum.UserID: sb.Append(post.ScreenName).Append(" : "); @@ -1741,9 +1012,9 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo var title = new StringBuilder(); GrowlHelper.NotifyType nt; - if (SettingManager.Common.DispUsername) + if (this.settings.Common.DispUsername) { - title.Append(tw.Username); + title.Append(this.tw.Username); title.Append(" - "); } @@ -1771,8 +1042,8 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo var bText = sb.ToString(); if (MyCommon.IsNullOrEmpty(bText)) return; - var image = this.IconCache.TryGetFromCache(post.ImageUrl); - gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl); + var image = this.iconCache.TryGetFromCache(post.ImageUrl); + this.gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl); } } else @@ -1785,7 +1056,7 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo if (post.IsReply && !post.IsExcludeReply) reply = true; if (post.IsDm) dm = true; if (sb.Length > 0) sb.Append(System.Environment.NewLine); - switch (SettingManager.Common.NameBalloon) + switch (this.settings.Common.NameBalloon) { case MyCommon.NameBalloonEnum.UserID: sb.Append(post.ScreenName).Append(" : "); @@ -1795,14 +1066,13 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo break; } sb.Append(post.TextFromApi); - } var title = new StringBuilder(); ToolTipIcon ntIcon; - if (SettingManager.Common.DispUsername) + if (this.settings.Common.DispUsername) { - title.Append(tw.Username); + title.Append(this.tw.Username); title.Append(" - "); } @@ -1830,16 +1100,16 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo var bText = sb.ToString(); if (MyCommon.IsNullOrEmpty(bText)) return; - NotifyIcon1.BalloonTipTitle = title.ToString(); - NotifyIcon1.BalloonTipText = bText; - NotifyIcon1.BalloonTipIcon = ntIcon; - NotifyIcon1.ShowBalloonTip(500); + this.NotifyIcon1.BalloonTipTitle = title.ToString(); + this.NotifyIcon1.BalloonTipText = bText; + this.NotifyIcon1.BalloonTipIcon = ntIcon; + this.NotifyIcon1.ShowBalloonTip(500); } } } - //サウンド再生 - if (!_initial && SettingManager.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile)) + // サウンド再生 + if (!this.initial && this.settings.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile)) { try { @@ -1856,8 +1126,8 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo } } - //mentions新着時に画面ブリンク - if (!_initial && SettingManager.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null) + // mentions新着時に画面ブリンク + if (!this.initial && this.settings.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null) { NativeMethods.FlashMyWindow(this.Handle, 3); } @@ -1881,190 +1151,47 @@ private async void MyList_SelectedIndexChanged(object sender, EventArgs e) this.PushSelectPostChain(); var post = this.CurrentPost!; - this._statuses.SetReadAllTab(post.StatusId, read: true); + this.statuses.SetReadAllTab(post.StatusId, read: true); - //キャッシュの書き換え - ChangeCacheStyleRead(true, index); // 既読へ(フォント、文字色) - - this.ColorizeList(); + this.listCache?.RefreshStyle(); await this.selectionDebouncer.Call(); } - private void ChangeCacheStyleRead(bool Read, int Index) - { - var tabInfo = this.CurrentTab; - //Read:true=既読 false=未読 - //未読管理していなかったら既読として扱う - if (!tabInfo.UnreadManage || - !SettingManager.Common.UnreadManage) Read = true; - - var listCache = this._listItemCache; - if (listCache == null) - return; - - // キャッシュに含まれていないアイテムは対象外 - if (!listCache.TryGetValue(Index, out var itm, out var post)) - return; - - ChangeItemStyleRead(Read, itm, post, (DetailsListView)listCache.TargetList); - } - - private void ChangeItemStyleRead(bool Read, ListViewItem Item, PostClass Post, DetailsListView? DList) - { - Font fnt; - string star; - //フォント - if (Read) - { - fnt = _fntReaded; - star = ""; - } - else - { - fnt = _fntUnread; - star = "★"; - } - if (Item.SubItems[5].Text != star) - Item.SubItems[5].Text = star; - - //文字色 - Color cl; - if (Post.IsFav) - cl = _clFav; - else if (Post.RetweetedId != null) - cl = _clRetweet; - else if (Post.IsOwl && (Post.IsDm || SettingManager.Common.OneWayLove)) - cl = _clOWL; - else if (Read || !SettingManager.Common.UseUnreadStyle) - cl = _clReaded; - else - cl = _clUnread; - - if (DList == null || Item.Index == -1) - { - Item.ForeColor = cl; - if (SettingManager.Common.UseUnreadStyle) - Item.Font = fnt; - } - else - { - DList.Update(); - if (SettingManager.Common.UseUnreadStyle) - DList.ChangeItemFontAndColor(Item, cl, fnt); - else - DList.ChangeItemForeColor(Item, cl); - } - } - - private void ColorizeList() - { - //Index:更新対象のListviewItem.Index。Colorを返す。 - //-1は全キャッシュ。Colorは返さない(ダミーを戻す) - PostClass? _post; - if (_anchorFlag) - _post = _anchorPost; - else - _post = this.CurrentPost; - - if (_post == null) return; - - var listCache = this._listItemCache; - if (listCache == null) - return; - - var listView = (DetailsListView)listCache.TargetList; - - // ValidateRectが呼ばれる前に選択色などの描画を済ませておく - listView.Update(); - - foreach (var (listViewItem, cachedPost) in listCache.Cache) - { - var backColor = this.JudgeColor(_post, cachedPost); - listView.ChangeItemBackColor(listViewItem, backColor); - } - } - - private void ColorizeList(ListViewItem Item, PostClass post) - { - //Index:更新対象のListviewItem.Index。Colorを返す。 - //-1は全キャッシュ。Colorは返さない(ダミーを戻す) - PostClass? _post; - if (_anchorFlag) - _post = _anchorPost; - else - _post = this.CurrentPost; - - if (_post == null) return; - - if (Item.Index == -1) - Item.BackColor = JudgeColor(_post, post); - else - this.CurrentListView.ChangeItemBackColor(Item, JudgeColor(_post, post)); - } - - private Color JudgeColor(PostClass BasePost, PostClass TargetPost) - { - Color cl; - if (TargetPost.StatusId == BasePost.InReplyToStatusId) - //@先 - cl = _clAtTo; - else if (TargetPost.IsMe) - //自分=発言者 - cl = _clSelf; - else if (TargetPost.IsReply) - //自分宛返信 - cl = _clAtSelf; - else if (BasePost.ReplyToList.Any(x => x.UserId == TargetPost.UserId)) - //返信先 - cl = _clAtFromTarget; - else if (TargetPost.ReplyToList.Any(x => x.UserId == BasePost.UserId)) - //その人への返信 - cl = _clAtTarget; - else if (TargetPost.UserId == BasePost.UserId) - //発言者 - cl = _clTarget; - else - //その他 - cl = _clListBackcolor; - - return cl; - } - private void StatusTextHistoryBack() { if (!string.IsNullOrWhiteSpace(this.StatusText.Text)) - this._history[_hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); + this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); - this._hisIdx -= 1; - if (this._hisIdx < 0) - this._hisIdx = 0; + this.hisIdx -= 1; + if (this.hisIdx < 0) + this.hisIdx = 0; - var historyItem = this._history[this._hisIdx]; - this.inReplyTo = historyItem.inReplyTo; - this.StatusText.Text = historyItem.status; + var historyItem = this.history[this.hisIdx]; + this.inReplyTo = historyItem.InReplyTo; + this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; } private void StatusTextHistoryForward() { if (!string.IsNullOrWhiteSpace(this.StatusText.Text)) - this._history[this._hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); + this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); - this._hisIdx += 1; - if (this._hisIdx > this._history.Count - 1) - this._hisIdx = this._history.Count - 1; + this.hisIdx += 1; + if (this.hisIdx > this.history.Count - 1) + this.hisIdx = this.history.Count - 1; - var historyItem = this._history[this._hisIdx]; - this.inReplyTo = historyItem.inReplyTo; - this.StatusText.Text = historyItem.status; + var historyItem = this.history[this.hisIdx]; + this.inReplyTo = historyItem.InReplyTo; + this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; } private async void PostButton_Click(object sender, EventArgs e) { - if (StatusText.Text.Trim().Length == 0) + if (this.StatusText.Text.Trim().Length == 0) { - if (!ImageSelector.Enabled) + if (!this.ImageSelector.Enabled) { await this.DoRefresh(); return; @@ -2072,7 +1199,7 @@ private async void PostButton_Click(object sender, EventArgs e) } var currentPost = this.CurrentPost; - if (this.ExistCurrentPost && currentPost != null && StatusText.Text.Trim() == string.Format("RT @{0}: {1}", currentPost.ScreenName, currentPost.TextFromApi)) + if (this.ExistCurrentPost && currentPost != null && this.StatusText.Text.Trim() == string.Format("RT @{0}: {1}", currentPost.ScreenName, currentPost.TextFromApi)) { var rtResult = MessageBox.Show(string.Format(Properties.Resources.PostButton_Click1, Environment.NewLine), "Retweet", @@ -2081,8 +1208,8 @@ private async void PostButton_Click(object sender, EventArgs e) switch (rtResult) { case DialogResult.Yes: - StatusText.Text = ""; - await this.doReTweetOfficial(false); + this.StatusText.Text = ""; + await this.DoReTweetOfficial(false); return; case DialogResult.Cancel: return; @@ -2098,16 +1225,16 @@ private async void PostButton_Click(object sender, EventArgs e) return; } - _history[_history.Count - 1] = new StatusTextHistory(StatusText.Text, this.inReplyTo); + this.history[this.history.Count - 1] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); - if (SettingManager.Common.Nicoms) + if (this.settings.Common.Nicoms) { - StatusText.SelectionStart = StatusText.Text.Length; - await UrlConvertAsync(MyCommon.UrlConverter.Nicoms); + this.StatusText.SelectionStart = this.StatusText.Text.Length; + await this.UrlConvertAsync(MyCommon.UrlConverter.Nicoms); } - StatusText.SelectionStart = StatusText.Text.Length; - CheckReplyTo(StatusText.Text); + this.StatusText.SelectionStart = this.StatusText.Text.Length; + this.CheckReplyTo(this.StatusText.Text); var status = new PostStatusParams(); @@ -2128,7 +1255,7 @@ private async void PostButton_Click(object sender, EventArgs e) // リプライ先がセットされていても autoPopulatedUserIds が空の場合は auto_populate_reply_metadata を有効にしない // (非公式 RT の場合など) - var replyToPost = this.inReplyTo != null ? this._statuses[this.inReplyTo.Value.StatusId] : null; + var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null; if (replyToPost != null && autoPopulatedUserIds.Length != 0) { status.AutoPopulateReplyMetadata = true; @@ -2149,28 +1276,28 @@ private async void PostButton_Click(object sender, EventArgs e) IMediaUploadService? uploadService = null; IMediaItem[]? uploadItems = null; - if (ImageSelector.Visible) + if (this.ImageSelector.Visible) { - //画像投稿 - if (!ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems)) + // 画像投稿 + if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems)) return; uploadService = this.ImageSelector.GetService(serviceName); } this.inReplyTo = null; - StatusText.Text = ""; - _history.Add(new StatusTextHistory()); - _hisIdx = _history.Count - 1; - if (!SettingManager.Common.FocusLockToStatusText) + this.StatusText.Text = ""; + this.history.Add(new StatusTextHistory("")); + this.hisIdx = this.history.Count - 1; + if (!this.settings.Common.FocusLockToStatusText) this.CurrentListView.Focus(); - urlUndoBuffer = null; - UrlUndoToolStripMenuItem.Enabled = false; //Undoをできないように設定 + this.urlUndoBuffer = null; + this.UrlUndoToolStripMenuItem.Enabled = false; // Undoをできないように設定 - //Google検索(試験実装) - if (StatusText.Text.StartsWith("Google:", StringComparison.OrdinalIgnoreCase) && StatusText.Text.Trim().Length > 7) + // Google検索(試験実装) + if (this.StatusText.Text.StartsWith("Google:", StringComparison.OrdinalIgnoreCase) && this.StatusText.Text.Trim().Length > 7) { - var tmp = string.Format(Properties.Resources.SearchItem2Url, Uri.EscapeDataString(StatusText.Text.Substring(7))); + var tmp = string.Format(Properties.Resources.SearchItem2Url, Uri.EscapeDataString(this.StatusText.Text.Substring(7))); await MyCommon.OpenInBrowserAsync(this, tmp); } @@ -2179,25 +1306,25 @@ private async void PostButton_Click(object sender, EventArgs e) private void EndToolStripMenuItem_Click(object sender, EventArgs e) { - MyCommon._endingFlag = true; + MyCommon.EndingFlag = true; this.Close(); } private void TweenMain_FormClosing(object sender, FormClosingEventArgs e) { - if (!SettingManager.Common.CloseToExit && e.CloseReason == CloseReason.UserClosing && MyCommon._endingFlag == false) + if (!this.settings.Common.CloseToExit && e.CloseReason == CloseReason.UserClosing && MyCommon.EndingFlag == false) { - //_endingFlag=false:フォームの×ボタン + // _endingFlag=false:フォームの×ボタン e.Cancel = true; this.Visible = false; } else { - _hookGlobalHotkey.UnregisterAllOriginalHotkey(); - _ignoreConfigSave = true; - MyCommon._endingFlag = true; + this.hookGlobalHotkey.UnregisterAllOriginalHotkey(); + this.ignoreConfigSave = true; + MyCommon.EndingFlag = true; this.timelineScheduler.Enabled = false; - TimerRefreshIcon.Enabled = false; + this.TimerRefreshIcon.Enabled = false; } } @@ -2232,14 +1359,16 @@ private static bool CheckAccountValid() } /// 指定された型 に合致する全てのタブを更新します - private Task RefreshTabAsync() where T : TabModel + private Task RefreshTabAsync() + where T : TabModel => this.RefreshTabAsync(backward: false); /// 指定された型 に合致する全てのタブを更新します - private Task RefreshTabAsync(bool backward) where T : TabModel + private Task RefreshTabAsync(bool backward) + where T : TabModel { var loadTasks = - from tab in this._statuses.GetTabsByType() + from tab in this.statuses.GetTabsByType() select this.RefreshTabAsync(tab, backward); return Task.WhenAll(loadTasks); @@ -2257,21 +1386,21 @@ private async Task RefreshTabAsync(TabModel tab, bool backward) try { this.RefreshTasktrayIcon(); - await Task.Run(() => tab.RefreshAsync(this.tw, backward, this._initial, this.workerProgress)); + await Task.Run(() => tab.RefreshAsync(this.tw, backward, this.initial, this.workerProgress)); } catch (WebApiException ex) { - this._myStatusError = true; + this.myStatusError = true; var tabType = tab switch { - HomeTabModel _ => "GetTimeline", - MentionsTabModel _ => "GetTimeline", - DirectMessagesTabModel _ => "GetDirectMessage", - FavoritesTabModel _ => "GetFavorites", - PublicSearchTabModel _ => "GetSearch", - UserTimelineTabModel _ => "GetUserTimeline", - ListTimelineTabModel _ => "GetListStatus", - RelatedPostsTabModel _ => "GetRelatedTweets", + HomeTabModel => "GetTimeline", + MentionsTabModel => "GetTimeline", + DirectMessagesTabModel => "GetDirectMessage", + FavoritesTabModel => "GetFavorites", + PublicSearchTabModel => "GetSearch", + UserTimelineTabModel => "GetUserTimeline", + ListTimelineTabModel => "GetListStatus", + RelatedPostsTabModel => "GetRelatedTweets", _ => tab.GetType().Name.Replace("Model", ""), }; this.StatusLabel.Text = $"Err:{ex.Message}({tabType})"; @@ -2296,7 +1425,7 @@ private async Task FavAddAsync(long statusId, TabModel tab) } catch (WebApiException ex) { - this._myStatusError = true; + this.myStatusError = true; this.StatusLabel.Text = $"Err:{ex.Message}(PostFavAdd)"; } finally @@ -2327,7 +1456,7 @@ await Task.Run(async () => { try { - await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId) + await this.tw.Api.FavoritesCreate(post.RetweetedId ?? post.StatusId) .IgnoreResponse() .ConfigureAwait(false); } @@ -2337,28 +1466,28 @@ await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId) // エラーコード 139 のみの場合は成功と見なす } - if (SettingManager.Common.RestrictFavCheck) + if (this.settings.Common.RestrictFavCheck) { - var status = await this.twitterApi.StatusesShow(post.RetweetedId ?? post.StatusId) + var status = await this.tw.Api.StatusesShow(post.RetweetedId ?? post.StatusId) .ConfigureAwait(false); if (status.Favorited != true) throw new WebApiException("NG(Restricted?)"); } - this._favTimestamps.Add(DateTimeUtc.Now); + this.favTimestamps.Add(DateTimeUtc.Now); // TLでも取得済みならfav反映 - if (this._statuses.Posts.TryGetValue(statusId, out var postTl)) + if (this.statuses.Posts.TryGetValue(statusId, out var postTl)) { postTl.IsFav = true; - var favTab = this._statuses.FavoriteTab; + var favTab = this.statuses.FavoriteTab; favTab.AddPostQueue(postTl); } // 検索,リスト,UserTimeline,Relatedの各タブに反映 - foreach (var tb in this._statuses.GetTabsInnerStorageType()) + foreach (var tb in this.statuses.GetTabsInnerStorageType()) { if (tb.Contains(statusId)) tb.Posts[statusId].IsFav = true; @@ -2374,13 +1503,13 @@ await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId) // 時速表示用 var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1); - foreach (var i in MyCommon.CountDown(this._favTimestamps.Count - 1, 0)) + foreach (var i in MyCommon.CountDown(this.favTimestamps.Count - 1, 0)) { - if (this._favTimestamps[i] < oneHour) - this._favTimestamps.RemoveAt(i); + if (this.favTimestamps[i] < oneHour) + this.favTimestamps.RemoveAt(i); } - this._statuses.DistributePosts(); + this.statuses.DistributePosts(); }); if (ct.IsCancellationRequested) @@ -2394,7 +1523,7 @@ await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId) { var idx = tab.IndexOf(statusId); if (idx != -1) - this.ChangeCacheStyleRead(post.IsRead, idx); + this.listCache?.RefreshStyle(idx); } var currentPost = this.CurrentPost; @@ -2416,7 +1545,7 @@ private async Task FavRemoveAsync(IReadOnlyList statusIds, TabModel tab) } catch (WebApiException ex) { - this._myStatusError = true; + this.myStatusError = true; this.StatusLabel.Text = $"Err:{ex.Message}(PostFavRemove)"; } finally @@ -2437,7 +1566,7 @@ private async Task FavRemoveAsyncInternal(IProgress p, CancellationToken await Task.Run(async () => { - //スレッド処理はしない + // スレッド処理はしない var allCount = 0; var failedCount = 0; foreach (var statusId in statusIds) @@ -2453,7 +1582,7 @@ await Task.Run(async () => try { - await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId) + await this.tw.Api.FavoritesDestroy(post.RetweetedId ?? post.StatusId) .IgnoreResponse() .ConfigureAwait(false); } @@ -2466,11 +1595,11 @@ await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId) successIds.Add(statusId); post.IsFav = false; // リスト再描画必要 - if (this._statuses.Posts.TryGetValue(statusId, out var tabinfoPost)) + if (this.statuses.Posts.TryGetValue(statusId, out var tabinfoPost)) tabinfoPost.IsFav = false; // 検索,リスト,UserTimeline,Relatedの各タブに反映 - foreach (var tb in this._statuses.GetTabsInnerStorageType()) + foreach (var tb in this.statuses.GetTabsInnerStorageType()) { if (tb.Contains(statusId)) tb.Posts[statusId].IsFav = false; @@ -2481,7 +1610,7 @@ await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId) if (ct.IsCancellationRequested) return; - var favTab = this._statuses.FavoriteTab; + var favTab = this.statuses.FavoriteTab; foreach (var statusId in successIds) { // ツイートが削除された訳ではないので IsDeleted はセットしない @@ -2503,11 +1632,8 @@ await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId) foreach (var statusId in successIds) { var idx = tab.IndexOf(statusId); - if (idx == -1) - continue; - - var post = tab.Posts[statusId]; - this.ChangeCacheStyleRead(post.IsRead, idx); + if (idx != -1) + this.listCache?.RefreshStyle(idx); } } @@ -2531,7 +1657,7 @@ private async Task PostMessageAsync(PostStatusParams postParams, IMediaUploadSer } catch (WebApiException ex) { - this._myStatusError = true; + this.myStatusError = true; this.StatusLabel.Text = $"Err:{ex.Message}(PostMessage)"; } finally @@ -2540,8 +1666,12 @@ private async Task PostMessageAsync(PostStatusParams postParams, IMediaUploadSer } } - private async Task PostMessageAsyncInternal(IProgress p, CancellationToken ct, PostStatusParams postParams, - IMediaUploadService? uploadService, IMediaItem[]? uploadItems) + private async Task PostMessageAsyncInternal( + IProgress p, + CancellationToken ct, + PostStatusParams postParams, + IMediaUploadService? uploadService, + IMediaItem[]? uploadItems) { if (ct.IsCancellationRequested) return; @@ -2577,14 +1707,14 @@ await Task.Run(async () => // 処理は中断せずエラーの表示のみ行う errMsg = $"Err:{ex.Message}(PostMessage)"; p.Report(errMsg); - this._myStatusError = true; + this.myStatusError = true; } catch (UnauthorizedAccessException ex) { // アップロード対象のファイルが開けなかった場合など errMsg = $"Err:{ex.Message}(PostMessage)"; p.Report(errMsg); - this._myStatusError = true; + this.myStatusError = true; } finally { @@ -2623,19 +1753,19 @@ await Task.Run(async () => this.StatusText.Focus(); // 連投モードのときだけEnterイベントが起きないので強制的に背景色を戻す - if (SettingManager.Common.FocusLockToStatusText) + if (this.settings.Common.FocusLockToStatusText) this.StatusText_Enter(this.StatusText, EventArgs.Empty); } return; } - this._postTimestamps.Add(DateTimeUtc.Now); + this.postTimestamps.Add(DateTimeUtc.Now); var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1); - foreach (var i in MyCommon.CountDown(this._postTimestamps.Count - 1, 0)) + foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0)) { - if (this._postTimestamps[i] < oneHour) - this._postTimestamps.RemoveAt(i); + if (this.postTimestamps[i] < oneHour) + this.postTimestamps.RemoveAt(i); } if (!this.HashMgr.IsPermanent && !MyCommon.IsNullOrEmpty(this.HashMgr.UseHash)) @@ -2649,17 +1779,15 @@ await Task.Run(async () => this.SetMainWindowTitle(); // TLに反映 - if (SettingManager.Common.PostAndGet) - await this.RefreshTabAsync(); - else + if (post != null) { - if (post != null) - { - this._statuses.AddPost(post); - this._statuses.DistributePosts(); - } + this.statuses.AddPost(post); + this.statuses.DistributePosts(); this.RefreshTimeline(); } + + if (this.settings.Common.PostAndGet) + await this.RefreshTabAsync(); } private async Task RetweetAsync(IReadOnlyList statusIds) @@ -2675,7 +1803,7 @@ private async Task RetweetAsync(IReadOnlyList statusIds) } catch (WebApiException ex) { - this._myStatusError = true; + this.myStatusError = true; this.StatusLabel.Text = $"Err:{ex.Message}(PostRetweet)"; } finally @@ -2693,10 +1821,10 @@ private async Task RetweetAsyncInternal(IProgress p, CancellationToken c throw new WebApiException("Auth error. Check your account"); bool read; - if (!SettingManager.Common.UnreadManage) + if (!this.settings.Common.UnreadManage) read = true; else - read = this._initial && SettingManager.Common.Read; + read = this.initial && this.settings.Common.Read; p.Report("Posting..."); @@ -2716,24 +1844,26 @@ await Task.Run(async () => p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4); - this._postTimestamps.Add(DateTimeUtc.Now); + this.postTimestamps.Add(DateTimeUtc.Now); var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1); - foreach (var i in MyCommon.CountDown(this._postTimestamps.Count - 1, 0)) + foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0)) { - if (this._postTimestamps[i] < oneHour) - this._postTimestamps.RemoveAt(i); + if (this.postTimestamps[i] < oneHour) + this.postTimestamps.RemoveAt(i); } // 自分のRTはTLの更新では取得できない場合があるので、 // 投稿時取得の有無に関わらず追加しておく - posts.ForEach(post => this._statuses.AddPost(post)); + posts.ForEach(post => this.statuses.AddPost(post)); - if (SettingManager.Common.PostAndGet) + if (this.settings.Common.PostAndGet) + { await this.RefreshTabAsync(); + } else { - this._statuses.DistributePosts(); + this.statuses.DistributePosts(); this.RefreshTimeline(); } } @@ -2752,7 +1882,7 @@ private async Task RefreshFollowerIdsAsync() this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText3; this.RefreshTimeline(); - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); this.CurrentListView.Refresh(); } catch (WebApiException ex) @@ -2826,7 +1956,7 @@ private async Task RefreshTwitterConfigurationAsync() } } - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); this.CurrentListView.Refresh(); } catch (WebApiException ex) @@ -2845,7 +1975,7 @@ private async Task RefreshMuteUserIdsAsync() try { - await tw.RefreshMuteUserIdsAsync(); + await this.tw.RefreshMuteUserIdsAsync(); } catch (WebApiException ex) { @@ -2863,7 +1993,7 @@ private void NotifyIcon1_MouseClick(object sender, MouseEventArgs e) this.Visible = true; if (this.WindowState == FormWindowState.Minimized) { - this.WindowState = _formWindowState; + this.WindowState = this.formWindowState; } this.Activate(); this.BringToFront(); @@ -2871,34 +2001,41 @@ private void NotifyIcon1_MouseClick(object sender, MouseEventArgs e) } private async void MyList_MouseDoubleClick(object sender, MouseEventArgs e) + => await this.ListItemDoubleClickAction(); + + private async Task ListItemDoubleClickAction() { - switch (SettingManager.Common.ListDoubleClickAction) + switch (this.settings.Common.ListDoubleClickAction) { - case 0: - MakeReplyOrDirectStatus(); + case MyCommon.ListItemDoubleClickActionType.Reply: + this.MakeReplyText(); break; - case 1: + case MyCommon.ListItemDoubleClickActionType.ReplyAll: + this.MakeReplyText(atAll: true); + break; + case MyCommon.ListItemDoubleClickActionType.Favorite: await this.FavoriteChange(true); break; - case 2: + case MyCommon.ListItemDoubleClickActionType.ShowProfile: var post = this.CurrentPost; if (post != null) await this.ShowUserStatus(post.ScreenName, false); break; - case 3: - await ShowUserTimeline(); + case MyCommon.ListItemDoubleClickActionType.ShowTimeline: + await this.ShowUserTimeline(); break; - case 4: - ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty); + case MyCommon.ListItemDoubleClickActionType.ShowRelated: + this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty); break; - case 5: - MoveToHomeToolStripMenuItem_Click(this.MoveToHomeToolStripMenuItem, EventArgs.Empty); + case MyCommon.ListItemDoubleClickActionType.OpenHomeInBrowser: + this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty); break; - case 6: - StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty); + case MyCommon.ListItemDoubleClickActionType.OpenStatusInBrowser: + this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty); break; - case 7: - //動作なし + case MyCommon.ListItemDoubleClickActionType.None: + default: + // 動作なし break; } } @@ -2909,39 +2046,40 @@ private async void FavAddToolStripMenuItem_Click(object sender, EventArgs e) private async void FavRemoveToolStripMenuItem_Click(object sender, EventArgs e) => await this.FavoriteChange(false); - private async void FavoriteRetweetMenuItem_Click(object sender, EventArgs e) => await this.FavoritesRetweetOfficial(); private async void FavoriteRetweetUnofficialMenuItem_Click(object sender, EventArgs e) => await this.FavoritesRetweetUnofficial(); - private async Task FavoriteChange(bool FavAdd, bool multiFavoriteChangeDialogEnable = true) + private async Task FavoriteChange(bool favAdd, bool multiFavoriteChangeDialogEnable = true) { var tab = this.CurrentTab; var posts = tab.SelectedPosts; - //trueでFavAdd,falseでFavRemove + // trueでFavAdd,falseでFavRemove if (tab.TabType == MyCommon.TabUsageType.DirectMessage || posts.Length == 0 || !this.ExistCurrentPost) return; if (posts.Length > 1) { - if (FavAdd) + if (favAdd) { // 複数ツイートの一括ふぁぼは禁止 // https://support.twitter.com/articles/76915#favoriting MessageBox.Show(string.Format(Properties.Resources.FavoriteLimitCountText, 1)); - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; return; } else { if (multiFavoriteChangeDialogEnable) { - var confirm = MessageBox.Show(Properties.Resources.FavRemoveToolStripMenuItem_ClickText1, + var confirm = MessageBox.Show( + Properties.Resources.FavRemoveToolStripMenuItem_ClickText1, Properties.Resources.FavRemoveToolStripMenuItem_ClickText2, - MessageBoxButtons.OKCancel, MessageBoxIcon.Question); + MessageBoxButtons.OKCancel, + MessageBoxIcon.Question); if (confirm == DialogResult.Cancel) return; @@ -2949,7 +2087,7 @@ private async Task FavoriteChange(bool FavAdd, bool multiFavoriteChangeDialogEna } } - if (FavAdd) + if (favAdd) { var selectedPost = posts.Single(); if (selectedPost.IsFav) @@ -2974,19 +2112,7 @@ private async Task FavoriteChange(bool FavAdd, bool multiFavoriteChangeDialogEna } } - private PostClass GetCurTabPost(int Index) - { - var listCache = this._listItemCache; - if (listCache != null) - { - if (listCache.TryGetValue(Index, out _, out var post)) - return post; - } - - return this.CurrentTab[Index]; - } - - private async void MoveToHomeToolStripMenuItem_Click(object sender, EventArgs e) + private async void AuthorOpenInBrowserMenuItem_Click(object sender, EventArgs e) { var post = this.CurrentPost; if (post != null) @@ -2995,23 +2121,16 @@ private async void MoveToHomeToolStripMenuItem_Click(object sender, EventArgs e) await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl); } - private async void MoveToFavToolStripMenuItem_Click(object sender, EventArgs e) - { - var post = this.CurrentPost; - if (post != null) - await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + "#!/" + post.ScreenName + "/favorites"); - } - private void TweenMain_ClientSizeChanged(object sender, EventArgs e) { - if ((!_initialLayout) && this.Visible) + if ((!this.initialLayout) && this.Visible) { if (this.WindowState == FormWindowState.Normal) { - _mySize = this.ClientSize; - _mySpDis = this.SplitContainer1.SplitterDistance; - _mySpDis3 = this.SplitContainer3.SplitterDistance; - if (StatusText.Multiline) _mySpDis2 = this.StatusText.Height; + this.mySize = this.ClientSize; + this.mySpDis = this.SplitContainer1.SplitterDistance; + this.mySpDis3 = this.SplitContainer3.SplitterDistance; + if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height; this.MarkSettingLocalModified(); } } @@ -3033,7 +2152,7 @@ private void MyList_ColumnClick(object sender, ColumnClickEventArgs e) /// ソートを行う ComparerMode。null であればソートを行わない private ComparerMode? GetComparerModeByColumnIndex(int columnIndex) { - if (this._iconCol) + if (this.Use2ColumnsMode) return ComparerMode.Id; return columnIndex switch @@ -3089,27 +2208,27 @@ private void SetSortLastColumn() ///
private void SetSortColumn(ComparerMode sortColumn) { - if (SettingManager.Common.SortOrderLock) + if (this.settings.Common.SortOrderLock) return; - this._statuses.ToggleSortOrder(sortColumn); + this.statuses.ToggleSortOrder(sortColumn); this.InitColumnText(); var list = this.CurrentListView; - if (_iconCol) + if (this.Use2ColumnsMode) { - list.Columns[0].Text = this.ColumnText[0]; - list.Columns[1].Text = this.ColumnText[2]; + list.Columns[0].Text = this.columnText[0]; + list.Columns[1].Text = this.columnText[2]; } else { for (var i = 0; i <= 7; i++) { - list.Columns[i].Text = this.ColumnText[i]; + list.Columns[i].Text = this.columnText[i]; } } - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); var tab = this.CurrentTab; var post = this.CurrentPost; @@ -3129,100 +2248,90 @@ private void SetSortColumn(ComparerMode sortColumn) private void TweenMain_LocationChanged(object sender, EventArgs e) { - if (this.WindowState == FormWindowState.Normal && !_initialLayout) + if (this.WindowState == FormWindowState.Normal && !this.initialLayout) { - _myLoc = this.DesktopLocation; + this.myLoc = this.DesktopLocation; this.MarkSettingLocalModified(); } } private void ContextMenuOperate_Opening(object sender, CancelEventArgs e) { + var post = this.CurrentPost; if (!this.ExistCurrentPost) { - ReplyStripMenuItem.Enabled = false; - ReplyAllStripMenuItem.Enabled = false; - DMStripMenuItem.Enabled = false; - ShowProfileMenuItem.Enabled = false; - ShowUserTimelineContextMenuItem.Enabled = false; - ListManageUserContextToolStripMenuItem2.Enabled = false; - MoveToFavToolStripMenuItem.Enabled = false; - TabMenuItem.Enabled = false; - IDRuleMenuItem.Enabled = false; - SourceRuleMenuItem.Enabled = false; - ReadedStripMenuItem.Enabled = false; - UnreadStripMenuItem.Enabled = false; + this.ReplyStripMenuItem.Enabled = false; + this.ReplyAllStripMenuItem.Enabled = false; + this.DMStripMenuItem.Enabled = false; + this.TabMenuItem.Enabled = false; + this.IDRuleMenuItem.Enabled = false; + this.SourceRuleMenuItem.Enabled = false; + this.ReadedStripMenuItem.Enabled = false; + this.UnreadStripMenuItem.Enabled = false; + this.AuthorContextMenuItem.Visible = false; + this.RetweetedByContextMenuItem.Visible = false; } else { - ShowProfileMenuItem.Enabled = true; - ListManageUserContextToolStripMenuItem2.Enabled = true; - ReplyStripMenuItem.Enabled = true; - ReplyAllStripMenuItem.Enabled = true; - DMStripMenuItem.Enabled = true; - ShowUserTimelineContextMenuItem.Enabled = true; - MoveToFavToolStripMenuItem.Enabled = true; - TabMenuItem.Enabled = true; - IDRuleMenuItem.Enabled = true; - SourceRuleMenuItem.Enabled = true; - ReadedStripMenuItem.Enabled = true; - UnreadStripMenuItem.Enabled = true; + this.ReplyStripMenuItem.Enabled = true; + this.ReplyAllStripMenuItem.Enabled = true; + this.DMStripMenuItem.Enabled = true; + this.TabMenuItem.Enabled = true; + this.IDRuleMenuItem.Enabled = true; + this.SourceRuleMenuItem.Enabled = true; + this.ReadedStripMenuItem.Enabled = true; + this.UnreadStripMenuItem.Enabled = true; + this.AuthorContextMenuItem.Visible = true; + this.AuthorContextMenuItem.Text = $"@{post!.ScreenName}"; + this.RetweetedByContextMenuItem.Visible = post.RetweetedByUserId != null; + this.RetweetedByContextMenuItem.Text = $"@{post.RetweetedBy}"; } var tab = this.CurrentTab; - var post = this.CurrentPost; if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm) { - FavAddToolStripMenuItem.Enabled = false; - FavRemoveToolStripMenuItem.Enabled = false; - StatusOpenMenuItem.Enabled = false; - ShowRelatedStatusesMenuItem.Enabled = false; + this.FavAddToolStripMenuItem.Enabled = false; + this.FavRemoveToolStripMenuItem.Enabled = false; + this.StatusOpenMenuItem.Enabled = false; + this.ShowRelatedStatusesMenuItem.Enabled = false; - ReTweetStripMenuItem.Enabled = false; - ReTweetUnofficialStripMenuItem.Enabled = false; - QuoteStripMenuItem.Enabled = false; - FavoriteRetweetContextMenu.Enabled = false; - FavoriteRetweetUnofficialContextMenu.Enabled = false; + this.ReTweetStripMenuItem.Enabled = false; + this.ReTweetUnofficialStripMenuItem.Enabled = false; + this.QuoteStripMenuItem.Enabled = false; + this.FavoriteRetweetContextMenu.Enabled = false; + this.FavoriteRetweetUnofficialContextMenu.Enabled = false; } else { - FavAddToolStripMenuItem.Enabled = true; - FavRemoveToolStripMenuItem.Enabled = true; - StatusOpenMenuItem.Enabled = true; - ShowRelatedStatusesMenuItem.Enabled = true; //PublicSearchの時問題出るかも + this.FavAddToolStripMenuItem.Enabled = true; + this.FavRemoveToolStripMenuItem.Enabled = true; + this.StatusOpenMenuItem.Enabled = true; + this.ShowRelatedStatusesMenuItem.Enabled = true; // PublicSearchの時問題出るかも - if (!post.CanRetweetBy(this.twitterApi.CurrentUserId)) + if (!post.CanRetweetBy(this.tw.UserId)) { - ReTweetStripMenuItem.Enabled = false; - ReTweetUnofficialStripMenuItem.Enabled = false; - QuoteStripMenuItem.Enabled = false; - FavoriteRetweetContextMenu.Enabled = false; - FavoriteRetweetUnofficialContextMenu.Enabled = false; + this.ReTweetStripMenuItem.Enabled = false; + this.ReTweetUnofficialStripMenuItem.Enabled = false; + this.QuoteStripMenuItem.Enabled = false; + this.FavoriteRetweetContextMenu.Enabled = false; + this.FavoriteRetweetUnofficialContextMenu.Enabled = false; } else { - ReTweetStripMenuItem.Enabled = true; - ReTweetUnofficialStripMenuItem.Enabled = true; - QuoteStripMenuItem.Enabled = true; - FavoriteRetweetContextMenu.Enabled = true; - FavoriteRetweetUnofficialContextMenu.Enabled = true; + this.ReTweetStripMenuItem.Enabled = true; + this.ReTweetUnofficialStripMenuItem.Enabled = true; + this.QuoteStripMenuItem.Enabled = true; + this.FavoriteRetweetContextMenu.Enabled = true; + this.FavoriteRetweetUnofficialContextMenu.Enabled = true; } } if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null) { - RepliedStatusOpenMenuItem.Enabled = false; + this.RepliedStatusOpenMenuItem.Enabled = false; } else { - RepliedStatusOpenMenuItem.Enabled = true; - } - if (!this.ExistCurrentPost || post == null || MyCommon.IsNullOrEmpty(post.RetweetedBy)) - { - MoveToRTHomeMenuItem.Enabled = false; - } - else - { - MoveToRTHomeMenuItem.Enabled = true; + this.RepliedStatusOpenMenuItem.Enabled = true; } if (this.ExistCurrentPost && post != null) @@ -3236,12 +2345,12 @@ private void ContextMenuOperate_Opening(object sender, CancelEventArgs e) } private void ReplyStripMenuItem_Click(object sender, EventArgs e) - => this.MakeReplyOrDirectStatus(false, true); + => this.MakeReplyText(); private void DMStripMenuItem_Click(object sender, EventArgs e) - => this.MakeReplyOrDirectStatus(false, false); + => this.MakeDirectMessageText(); - private async Task doStatusDelete() + private async Task DoStatusDelete() { var posts = this.CurrentTab.SelectedPosts; if (posts.Length == 0) @@ -3251,10 +2360,12 @@ private async Task doStatusDelete() if (!posts.Any(x => x.CanDeleteBy(this.tw.UserId))) return; - var ret = MessageBox.Show(this, + var ret = MessageBox.Show( + this, string.Format(Properties.Resources.DeleteStripMenuItem_ClickText1, Environment.NewLine), Properties.Resources.DeleteStripMenuItem_ClickText2, - MessageBoxButtons.OKCancel, MessageBoxIcon.Question); + MessageBoxButtons.OKCancel, + MessageBoxIcon.Question); if (ret != DialogResult.OK) return; @@ -3274,7 +2385,7 @@ private async Task doStatusDelete() { if (post.IsDm) { - await this.twitterApi.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture)); + await this.tw.Api.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture)); } else { @@ -3282,7 +2393,7 @@ private async Task doStatusDelete() { // 自分が RT したツイート (自分が RT した自分のツイートも含む) // => RT を取り消し - await this.twitterApi.StatusesDestroy(post.StatusId) + await this.tw.Api.StatusesDestroy(post.StatusId) .IgnoreResponse(); } else @@ -3290,15 +2401,19 @@ await this.twitterApi.StatusesDestroy(post.StatusId) if (post.UserId == this.tw.UserId) { if (post.RetweetedId != null) + { // 他人に RT された自分のツイート // => RT 元の自分のツイートを削除 - await this.twitterApi.StatusesDestroy(post.RetweetedId.Value) + await this.tw.Api.StatusesDestroy(post.RetweetedId.Value) .IgnoreResponse(); + } else + { // 自分のツイート // => ツイートを削除 - await this.twitterApi.StatusesDestroy(post.StatusId) + await this.tw.Api.StatusesDestroy(post.StatusId) .IgnoreResponse(); + } } } } @@ -3309,7 +2424,7 @@ await this.twitterApi.StatusesDestroy(post.StatusId) continue; } - this._statuses.RemovePostFromAllTabs(post.StatusId, setIsDeleted: true); + this.statuses.RemovePostFromAllTabs(post.StatusId, setIsDeleted: true); } if (lastException == null) @@ -3317,50 +2432,45 @@ await this.twitterApi.StatusesDestroy(post.StatusId) else this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText3; // 失敗 - this.PurgeListViewItemCache(); - - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + using (ControlTransaction.Update(currentListView)) { - var tabPage = this.ListTab.TabPages[index]; - var listView = (DetailsListView)tabPage.Tag; - - using (ControlTransaction.Update(listView)) - { - listView.VirtualListSize = tab.AllCount; + this.listCache?.PurgeCache(); + this.listCache?.UpdateListSize(); - if (tab.TabName == this.CurrentTabName) - { - listView.SelectedIndices.Clear(); + currentListView.SelectedIndices.Clear(); - if (tab.AllCount != 0) - { - int selectedIndex; - if (tab.AllCount - 1 > focusedIndex && focusedIndex > -1) - selectedIndex = focusedIndex; - else - selectedIndex = tab.AllCount - 1; + var currentTab = this.CurrentTab; + if (currentTab.AllCount != 0) + { + int selectedIndex; + if (currentTab.AllCount - 1 > focusedIndex && focusedIndex > -1) + selectedIndex = focusedIndex; + else + selectedIndex = currentTab.AllCount - 1; - listView.SelectedIndices.Add(selectedIndex); - listView.EnsureVisible(selectedIndex); - listView.FocusedItem = listView.Items[selectedIndex]; - } - } + currentListView.SelectedIndices.Add(selectedIndex); + currentListView.EnsureVisible(selectedIndex); + currentListView.FocusedItem = currentListView.Items[selectedIndex]; } + } - if (SettingManager.Common.TabIconDisp && tab.UnreadCount == 0) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) + { + var tabPage = this.ListTab.TabPages[index]; + if (this.settings.Common.TabIconDisp && tab.UnreadCount == 0) { if (tabPage.ImageIndex == 0) tabPage.ImageIndex = -1; // タブアイコン } } - if (!SettingManager.Common.TabIconDisp) + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); } } private async void DeleteStripMenuItem_Click(object sender, EventArgs e) - => await this.doStatusDelete(); + => await this.DoStatusDelete(); private void ReadedStripMenuItem_Click(object sender, EventArgs e) { @@ -3369,15 +2479,14 @@ private void ReadedStripMenuItem_Click(object sender, EventArgs e) var tab = this.CurrentTab; foreach (var statusId in tab.SelectedStatusIds) { - this._statuses.SetReadAllTab(statusId, read: true); + this.statuses.SetReadAllTab(statusId, read: true); var idx = tab.IndexOf(statusId); - ChangeCacheStyleRead(true, idx); + this.listCache?.RefreshStyle(idx); } - ColorizeList(); } - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { if (tab.UnreadCount == 0) { @@ -3387,7 +2496,7 @@ private void ReadedStripMenuItem_Click(object sender, EventArgs e) } } } - if (!SettingManager.Common.TabIconDisp) ListTab.Refresh(); + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); } private void UnreadStripMenuItem_Click(object sender, EventArgs e) @@ -3397,15 +2506,14 @@ private void UnreadStripMenuItem_Click(object sender, EventArgs e) var tab = this.CurrentTab; foreach (var statusId in tab.SelectedStatusIds) { - this._statuses.SetReadAllTab(statusId, read: false); + this.statuses.SetReadAllTab(statusId, read: false); var idx = tab.IndexOf(statusId); - ChangeCacheStyleRead(false, idx); + this.listCache?.RefreshStyle(idx); } - ColorizeList(); } - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { if (tab.UnreadCount > 0) { @@ -3415,7 +2523,7 @@ private void UnreadStripMenuItem_Click(object sender, EventArgs e) } } } - if (!SettingManager.Common.TabIconDisp) ListTab.Refresh(); + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); } private async void RefreshStripMenuItem_Click(object sender, EventArgs e) @@ -3427,21 +2535,15 @@ private async Task DoRefresh() private async Task DoRefreshMore() => await this.RefreshTabAsync(this.CurrentTab, backward: true); - private DialogResult ShowSettingDialog(bool showTaskbarIcon = false) + private DialogResult ShowSettingDialog() { - var result = DialogResult.Abort; - using var settingDialog = new AppendSettingDialog(); - settingDialog.Icon = this.MainIcon; - settingDialog.Owner = this; - settingDialog.ShowInTaskbar = showTaskbarIcon; + settingDialog.Icon = this.iconAssets.IconMain; settingDialog.IntervalChanged += this.TimerInterval_Changed; - settingDialog.tw = this.tw; - settingDialog.twitterApi = this.twitterApi; - - settingDialog.LoadConfig(SettingManager.Common, SettingManager.Local); + settingDialog.LoadConfig(this.settings.Common, this.settings.Local); + DialogResult result; try { result = settingDialog.ShowDialog(this); @@ -3453,9 +2555,9 @@ private DialogResult ShowSettingDialog(bool showTaskbarIcon = false) if (result == DialogResult.OK) { - lock (_syncObject) + lock (this.syncObject) { - settingDialog.SaveConfig(SettingManager.Common, SettingManager.Local); + settingDialog.SaveConfig(this.settings.Common, this.settings.Local); } } @@ -3465,45 +2567,38 @@ private DialogResult ShowSettingDialog(bool showTaskbarIcon = false) private async void SettingStripMenuItem_Click(object sender, EventArgs e) { // 設定画面表示前のユーザー情報 - var oldUser = new { tw.AccessToken, tw.AccessTokenSecret, tw.Username, tw.UserId }; + var previousUserId = this.settings.Common.UserId; + var oldIconCol = this.Use2ColumnsMode; - var oldIconSz = SettingManager.Common.IconSize; - - if (ShowSettingDialog() == DialogResult.OK) + if (this.ShowSettingDialog() == DialogResult.OK) { - lock (_syncObject) + lock (this.syncObject) { - tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck; - tw.ReadOwnPost = SettingManager.Common.ReadOwnPost; - ShortUrl.Instance.DisableExpanding = !SettingManager.Common.TinyUrlResolve; - ShortUrl.Instance.BitlyAccessToken = SettingManager.Common.BitlyAccessToken; - ShortUrl.Instance.BitlyId = SettingManager.Common.BilyUser; - ShortUrl.Instance.BitlyKey = SettingManager.Common.BitlyPwd; - TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost; + this.settings.ApplySettings(); + + if (MyCommon.IsNullOrEmpty(this.settings.Common.Token)) + this.tw.ClearAuthInfo(); - Networking.DefaultTimeout = TimeSpan.FromSeconds(SettingManager.Common.DefaultTimeOut); - Networking.UploadImageTimeout = TimeSpan.FromSeconds(SettingManager.Common.UploadImageTimeout); - Networking.SetWebProxy(SettingManager.Local.ProxyType, - SettingManager.Local.ProxyAddress, SettingManager.Local.ProxyPort, - SettingManager.Local.ProxyUser, SettingManager.Local.ProxyPassword); - Networking.ForceIPv4 = SettingManager.Common.ForceIPv4; + this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId); + this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; + this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; - ImageSelector.Reset(tw, this.tw.Configuration); + this.ImageSelector.Reset(this.tw, this.tw.Configuration); try { - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - ListTab.DrawItem -= ListTab_DrawItem; - ListTab.DrawMode = TabDrawMode.Normal; - ListTab.ImageList = this.TabImage; + this.ListTab.DrawItem -= this.ListTab_DrawItem; + this.ListTab.DrawMode = TabDrawMode.Normal; + this.ListTab.ImageList = this.TabImage; } else { - ListTab.DrawItem -= ListTab_DrawItem; - ListTab.DrawItem += ListTab_DrawItem; - ListTab.DrawMode = TabDrawMode.OwnerDrawFixed; - ListTab.ImageList = null; + this.ListTab.DrawItem -= this.ListTab_DrawItem; + this.ListTab.DrawItem += this.ListTab_DrawItem; + this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed; + this.ListTab.ImageList = null; } } catch (Exception ex) @@ -3515,13 +2610,13 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) try { - if (!SettingManager.Common.UnreadManage) + if (!this.settings.Common.UnreadManage) { - ReadedStripMenuItem.Enabled = false; - UnreadStripMenuItem.Enabled = false; - if (SettingManager.Common.TabIconDisp) + this.ReadedStripMenuItem.Enabled = false; + this.UnreadStripMenuItem.Enabled = false; + if (this.settings.Common.TabIconDisp) { - foreach (TabPage myTab in ListTab.TabPages) + foreach (TabPage myTab in this.ListTab.TabPages) { myTab.ImageIndex = -1; } @@ -3529,8 +2624,8 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) } else { - ReadedStripMenuItem.Enabled = true; - UnreadStripMenuItem.Enabled = true; + this.ReadedStripMenuItem.Enabled = true; + this.UnreadStripMenuItem.Enabled = true; } } catch (Exception ex) @@ -3541,59 +2636,33 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) } // タブの表示位置の決定 - SetTabAlignment(); - - SplitContainer1.IsPanelInverted = !SettingManager.Common.StatusAreaAtBottom; - - var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance; - imgazyobizinet.Enabled = SettingManager.Common.EnableImgAzyobuziNet; - imgazyobizinet.DisabledInDM = SettingManager.Common.ImgAzyobuziNetDisabledInDM; - - this.NewPostPopMenuItem.Checked = SettingManager.Common.NewAllPop; - this.NotifyFileMenuItem.Checked = SettingManager.Common.NewAllPop; - this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound; - this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound; - _fntUnread = SettingManager.Local.FontUnread; - _clUnread = SettingManager.Local.ColorUnread; - _fntReaded = SettingManager.Local.FontRead; - _clReaded = SettingManager.Local.ColorRead; - _clFav = SettingManager.Local.ColorFav; - _clOWL = SettingManager.Local.ColorOWL; - _clRetweet = SettingManager.Local.ColorRetweet; - _fntDetail = SettingManager.Local.FontDetail; - _clDetail = SettingManager.Local.ColorDetail; - _clDetailLink = SettingManager.Local.ColorDetailLink; - _clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor; - _clSelf = SettingManager.Local.ColorSelf; - _clAtSelf = SettingManager.Local.ColorAtSelf; - _clTarget = SettingManager.Local.ColorTarget; - _clAtTarget = SettingManager.Local.ColorAtTarget; - _clAtFromTarget = SettingManager.Local.ColorAtFromTarget; - _clAtTo = SettingManager.Local.ColorAtTo; - _clListBackcolor = SettingManager.Local.ColorListBackcolor; - _clInputBackcolor = SettingManager.Local.ColorInputBackcolor; - _clInputFont = SettingManager.Local.ColorInputFont; - _fntInputFont = SettingManager.Local.FontInputFont; - _brsBackColorMine.Dispose(); - _brsBackColorAt.Dispose(); - _brsBackColorYou.Dispose(); - _brsBackColorAtYou.Dispose(); - _brsBackColorAtFromTarget.Dispose(); - _brsBackColorAtTo.Dispose(); - _brsBackColorNone.Dispose(); - _brsBackColorMine = new SolidBrush(_clSelf); - _brsBackColorAt = new SolidBrush(_clAtSelf); - _brsBackColorYou = new SolidBrush(_clTarget); - _brsBackColorAtYou = new SolidBrush(_clAtTarget); - _brsBackColorAtFromTarget = new SolidBrush(_clAtFromTarget); - _brsBackColorAtTo = new SolidBrush(_clAtTo); - _brsBackColorNone = new SolidBrush(_clListBackcolor); + this.SetTabAlignment(); + + this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom; + + var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet; + imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet; + imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM; + + this.NewPostPopMenuItem.Checked = this.settings.Common.NewAllPop; + this.NotifyFileMenuItem.Checked = this.settings.Common.NewAllPop; + this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound; + this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound; + + var newTheme = new ThemeManager(this.settings.Local); + (var oldTheme, this.themeManager) = (this.themeManager, newTheme); + this.tweetDetailsView.Theme = this.themeManager; + if (this.listDrawer != null) + this.listDrawer.Theme = this.themeManager; + oldTheme.Dispose(); try { - if (StatusText.Focused) StatusText.BackColor = _clInputBackcolor; - StatusText.Font = _fntInputFont; - StatusText.ForeColor = _clInputFont; + if (this.StatusText.Focused) + this.StatusText.BackColor = this.themeManager.ColorInputBackcolor; + + this.StatusText.Font = this.themeManager.FontInputFont; + this.StatusText.ForeColor = this.themeManager.ColorInputFont; } catch (Exception ex) { @@ -3602,7 +2671,7 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) try { - InitDetailHtmlFormat(); + this.InitDetailHtmlFormat(); } catch (Exception ex) { @@ -3613,9 +2682,9 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) try { - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { var tabPage = this.ListTab.TabPages[index]; if (tab.UnreadCount == 0) @@ -3634,23 +2703,18 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) try { - var oldIconCol = _iconCol; - - if (SettingManager.Common.IconSize != oldIconSz) - ApplyListViewIconSize(SettingManager.Common.IconSize); + this.ApplyListViewIconSize(this.settings.Common.IconSize); - foreach (TabPage tp in ListTab.TabPages) + foreach (TabPage tp in this.ListTab.TabPages) { var lst = (DetailsListView)tp.Tag; using (ControlTransaction.Update(lst)) { - lst.GridLines = SettingManager.Common.ShowGrid; - lst.Font = _fntReaded; - lst.BackColor = _clListBackcolor; + lst.GridLines = this.settings.Common.ShowGrid; - if (_iconCol != oldIconCol) - ResetColumns(lst); + if (this.Use2ColumnsMode != oldIconCol) + this.ResetColumns(lst); } } } @@ -3661,53 +2725,48 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) throw; } - SetMainWindowTitle(); - SetNotifyIconText(); + this.SetMainWindowTitle(); + this.SetNotifyIconText(); - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); this.CurrentListView.Refresh(); - ListTab.Refresh(); + this.ListTab.Refresh(); - _hookGlobalHotkey.UnregisterAllOriginalHotkey(); - if (SettingManager.Common.HotkeyEnabled) + this.hookGlobalHotkey.UnregisterAllOriginalHotkey(); + if (this.settings.Common.HotkeyEnabled) { - ///グローバルホットキーの登録。設定で変更可能にするかも + // グローバルホットキーの登録。設定で変更可能にするかも var modKey = HookGlobalHotkey.ModKeys.None; - if ((SettingManager.Common.HotkeyModifier & Keys.Alt) == Keys.Alt) + if ((this.settings.Common.HotkeyModifier & Keys.Alt) == Keys.Alt) modKey |= HookGlobalHotkey.ModKeys.Alt; - if ((SettingManager.Common.HotkeyModifier & Keys.Control) == Keys.Control) + if ((this.settings.Common.HotkeyModifier & Keys.Control) == Keys.Control) modKey |= HookGlobalHotkey.ModKeys.Ctrl; - if ((SettingManager.Common.HotkeyModifier & Keys.Shift) == Keys.Shift) - modKey |= HookGlobalHotkey.ModKeys.Shift; - if ((SettingManager.Common.HotkeyModifier & Keys.LWin) == Keys.LWin) + if ((this.settings.Common.HotkeyModifier & Keys.Shift) == Keys.Shift) + modKey |= HookGlobalHotkey.ModKeys.Shift; + if ((this.settings.Common.HotkeyModifier & Keys.LWin) == Keys.LWin) modKey |= HookGlobalHotkey.ModKeys.Win; - _hookGlobalHotkey.RegisterOriginalHotkey(SettingManager.Common.HotkeyKey, SettingManager.Common.HotkeyValue, modKey); + this.hookGlobalHotkey.RegisterOriginalHotkey(this.settings.Common.HotkeyKey, this.settings.Common.HotkeyValue, modKey); } - if (SettingManager.Common.IsUseNotifyGrowl) gh.RegisterGrowl(); + if (this.settings.Common.IsUseNotifyGrowl) this.gh.RegisterGrowl(); try { - StatusText_TextChanged(this.StatusText, EventArgs.Empty); + this.StatusText_TextChanged(this.StatusText, EventArgs.Empty); } catch (Exception) { } } } - else - { - // キャンセル時は Twitter クラスの認証情報を画面表示前の状態に戻す - this.tw.Initialize(oldUser.AccessToken, oldUser.AccessTokenSecret, oldUser.Username, oldUser.UserId); - } Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid; - this.TopMost = SettingManager.Common.AlwaysTop; - SaveConfigsAll(false); + this.TopMost = this.settings.Common.AlwaysTop; + this.SaveConfigsAll(false); - if (tw.UserId != oldUser.UserId) - await this.doGetFollowersMenu(); + if (this.tw.UserId != previousUserId) + await this.DoGetFollowersMenu(); } /// @@ -3715,50 +2774,25 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) /// private void SetTabAlignment() { - var newAlignment = SettingManager.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top; - if (ListTab.Alignment == newAlignment) return; + var newAlignment = this.settings.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top; + if (this.ListTab.Alignment == newAlignment) return; - // 各タブのリスト上の選択位置などを退避 - var listSelections = this.SaveListViewSelection(); + // リスト上の選択位置などを退避 + var currentListViewState = this.listViewState[this.CurrentTabName]; + currentListViewState.Save(this.ListLockMenuItem.Checked); - ListTab.Alignment = newAlignment; + this.ListTab.Alignment = newAlignment; - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) - { - var lst = (DetailsListView)this.ListTab.TabPages[index].Tag; - using (ControlTransaction.Update(lst)) - { - // 選択位置などを復元 - this.RestoreListViewSelection(lst, tab, listSelections[tab.TabName]); - } - } + currentListViewState.Restore(); } private void ApplyListViewIconSize(MyCommon.IconSizes iconSz) - { - // アイコンサイズの再設定 - this._iconSz = iconSz switch - { - MyCommon.IconSizes.IconNone => 0, - MyCommon.IconSizes.Icon16 => 16, - MyCommon.IconSizes.Icon24 => 26, - MyCommon.IconSizes.Icon48 => 48, - MyCommon.IconSizes.Icon48_2 => 48, - _ => throw new InvalidEnumArgumentException(nameof(iconSz), (int)iconSz, typeof(MyCommon.IconSizes)), - }; - this._iconCol = iconSz == MyCommon.IconSizes.Icon48_2; - - if (_iconSz > 0) - { - // ディスプレイの DPI 設定を考慮したサイズを設定する - _listViewImageList.ImageSize = new Size( - 1, - (int)Math.Ceiling(this._iconSz * this.CurrentScaleFactor.Height)); - } - else - { - _listViewImageList.ImageSize = new Size(1, 1); - } + { + // アイコンサイズの再設定 + if (this.listDrawer != null) + this.listDrawer.IconSize = iconSz; + + this.listCache?.PurgeCache(); } private void ResetColumns(DetailsListView list) @@ -3767,59 +2801,59 @@ private void ResetColumns(DetailsListView list) using (ControlTransaction.Layout(list, false)) { // カラムヘッダの再設定 - list.ColumnClick -= MyList_ColumnClick; - list.DrawColumnHeader -= MyList_DrawColumnHeader; - list.ColumnReordered -= MyList_ColumnReordered; - list.ColumnWidthChanged -= MyList_ColumnWidthChanged; + list.ColumnClick -= this.MyList_ColumnClick; + list.DrawColumnHeader -= this.MyList_DrawColumnHeader; + list.ColumnReordered -= this.MyList_ColumnReordered; + list.ColumnWidthChanged -= this.MyList_ColumnWidthChanged; var cols = list.Columns.Cast().ToList(); list.Columns.Clear(); cols.ForEach(col => col.Dispose()); cols.Clear(); - InitColumns(list, true); + this.InitColumns(list, true); - list.ColumnClick += MyList_ColumnClick; - list.DrawColumnHeader += MyList_DrawColumnHeader; - list.ColumnReordered += MyList_ColumnReordered; - list.ColumnWidthChanged += MyList_ColumnWidthChanged; + list.ColumnClick += this.MyList_ColumnClick; + list.DrawColumnHeader += this.MyList_DrawColumnHeader; + list.ColumnReordered += this.MyList_ColumnReordered; + list.ColumnWidthChanged += this.MyList_ColumnWidthChanged; } } public void AddNewTabForSearch(string searchWord) { - //同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了 - foreach (var tb in _statuses.GetTabsByType()) + // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了 + foreach (var tb in this.statuses.GetTabsByType()) { if (tb.SearchWords == searchWord && MyCommon.IsNullOrEmpty(tb.SearchLang)) { - var tabIndex = this._statuses.Tabs.IndexOf(tb); + var tabIndex = this.statuses.Tabs.IndexOf(tb); this.ListTab.SelectedIndex = tabIndex; return; } } - //ユニークなタブ名生成 + // ユニークなタブ名生成 var tabName = searchWord; for (var i = 0; i <= 100; i++) { - if (_statuses.ContainsTab(tabName)) + if (this.statuses.ContainsTab(tabName)) tabName += "_"; else break; } - //タブ追加 + // タブ追加 var tab = new PublicSearchTabModel(tabName); - _statuses.AddTab(tab); - AddNewTab(tab, startup: false); - //追加したタブをアクティブに - ListTab.SelectedIndex = this._statuses.Tabs.Count - 1; - //検索条件の設定 + this.statuses.AddTab(tab); + this.AddNewTab(tab, startup: false); + // 追加したタブをアクティブに + this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1; + // 検索条件の設定 var tabPage = this.CurrentTabPage; var cmb = (ComboBox)tabPage.Controls["panelSearch"].Controls["comboSearch"]; cmb.Items.Add(searchWord); cmb.Text = searchWord; - SaveConfigsTabs(); - //検索実行 + this.SaveConfigsTabs(); + // 検索実行 this.SearchButton_Click(tabPage.Controls["panelSearch"].Controls["comboSearch"], EventArgs.Empty); } @@ -3830,72 +2864,79 @@ private async Task ShowUserTimeline() await this.AddNewTabForUserTimeline(post.ScreenName); } + private async Task ShowRetweeterTimeline() + { + var retweetedBy = this.CurrentPost?.RetweetedBy; + if (retweetedBy == null || !this.ExistCurrentPost) return; + await this.AddNewTabForUserTimeline(retweetedBy); + } + private void SearchComboBox_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Escape) { - RemoveSpecifiedTab(this.CurrentTabName, false); - SaveConfigsTabs(); + this.RemoveSpecifiedTab(this.CurrentTabName, false); + this.SaveConfigsTabs(); e.SuppressKeyPress = true; } } public async Task AddNewTabForUserTimeline(string user) { - //同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了 - foreach (var tb in _statuses.GetTabsByType()) + // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了 + foreach (var tb in this.statuses.GetTabsByType()) { if (tb.ScreenName == user) { - var tabIndex = this._statuses.Tabs.IndexOf(tb); + var tabIndex = this.statuses.Tabs.IndexOf(tb); this.ListTab.SelectedIndex = tabIndex; return; } } - //ユニークなタブ名生成 + // ユニークなタブ名生成 var tabName = "user:" + user; - while (_statuses.ContainsTab(tabName)) + while (this.statuses.ContainsTab(tabName)) { tabName += "_"; } - //タブ追加 + // タブ追加 var tab = new UserTimelineTabModel(tabName, user); - this._statuses.AddTab(tab); + this.statuses.AddTab(tab); this.AddNewTab(tab, startup: false); - //追加したタブをアクティブに - ListTab.SelectedIndex = this._statuses.Tabs.Count - 1; - SaveConfigsTabs(); - //検索実行 + // 追加したタブをアクティブに + this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1; + this.SaveConfigsTabs(); + // 検索実行 await this.RefreshTabAsync(tab); } public bool AddNewTab(TabModel tab, bool startup) { - //重複チェック + // 重複チェック if (this.ListTab.TabPages.Cast().Any(x => x.Text == tab.TabName)) return false; - //新規タブ名チェック + // 新規タブ名チェック if (tab.TabName == Properties.Resources.AddNewTabText1) return false; - var _tabPage = new TabPage(); - var _listCustom = new DetailsListView(); + var tabPage = new TabPage(); + var listCustom = new DetailsListView(); - var cnt = this._statuses.Tabs.Count; + var cnt = this.statuses.Tabs.Count; - ///ToDo:Create and set controls follow tabtypes + // ToDo:Create and set controls follow tabtypes - using (ControlTransaction.Update(_listCustom)) + using (ControlTransaction.Update(listCustom)) using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false)) using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false)) using (ControlTransaction.Layout(this.SplitContainer1, false)) using (ControlTransaction.Layout(this.ListTab, false)) using (ControlTransaction.Layout(this)) - using (ControlTransaction.Layout(_tabPage, false)) + using (ControlTransaction.Layout(tabPage, false)) { - _tabPage.Controls.Add(_listCustom); + tabPage.Controls.Add(listCustom); - /// UserTimeline関連 + // UserTimeline関連 var userTab = tab as UserTimelineTabModel; var listTab = tab as ListTimelineTabModel; var searchTab = tab as PublicSearchTabModel; @@ -3922,9 +2963,9 @@ public bool AddNewTab(TabModel tab, bool startup) { label.Height = tmpComboBox.Height; } - _tabPage.Controls.Add(label); + tabPage.Controls.Add(label); } - /// 検索関連の準備 + // 検索関連の準備 else if (searchTab != null) { var pnl = new Panel(); @@ -3944,8 +2985,8 @@ public bool AddNewTab(TabModel tab, bool startup) pnl.TabIndex = 0; pnl.Dock = DockStyle.Top; pnl.Height = cmb.Height; - pnl.Enter += SearchControls_Enter; - pnl.Leave += SearchControls_Leave; + pnl.Enter += this.SearchControls_Enter; + pnl.Leave += this.SearchControls_Leave; cmb.Text = ""; cmb.Anchor = AnchorStyles.Left | AnchorStyles.Right; @@ -3956,7 +2997,7 @@ public bool AddNewTab(TabModel tab, bool startup) cmb.TabStop = false; cmb.TabIndex = 1; cmb.AutoCompleteMode = AutoCompleteMode.None; - cmb.KeyDown += SearchComboBox_KeyDown; + cmb.KeyDown += this.SearchComboBox_KeyDown; cmbLang.Text = ""; cmbLang.Anchor = AnchorStyles.Left | AnchorStyles.Right; @@ -4001,7 +3042,7 @@ public bool AddNewTab(TabModel tab, bool startup) btn.Dock = DockStyle.Right; btn.TabStop = false; btn.TabIndex = 3; - btn.Click += SearchButton_Click; + btn.Click += this.SearchButton_Click; if (!MyCommon.IsNullOrEmpty(searchTab.SearchWords)) { @@ -4011,227 +3052,219 @@ public bool AddNewTab(TabModel tab, bool startup) cmbLang.Text = searchTab.SearchLang; - _tabPage.Controls.Add(pnl); - } - } - - _tabPage.Tag = _listCustom; - this.ListTab.Controls.Add(_tabPage); - - _tabPage.Location = new Point(4, 4); - _tabPage.Name = "CTab" + cnt; - _tabPage.Size = new Size(380, 260); - _tabPage.TabIndex = 2 + cnt; - _tabPage.Text = tab.TabName; - _tabPage.UseVisualStyleBackColor = true; - _tabPage.AccessibleRole = AccessibleRole.PageTab; - - _listCustom.AccessibleName = Properties.Resources.AddNewTab_ListView_AccessibleName; - _listCustom.TabIndex = 1; - _listCustom.AllowColumnReorder = true; - _listCustom.ContextMenuStrip = this.ContextMenuOperate; - _listCustom.ColumnHeaderContextMenuStrip = this.ContextMenuColumnHeader; - _listCustom.Dock = DockStyle.Fill; - _listCustom.FullRowSelect = true; - _listCustom.HideSelection = false; - _listCustom.Location = new Point(0, 0); - _listCustom.Margin = new Padding(0); - _listCustom.Name = "CList" + Environment.TickCount; - _listCustom.ShowItemToolTips = true; - _listCustom.Size = new Size(380, 260); - _listCustom.UseCompatibleStateImageBehavior = false; - _listCustom.View = View.Details; - _listCustom.OwnerDraw = true; - _listCustom.VirtualMode = true; - _listCustom.Font = _fntReaded; - _listCustom.BackColor = _clListBackcolor; - - _listCustom.GridLines = SettingManager.Common.ShowGrid; - _listCustom.AllowDrop = true; - - _listCustom.SmallImageList = _listViewImageList; - - InitColumns(_listCustom, startup); - - _listCustom.SelectedIndexChanged += MyList_SelectedIndexChanged; - _listCustom.MouseDoubleClick += MyList_MouseDoubleClick; - _listCustom.ColumnClick += MyList_ColumnClick; - _listCustom.DrawColumnHeader += MyList_DrawColumnHeader; - _listCustom.DragDrop += TweenMain_DragDrop; - _listCustom.DragEnter += TweenMain_DragEnter; - _listCustom.DragOver += TweenMain_DragOver; - _listCustom.DrawItem += MyList_DrawItem; - _listCustom.MouseClick += MyList_MouseClick; - _listCustom.ColumnReordered += MyList_ColumnReordered; - _listCustom.ColumnWidthChanged += MyList_ColumnWidthChanged; - _listCustom.CacheVirtualItems += MyList_CacheVirtualItems; - _listCustom.RetrieveVirtualItem += MyList_RetrieveVirtualItem; - _listCustom.DrawSubItem += MyList_DrawSubItem; - _listCustom.HScrolled += MyList_HScrolled; - } + tabPage.Controls.Add(pnl); + } + } + + tabPage.Tag = listCustom; + this.ListTab.Controls.Add(tabPage); + + tabPage.Location = new Point(4, 4); + tabPage.Name = "CTab" + cnt; + tabPage.Size = new Size(380, 260); + tabPage.TabIndex = 2 + cnt; + tabPage.Text = tab.TabName; + tabPage.UseVisualStyleBackColor = true; + tabPage.AccessibleRole = AccessibleRole.PageTab; + + listCustom.AccessibleName = Properties.Resources.AddNewTab_ListView_AccessibleName; + listCustom.TabIndex = 1; + listCustom.AllowColumnReorder = true; + listCustom.ContextMenuStrip = this.ContextMenuOperate; + listCustom.ColumnHeaderContextMenuStrip = this.ContextMenuColumnHeader; + listCustom.Dock = DockStyle.Fill; + listCustom.FullRowSelect = true; + listCustom.HideSelection = false; + listCustom.Location = new Point(0, 0); + listCustom.Margin = new Padding(0); + listCustom.Name = "CList" + Environment.TickCount; + listCustom.ShowItemToolTips = true; + listCustom.Size = new Size(380, 260); + listCustom.UseCompatibleStateImageBehavior = false; + listCustom.View = View.Details; + listCustom.OwnerDraw = true; + listCustom.VirtualMode = true; + + listCustom.GridLines = this.settings.Common.ShowGrid; + listCustom.AllowDrop = true; + + this.InitColumns(listCustom, startup); + + listCustom.SelectedIndexChanged += this.MyList_SelectedIndexChanged; + listCustom.MouseDoubleClick += this.MyList_MouseDoubleClick; + listCustom.ColumnClick += this.MyList_ColumnClick; + listCustom.DrawColumnHeader += this.MyList_DrawColumnHeader; + listCustom.DragDrop += this.TweenMain_DragDrop; + listCustom.DragEnter += this.TweenMain_DragEnter; + listCustom.DragOver += this.TweenMain_DragOver; + listCustom.MouseClick += this.MyList_MouseClick; + listCustom.ColumnReordered += this.MyList_ColumnReordered; + listCustom.ColumnWidthChanged += this.MyList_ColumnWidthChanged; + listCustom.HScrolled += this.MyList_HScrolled; + } + + var state = new TimelineListViewState(listCustom, tab); + this.listViewState[tab.TabName] = state; return true; } - public bool RemoveSpecifiedTab(string TabName, bool confirm) + public bool RemoveSpecifiedTab(string tabName, bool confirm) { - var tabInfo = _statuses.GetTabByName(TabName); + var tabInfo = this.statuses.GetTabByName(tabName); if (tabInfo == null || tabInfo.IsDefaultTabType || tabInfo.Protected) return false; if (confirm) { var tmp = string.Format(Properties.Resources.RemoveSpecifiedTabText1, Environment.NewLine); - if (MessageBox.Show(tmp, TabName + " " + Properties.Resources.RemoveSpecifiedTabText2, - MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Cancel) + var result = MessageBox.Show( + tmp, + tabName + " " + Properties.Resources.RemoveSpecifiedTabText2, + MessageBoxButtons.OKCancel, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button2); + if (result == DialogResult.Cancel) { return false; } } - var tabIndex = this._statuses.Tabs.IndexOf(TabName); + var tabIndex = this.statuses.Tabs.IndexOf(tabName); if (tabIndex == -1) return false; - var _tabPage = this.ListTab.TabPages[tabIndex]; + var tabPage = this.ListTab.TabPages[tabIndex]; + + this.SetListProperty(); // 他のタブに列幅等を反映 - SetListProperty(); //他のタブに列幅等を反映 + this.listViewState.Remove(tabName); - //オブジェクトインスタンスの削除 - var _listCustom = (DetailsListView)_tabPage.Tag; - _tabPage.Tag = null; + // オブジェクトインスタンスの削除 + var listCustom = (DetailsListView)tabPage.Tag; + tabPage.Tag = null; using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false)) using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false)) using (ControlTransaction.Layout(this.SplitContainer1, false)) using (ControlTransaction.Layout(this.ListTab, false)) using (ControlTransaction.Layout(this)) - using (ControlTransaction.Layout(_tabPage, false)) + using (ControlTransaction.Layout(tabPage, false)) { - if (this.CurrentTabName == TabName) + if (this.CurrentTabName == tabName) { - this.ListTab.SelectTab((this._beforeSelectedTab != null && this.ListTab.TabPages.Contains(this._beforeSelectedTab)) ? this._beforeSelectedTab : this.ListTab.TabPages[0]); - this._beforeSelectedTab = null; + this.ListTab.SelectTab((this.beforeSelectedTab != null && this.ListTab.TabPages.Contains(this.beforeSelectedTab)) ? this.beforeSelectedTab : this.ListTab.TabPages[0]); + this.beforeSelectedTab = null; } - this.ListTab.Controls.Remove(_tabPage); + this.ListTab.Controls.Remove(tabPage); // 後付けのコントロールを破棄 if (tabInfo.TabType == MyCommon.TabUsageType.UserTimeline || tabInfo.TabType == MyCommon.TabUsageType.Lists) { - using var label = _tabPage.Controls["labelUser"]; - _tabPage.Controls.Remove(label); + using var label = tabPage.Controls["labelUser"]; + tabPage.Controls.Remove(label); } else if (tabInfo.TabType == MyCommon.TabUsageType.PublicSearch) { - using var pnl = _tabPage.Controls["panelSearch"]; + using var pnl = tabPage.Controls["panelSearch"]; - pnl.Enter -= SearchControls_Enter; - pnl.Leave -= SearchControls_Leave; - _tabPage.Controls.Remove(pnl); + pnl.Enter -= this.SearchControls_Enter; + pnl.Leave -= this.SearchControls_Leave; + tabPage.Controls.Remove(pnl); foreach (Control ctrl in pnl.Controls) { if (ctrl.Name == "buttonSearch") { - ctrl.Click -= SearchButton_Click; + ctrl.Click -= this.SearchButton_Click; } else if (ctrl.Name == "comboSearch") { - ctrl.KeyDown -= SearchComboBox_KeyDown; + ctrl.KeyDown -= this.SearchComboBox_KeyDown; } pnl.Controls.Remove(ctrl); ctrl.Dispose(); } } - _tabPage.Controls.Remove(_listCustom); - - _listCustom.SelectedIndexChanged -= MyList_SelectedIndexChanged; - _listCustom.MouseDoubleClick -= MyList_MouseDoubleClick; - _listCustom.ColumnClick -= MyList_ColumnClick; - _listCustom.DrawColumnHeader -= MyList_DrawColumnHeader; - _listCustom.DragDrop -= TweenMain_DragDrop; - _listCustom.DragEnter -= TweenMain_DragEnter; - _listCustom.DragOver -= TweenMain_DragOver; - _listCustom.DrawItem -= MyList_DrawItem; - _listCustom.MouseClick -= MyList_MouseClick; - _listCustom.ColumnReordered -= MyList_ColumnReordered; - _listCustom.ColumnWidthChanged -= MyList_ColumnWidthChanged; - _listCustom.CacheVirtualItems -= MyList_CacheVirtualItems; - _listCustom.RetrieveVirtualItem -= MyList_RetrieveVirtualItem; - _listCustom.DrawSubItem -= MyList_DrawSubItem; - _listCustom.HScrolled -= MyList_HScrolled; - - var cols = _listCustom.Columns.Cast().ToList(); - _listCustom.Columns.Clear(); + tabPage.Controls.Remove(listCustom); + + listCustom.SelectedIndexChanged -= this.MyList_SelectedIndexChanged; + listCustom.MouseDoubleClick -= this.MyList_MouseDoubleClick; + listCustom.ColumnClick -= this.MyList_ColumnClick; + listCustom.DrawColumnHeader -= this.MyList_DrawColumnHeader; + listCustom.DragDrop -= this.TweenMain_DragDrop; + listCustom.DragEnter -= this.TweenMain_DragEnter; + listCustom.DragOver -= this.TweenMain_DragOver; + listCustom.MouseClick -= this.MyList_MouseClick; + listCustom.ColumnReordered -= this.MyList_ColumnReordered; + listCustom.ColumnWidthChanged -= this.MyList_ColumnWidthChanged; + listCustom.HScrolled -= this.MyList_HScrolled; + + var cols = listCustom.Columns.Cast().ToList(); + listCustom.Columns.Clear(); cols.ForEach(col => col.Dispose()); cols.Clear(); - _listCustom.ContextMenuStrip = null; - _listCustom.ColumnHeaderContextMenuStrip = null; - _listCustom.Font = null; + listCustom.ContextMenuStrip = null; + listCustom.ColumnHeaderContextMenuStrip = null; + listCustom.Font = null; - _listCustom.SmallImageList = null; - _listCustom.ListViewItemSorter = null; + listCustom.SmallImageList = null; + listCustom.ListViewItemSorter = null; // キャッシュのクリア - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); } - _tabPage.Dispose(); - _listCustom.Dispose(); - _statuses.RemoveTab(TabName); - - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) - { - var lst = (DetailsListView)this.ListTab.TabPages[index].Tag; - lst.VirtualListSize = tab.AllCount; - } + tabPage.Dispose(); + listCustom.Dispose(); + this.statuses.RemoveTab(tabName); return true; } private void ListTab_Deselected(object sender, TabControlEventArgs e) { - this.PurgeListViewItemCache(); - _beforeSelectedTab = e.TabPage; + this.listCache?.PurgeCache(); + this.beforeSelectedTab = e.TabPage; } private void ListTab_MouseMove(object sender, MouseEventArgs e) { - //タブのD&D + // タブのD&D - if (!SettingManager.Common.TabMouseLock && e.Button == MouseButtons.Left && _tabDrag) + if (!this.settings.Common.TabMouseLock && e.Button == MouseButtons.Left && this.tabDrag) { var tn = ""; - var dragEnableRectangle = new Rectangle(_tabMouseDownPoint.X - (SystemInformation.DragSize.Width / 2), _tabMouseDownPoint.Y - (SystemInformation.DragSize.Height / 2), SystemInformation.DragSize.Width, SystemInformation.DragSize.Height); + var dragEnableRectangle = new Rectangle(this.tabMouseDownPoint.X - (SystemInformation.DragSize.Width / 2), this.tabMouseDownPoint.Y - (SystemInformation.DragSize.Height / 2), SystemInformation.DragSize.Width, SystemInformation.DragSize.Height); if (!dragEnableRectangle.Contains(e.Location)) { - //タブが多段の場合にはMouseDownの前の段階で選択されたタブの段が変わっているので、このタイミングでカーソルの位置からタブを判定出来ない。 + // タブが多段の場合にはMouseDownの前の段階で選択されたタブの段が変わっているので、このタイミングでカーソルの位置からタブを判定出来ない。 tn = this.CurrentTabName; } if (MyCommon.IsNullOrEmpty(tn)) return; - var tabIndex = this._statuses.Tabs.IndexOf(tn); + var tabIndex = this.statuses.Tabs.IndexOf(tn); if (tabIndex != -1) { var tabPage = this.ListTab.TabPages[tabIndex]; - ListTab.DoDragDrop(tabPage, DragDropEffects.All); + this.ListTab.DoDragDrop(tabPage, DragDropEffects.All); } } else { - _tabDrag = false; + this.tabDrag = false; } var cpos = new Point(e.X, e.Y); - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { - var rect = ListTab.GetTabRect(index); + var rect = this.ListTab.GetTabRect(index); if (rect.Contains(cpos)) { - _rclickTabName = tab.TabName; + this.rclickTabName = tab.TabName; break; } } @@ -4239,19 +3272,19 @@ private void ListTab_MouseMove(object sender, MouseEventArgs e) private void ListTab_SelectedIndexChanged(object sender, EventArgs e) { - SetMainWindowTitle(); - SetStatusLabelUrl(); - SetApiStatusLabel(); - if (ListTab.Focused || ((Control)this.CurrentTabPage.Tag).Focused) - this.Tag = ListTab.Tag; - TabMenuControl(this.CurrentTabName); + this.SetMainWindowTitle(); + this.SetStatusLabelUrl(); + this.SetApiStatusLabel(); + if (this.ListTab.Focused || ((Control)this.CurrentTabPage.Tag).Focused) + this.Tag = this.ListTab.Tag; + this.TabMenuControl(this.CurrentTabName); this.PushSelectPostChain(); - DispSelectedPost(); + this.DispSelectedPost(); } private void SetListProperty() { - if (!_isColumnChanged) return; + if (!this.isColumnChanged) return; var currentListView = this.CurrentListView; @@ -4268,8 +3301,8 @@ private void SetListProperty() } } - //列幅、列並びを他のタブに設定 - foreach (TabPage tb in ListTab.TabPages) + // 列幅、列並びを他のタブに設定 + foreach (TabPage tb in this.ListTab.TabPages) { if (tb.Text == this.CurrentTabName) continue; @@ -4285,25 +3318,25 @@ private void SetListProperty() } } - _isColumnChanged = false; + this.isColumnChanged = false; } private void StatusText_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '@') { - if (!SettingManager.Common.UseAtIdSupplement) return; - //@マーク - var cnt = AtIdSupl.ItemCount; - ShowSuplDialog(StatusText, AtIdSupl); - if (cnt != AtIdSupl.ItemCount) + if (!this.settings.Common.UseAtIdSupplement) return; + // @マーク + var cnt = this.AtIdSupl.ItemCount; + this.ShowSuplDialog(this.StatusText, this.AtIdSupl); + if (cnt != this.AtIdSupl.ItemCount) this.MarkSettingAtIdModified(); e.Handled = true; } else if (e.KeyChar == '#') { - if (!SettingManager.Common.UseHashSupplement) return; - ShowSuplDialog(StatusText, HashSupl); + if (!this.settings.Common.UseHashSupplement) return; + this.ShowSuplDialog(this.StatusText, this.HashSupl); e.Handled = true; } } @@ -4325,13 +3358,13 @@ public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset, str { dialog.ShowDialog(); } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; var selStart = owner.SelectionStart; var fHalf = ""; var eHalf = ""; if (dialog.DialogResult == DialogResult.OK) { - if (!MyCommon.IsNullOrEmpty(dialog.inputText)) + if (!MyCommon.IsNullOrEmpty(dialog.InputText)) { if (selStart > 0) { @@ -4341,8 +3374,8 @@ public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset, str { eHalf = owner.Text.Substring(selStart); } - owner.Text = fHalf + dialog.inputText + eHalf; - owner.SelectionStart = selStart + dialog.inputText.Length; + owner.Text = fHalf + dialog.InputText + eHalf; + owner.SelectionStart = selStart + dialog.InputText.Length; } } else @@ -4366,13 +3399,13 @@ public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset, str private void StatusText_KeyUp(object sender, KeyEventArgs e) { - //スペースキーで未読ジャンプ + // スペースキーで未読ジャンプ if (!e.Alt && !e.Control && !e.Shift) { if (e.KeyCode == Keys.Space || e.KeyCode == Keys.ProcessKey) { var isSpace = false; - foreach (var c in StatusText.Text) + foreach (var c in this.StatusText.Text) { if (c == ' ' || c == ' ') { @@ -4387,8 +3420,8 @@ private void StatusText_KeyUp(object sender, KeyEventArgs e) if (isSpace) { e.Handled = true; - StatusText.Text = ""; - JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty); + this.StatusText.Text = ""; + this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty); } } } @@ -4397,21 +3430,21 @@ private void StatusText_KeyUp(object sender, KeyEventArgs e) private void StatusText_TextChanged(object sender, EventArgs e) { - //文字数カウント + // 文字数カウント var pLen = this.GetRestStatusCount(this.FormatStatusTextExtended(this.StatusText.Text)); - lblLen.Text = pLen.ToString(); + this.lblLen.Text = pLen.ToString(); if (pLen < 0) { - StatusText.ForeColor = Color.Red; + this.StatusText.ForeColor = Color.Red; } else { - StatusText.ForeColor = _clInputFont; + this.StatusText.ForeColor = this.themeManager.ColorInputFont; } this.StatusText.AccessibleDescription = string.Format(Properties.Resources.StatusText_AccessibleDescription, pLen); - if (MyCommon.IsNullOrEmpty(StatusText.Text)) + if (MyCommon.IsNullOrEmpty(this.StatusText.Text)) { this.inReplyTo = null; } @@ -4448,28 +3481,28 @@ internal static bool TextContainsOnlyMentions(string text) ///
private string RemoveAutoPopuratedMentions(string statusText, out long[] autoPopulatedUserIds) { - var _autoPopulatedUserIds = new List(); + var autoPopulatedUserIdList = new List(); - var replyToPost = this.inReplyTo != null ? this._statuses[this.inReplyTo.Value.StatusId] : null; + var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null; if (replyToPost != null) { if (statusText.StartsWith($"@{replyToPost.ScreenName} ", StringComparison.Ordinal)) { statusText = statusText.Substring(replyToPost.ScreenName.Length + 2); - _autoPopulatedUserIds.Add(replyToPost.UserId); + autoPopulatedUserIdList.Add(replyToPost.UserId); foreach (var (userId, screenName) in replyToPost.ReplyToList) { if (statusText.StartsWith($"@{screenName} ", StringComparison.Ordinal)) { statusText = statusText.Substring(screenName.Length + 2); - _autoPopulatedUserIds.Add(userId); + autoPopulatedUserIdList.Add(userId); } } } } - autoPopulatedUserIds = _autoPopulatedUserIds.ToArray(); + autoPopulatedUserIds = autoPopulatedUserIdList.ToArray(); return statusText; } @@ -4526,7 +3559,7 @@ private string FormatStatusText(string statusText) statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& "); } - if (SettingManager.Common.WideSpaceConvert) + if (this.settings.Common.WideSpaceConvert) { // 文中の全角スペースを半角スペース1個にする statusText = statusText.Replace(" ", " "); @@ -4537,13 +3570,13 @@ private string FormatStatusText(string statusText) return statusText; bool disableFooter; - if (SettingManager.Common.PostShiftEnter) + if (this.settings.Common.PostShiftEnter) { disableFooter = MyCommon.IsKeyDown(Keys.Control); } else { - if (this.StatusText.Multiline && !SettingManager.Common.PostCtrlEnter) + if (this.StatusText.Multiline && !this.settings.Common.PostCtrlEnter) disableFooter = MyCommon.IsKeyDown(Keys.Control); else disableFooter = MyCommon.IsKeyDown(Keys.Shift); @@ -4569,23 +3602,23 @@ private string FormatStatusText(string statusText) var hashtag = this.HashMgr.UseHash; if (!MyCommon.IsNullOrEmpty(hashtag) && !(this.HashMgr.IsNotAddToAtReply && this.inReplyTo != null)) { - if (HashMgr.IsHead) - header = HashMgr.UseHash + " "; + if (this.HashMgr.IsHead) + header = this.HashMgr.UseHash + " "; else - footer = " " + HashMgr.UseHash; + footer = " " + this.HashMgr.UseHash; } if (!disableFooter) { - if (SettingManager.Local.UseRecommendStatus) + if (this.settings.Local.UseRecommendStatus) { // 推奨ステータスを使用する footer += this.recommendedStatusFooter; } - else if (!MyCommon.IsNullOrEmpty(SettingManager.Local.StatusText)) + else if (!MyCommon.IsNullOrEmpty(this.settings.Local.StatusText)) { // テキストボックスに入力されている文字列を使用する - footer += " " + SettingManager.Local.StatusText.Trim(); + footer += " " + this.settings.Local.StatusText.Trim(); } } @@ -4627,136 +3660,6 @@ private int GetRestStatusCount(string statusText) private IMediaUploadService? GetSelectedImageService() => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null; - private void MyList_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) - { - if (sender != this.CurrentListView) - return; - - var listCache = this._listItemCache; - if (listCache?.TargetList == sender && listCache.IsSupersetOf(e.StartIndex, e.EndIndex)) - { - // If the newly requested cache is a subset of the old cache, - // no need to rebuild everything, so do nothing. - return; - } - - // Now we need to rebuild the cache. - this.CreateCache(e.StartIndex, e.EndIndex); - } - - private void MyList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) - { - var listCache = this._listItemCache; - if (listCache?.TargetList == sender) - { - if (listCache.TryGetValue(e.ItemIndex, out var item, out _)) - { - e.Item = item; - return; - } - } - - // A cache miss, so create a new ListViewItem and pass it back. - var tabPage = (TabPage)((DetailsListView)sender).Parent; - var tab = this._statuses.Tabs[tabPage.Text]; - try - { - e.Item = this.CreateItem(tab, tab[e.ItemIndex]); - } - catch (Exception) - { - // 不正な要求に対する間に合わせの応答 - string[] sitem = {"", "", "", "", "", "", "", ""}; - e.Item = new ImageListViewItem(sitem); - } - } - - private void CreateCache(int startIndex, int endIndex) - { - var tabInfo = this.CurrentTab; - - if (tabInfo.AllCount == 0) - return; - - // インデックスを 0...(tabInfo.AllCount - 1) の範囲内にする - int FilterRange(int index) - => Math.Max(Math.Min(index, tabInfo.AllCount - 1), 0); - - // キャッシュ要求(要求範囲±30を作成) - startIndex = FilterRange(startIndex - 30); - endIndex = FilterRange(endIndex + 30); - - var cacheLength = endIndex - startIndex + 1; - - var tab = this.CurrentTab; - var posts = tabInfo[startIndex, endIndex]; //配列で取得 - var listItems = Enumerable.Range(0, cacheLength) - .Select(x => this.CreateItem(tab, posts[x])) - .ToArray(); - - var listCache = new ListViewItemCache - { - TargetList = this.CurrentListView, - StartIndex = startIndex, - EndIndex = endIndex, - Cache = Enumerable.Zip(listItems, posts, (x, y) => (x, y)).ToArray(), - }; - - Interlocked.Exchange(ref this._listItemCache, listCache); - } - - /// - /// DetailsListView のための ListViewItem のキャッシュを消去する - /// - private void PurgeListViewItemCache() - => Interlocked.Exchange(ref this._listItemCache, null); - - private ListViewItem CreateItem(TabModel tab, PostClass Post) - { - var mk = new StringBuilder(); - - if (Post.FavoritedCount > 0) mk.Append("+" + Post.FavoritedCount); - ImageListViewItem itm; - if (Post.RetweetedId == null) - { - string[] sitem= {"", - Post.Nickname, - Post.IsDeleted ? "(DELETED)" : Post.AccessibleText.Replace('\n', ' '), - Post.CreatedAt.ToLocalTimeString(SettingManager.Common.DateTimeFormat), - Post.ScreenName, - "", - mk.ToString(), - Post.Source}; - itm = new ImageListViewItem(sitem, this.IconCache, Post.ImageUrl); - } - else - { - string[] sitem = {"", - Post.Nickname, - Post.IsDeleted ? "(DELETED)" : Post.AccessibleText.Replace('\n', ' '), - Post.CreatedAt.ToLocalTimeString(SettingManager.Common.DateTimeFormat), - Post.ScreenName + Environment.NewLine + "(RT:" + Post.RetweetedBy + ")", - "", - mk.ToString(), - Post.Source}; - itm = new ImageListViewItem(sitem, this.IconCache, Post.ImageUrl); - } - itm.StateIndex = Post.StateIndex; - itm.Tag = Post; - - var read = Post.IsRead; - // 未読管理していなかったら既読として扱う - if (!tab.UnreadManage || !SettingManager.Common.UnreadManage) - read = true; - - ChangeItemStyleRead(read, itm, Post, null); - - if (tab.TabName == this.CurrentTabName) - this.ColorizeList(itm, Post); - - return itm; - } - /// /// 全てのタブの振り分けルールを反映し直します /// @@ -4764,19 +3667,20 @@ private void ApplyPostFilters() { using (ControlTransaction.Cursor(this, Cursors.WaitCursor)) { - this.PurgeListViewItemCache(); - this._statuses.FilterAll(); + this.statuses.FilterAll(); - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + var listView = this.CurrentListView; + using (ControlTransaction.Update(listView)) + { + this.listCache?.PurgeCache(); + this.listCache?.UpdateListSize(); + } + + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { var tabPage = this.ListTab.TabPages[index]; - var listview = (DetailsListView)tabPage.Tag; - using (ControlTransaction.Update(listview)) - { - listview.VirtualListSize = tab.AllCount; - } - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { if (tab.UnreadCount > 0) tabPage.ImageIndex = 0; @@ -4785,11 +3689,11 @@ private void ApplyPostFilters() } } - if (!SettingManager.Common.TabIconDisp) + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); - SetMainWindowTitle(); - SetStatusLabelUrl(); + this.SetMainWindowTitle(); + this.SetStatusLabelUrl(); } } @@ -4799,207 +3703,6 @@ private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEven private void MyList_HScrolled(object sender, EventArgs e) => ((DetailsListView)sender).Refresh(); - private void MyList_DrawItem(object sender, DrawListViewItemEventArgs e) - { - if (e.State == 0) return; - e.DrawDefault = false; - - SolidBrush brs2; - if (!e.Item.Selected) //e.ItemStateでうまく判定できない??? - { - if (e.Item.BackColor == _clSelf) - brs2 = _brsBackColorMine; - else if (e.Item.BackColor == _clAtSelf) - brs2 = _brsBackColorAt; - else if (e.Item.BackColor == _clTarget) - brs2 = _brsBackColorYou; - else if (e.Item.BackColor == _clAtTarget) - brs2 = _brsBackColorAtYou; - else if (e.Item.BackColor == _clAtFromTarget) - brs2 = _brsBackColorAtFromTarget; - else if (e.Item.BackColor == _clAtTo) - brs2 = _brsBackColorAtTo; - else - brs2 = _brsBackColorNone; - } - else - { - //選択中の行 - if (((Control)sender).Focused) - brs2 = _brsHighLight; - else - brs2 = _brsDeactiveSelection; - } - e.Graphics.FillRectangle(brs2, e.Bounds); - e.DrawFocusRectangle(); - this.DrawListViewItemIcon(e); - } - - private void MyList_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) - { - if (e.ItemState == 0) return; - - if (e.ColumnIndex > 0) - { - //アイコン以外の列 - var post = (PostClass)e.Item.Tag; - - RectangleF rct = e.Bounds; - rct.Width = e.Header.Width; - var fontHeight = e.Item.Font.Height; - if (_iconCol) - { - rct.Y += fontHeight; - rct.Height -= fontHeight; - } - - var drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out var heightDiff)); - - //フォントの高さの半分を足してるのは保険。無くてもいいかも。 - if (this._iconCol || drawLineCount > 1) - { - if (heightDiff < fontHeight * 0.7) - { - // 最終行が70%以上欠けていたら、最終行は表示しない - rct.Height = (fontHeight * drawLineCount) - 1; - } - else - { - drawLineCount += 1; - } - } - - if (rct.Width > 0) - { - var color = (!e.Item.Selected) ? e.Item.ForeColor : //選択されていない行 - (((Control)sender).Focused) ? _clHighLight : //選択中の行 - _clUnread; - - if (_iconCol) - { - var rctB = e.Bounds; - rctB.Width = e.Header.Width; - rctB.Height = fontHeight; - - using var fnt = new Font(e.Item.Font, FontStyle.Bold); - - TextRenderer.DrawText(e.Graphics, - post.IsDeleted ? "(DELETED)" : post.TextSingleLine, - e.Item.Font, - Rectangle.Round(rct), - color, - TextFormatFlags.WordBreak | - TextFormatFlags.EndEllipsis | - TextFormatFlags.GlyphOverhangPadding | - TextFormatFlags.NoPrefix); - TextRenderer.DrawText(e.Graphics, - e.Item.SubItems[4].Text + " / " + e.Item.SubItems[1].Text + " (" + e.Item.SubItems[3].Text + ") " + e.Item.SubItems[5].Text + e.Item.SubItems[6].Text + " [" + e.Item.SubItems[7].Text + "]", - fnt, - rctB, - color, - TextFormatFlags.SingleLine | - TextFormatFlags.EndEllipsis | - TextFormatFlags.GlyphOverhangPadding | - TextFormatFlags.NoPrefix); - } - else - { - string text; - if (e.ColumnIndex != 2) - text = e.SubItem.Text; - else - text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine; - - if (drawLineCount == 1) - { - TextRenderer.DrawText(e.Graphics, - text, - e.Item.Font, - Rectangle.Round(rct), - color, - TextFormatFlags.SingleLine | - TextFormatFlags.EndEllipsis | - TextFormatFlags.GlyphOverhangPadding | - TextFormatFlags.NoPrefix | - TextFormatFlags.VerticalCenter); - } - else - { - TextRenderer.DrawText(e.Graphics, - text, - e.Item.Font, - Rectangle.Round(rct), - color, - TextFormatFlags.WordBreak | - TextFormatFlags.EndEllipsis | - TextFormatFlags.GlyphOverhangPadding | - TextFormatFlags.NoPrefix); - } - } - } - } - } - - private void DrawListViewItemIcon(DrawListViewItemEventArgs e) - { - if (_iconSz == 0) return; - - var item = (ImageListViewItem)e.Item; - - //e.Bounds.Leftが常に0を指すから自前で計算 - var itemRect = item.Bounds; - var col0 = e.Item.ListView.Columns[0]; - itemRect.Width = col0.Width; - - if (col0.DisplayIndex > 0) - { - foreach (ColumnHeader clm in e.Item.ListView.Columns) - { - if (clm.DisplayIndex < col0.DisplayIndex) - itemRect.X += clm.Width; - } - } - - // ディスプレイの DPI 設定を考慮したアイコンサイズ - var realIconSize = new SizeF(this._iconSz * this.CurrentScaleFactor.Width, this._iconSz * this.CurrentScaleFactor.Height).ToSize(); - var realStateSize = new SizeF(16 * this.CurrentScaleFactor.Width, 16 * this.CurrentScaleFactor.Height).ToSize(); - - Rectangle iconRect; - var img = item.Image; - if (img != null) - { - iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, realIconSize), itemRect); - iconRect.Offset(0, Math.Max(0, (itemRect.Height - realIconSize.Height) / 2)); - - if (iconRect.Width > 0) - { - e.Graphics.FillRectangle(Brushes.White, iconRect); - e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; - try - { - e.Graphics.DrawImage(img.Image, iconRect); - } - catch (ArgumentException) - { - item.RefreshImageAsync(); - } - } - } - else - { - iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, new Size(1, 1)), itemRect); - - item.GetImageAsync(); - } - - if (item.StateIndex > -1) - { - var stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect); - if (stateRect.Width > 0) - e.Graphics.DrawImage(this.PostStateImageList.Images[item.StateIndex], stateRect); - } - } - protected override void ScaleControl(SizeF factor, BoundsSpecified specified) { base.ScaleControl(factor, specified); @@ -5123,10 +3826,10 @@ private void ShowSearchDialog() { if (this.SearchDialog.ShowDialog(this) != DialogResult.OK) { - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; return; } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; var searchOptions = this.SearchDialog.ResultOptions!; if (searchOptions.Type == SearchWordDialog.SearchType.Timeline) @@ -5137,7 +3840,7 @@ private void ShowSearchDialog() try { - tabName = this._statuses.MakeTabName(tabName); + tabName = this.statuses.MakeTabName(tabName); } catch (TabException ex) { @@ -5146,7 +3849,7 @@ private void ShowSearchDialog() var resultTab = new LocalSearchTabModel(tabName); this.AddNewTab(resultTab, startup: false); - this._statuses.AddTab(resultTab); + this.statuses.AddTab(resultTab); var targetTab = this.CurrentTab; @@ -5174,10 +3877,10 @@ private void ShowSearchDialog() resultTab.AddPostQueue(post); } - this._statuses.DistributePosts(); + this.statuses.DistributePosts(); this.RefreshTimeline(); - var tabIndex = this._statuses.Tabs.IndexOf(tabName); + var tabIndex = this.statuses.Tabs.IndexOf(tabName); this.ListTab.SelectedIndex = tabIndex; } else @@ -5222,41 +3925,41 @@ private void AboutMenuItem_Click(object sender, EventArgs e) { about.ShowDialog(this); } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; } private void JumpUnreadMenuItem_Click(object sender, EventArgs e) { - var bgnIdx = this._statuses.SelectedTabIndex; + var bgnIdx = this.statuses.SelectedTabIndex; - if (ImageSelector.Enabled) + if (this.ImageSelector.Enabled) return; TabModel? foundTab = null; var foundIndex = 0; - //現在タブから最終タブまで探索 - foreach (var (tab, index) in this._statuses.Tabs.WithIndex().Skip(bgnIdx)) + // 現在タブから最終タブまで探索 + foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Skip(bgnIdx)) { var unreadIndex = tab.NextUnreadIndex; if (unreadIndex != -1) { - ListTab.SelectedIndex = index; + this.ListTab.SelectedIndex = index; foundTab = tab; foundIndex = unreadIndex; break; } } - //未読みつからず&現在タブが先頭ではなかったら、先頭タブから現在タブの手前まで探索 + // 未読みつからず&現在タブが先頭ではなかったら、先頭タブから現在タブの手前まで探索 if (foundTab == null && bgnIdx > 0) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex().Take(bgnIdx)) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Take(bgnIdx)) { var unreadIndex = tab.NextUnreadIndex; if (unreadIndex != -1) { - ListTab.SelectedIndex = index; + this.ListTab.SelectedIndex = index; foundTab = tab; foundIndex = unreadIndex; break; @@ -5268,15 +3971,15 @@ private void JumpUnreadMenuItem_Click(object sender, EventArgs e) if (foundTab == null) { - //全部調べたが未読見つからず→先頭タブの最新発言へ - ListTab.SelectedIndex = 0; + // 全部調べたが未読見つからず→先頭タブの最新発言へ + this.ListTab.SelectedIndex = 0; var tabPage = this.ListTab.TabPages[0]; - var tab = this._statuses.Tabs[0]; + var tab = this.statuses.Tabs[0]; if (tab.AllCount == 0) return; - if (_statuses.SortOrder == SortOrder.Ascending) + if (this.statuses.SortOrder == SortOrder.Ascending) foundIndex = tab.AllCount - 1; else foundIndex = 0; @@ -5285,18 +3988,19 @@ private void JumpUnreadMenuItem_Click(object sender, EventArgs e) } else { - var foundTabIndex = this._statuses.Tabs.IndexOf(foundTab); + var foundTabIndex = this.statuses.Tabs.IndexOf(foundTab); lst = (DetailsListView)this.ListTab.TabPages[foundTabIndex].Tag; } - SelectListItem(lst, foundIndex); + this.SelectListItem(lst, foundIndex); - if (_statuses.SortMode == ComparerMode.Id) + if (this.statuses.SortMode == ComparerMode.Id) { - if (_statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - _iconSz - 10 || - _statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < _iconSz + 10) + var rowHeight = lst.SmallImageList.ImageSize.Height; + if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - rowHeight - 10 || + this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < rowHeight + 10) { - MoveTop(); + this.MoveTop(); } else { @@ -5327,8 +4031,8 @@ private void RunTweenUp() var pinfo = new ProcessStartInfo { UseShellExecute = true, - WorkingDirectory = MyCommon.settingPath, - FileName = Path.Combine(MyCommon.settingPath, "TweenUp3.exe"), + WorkingDirectory = this.settings.SettingsPath, + FileName = Path.Combine(this.settings.SettingsPath, "TweenUp3.exe"), Arguments = "\"" + Application.StartupPath + "\"", }; @@ -5342,15 +4046,11 @@ private void RunTweenUp() } } - public class VersionInfo - { - public Version Version { get; } - public Uri DownloadUri { get; } - public string ReleaseNote { get; } - - public VersionInfo(Version version, Uri downloadUri, string releaseNote) - => (this.Version, this.DownloadUri, this.ReleaseNote) = (version, downloadUri, releaseNote); - } + public readonly record struct VersionInfo( + Version Version, + Uri DownloadUri, + string ReleaseNote + ); /// /// OpenTween の最新バージョンの情報を取得します @@ -5371,11 +4071,12 @@ public async Task GetVersionInfoAsync() msgBody = Regex.Replace(msgBody, "(? CRLF - return new VersionInfo( - version: Version.Parse(msgHeader[0]), - downloadUri: new Uri(msgHeader[1]), - releaseNote: msgBody - ); + return new VersionInfo + { + Version = Version.Parse(msgHeader[0]), + DownloadUri = new Uri(msgHeader[1]), + ReleaseNote = msgBody, + }; } private async Task CheckNewVersion(bool startup = false) @@ -5392,18 +4093,22 @@ private async Task CheckNewVersion(bool startup = false) // 更新不要 if (!startup) { - var msgtext = string.Format(Properties.Resources.CheckNewVersionText7, - MyCommon.GetReadableVersion(), MyCommon.GetReadableVersion(versionInfo.Version)); + var msgtext = string.Format( + Properties.Resources.CheckNewVersionText7, + MyCommon.GetReadableVersion(), + MyCommon.GetReadableVersion(versionInfo.Version)); msgtext = MyCommon.ReplaceAppName(msgtext); - MessageBox.Show(msgtext, + MessageBox.Show( + msgtext, MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2), - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBoxButtons.OK, + MessageBoxIcon.Information); } return; } - if (startup && versionInfo.Version <= SettingManager.Common.SkipUpdateVersion) + if (startup && versionInfo.Version <= this.settings.Common.SkipUpdateVersion) return; using var dialog = new UpdateDialog(); @@ -5418,7 +4123,7 @@ private async Task CheckNewVersion(bool startup = false) } else if (dialog.SkipButtonPressed) { - SettingManager.Common.SkipUpdateVersion = versionInfo.Version; + this.settings.Common.SkipUpdateVersion = versionInfo.Version; this.MarkSettingCommonModified(); } } @@ -5427,29 +4132,32 @@ private async Task CheckNewVersion(bool startup = false) this.StatusLabel.Text = Properties.Resources.CheckNewVersionText9; if (!startup) { - MessageBox.Show(Properties.Resources.CheckNewVersionText10, + MessageBox.Show( + Properties.Resources.CheckNewVersionText10, MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2), - MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2); + MessageBoxButtons.OK, + MessageBoxIcon.Exclamation, + MessageBoxDefaultButton.Button2); } } } private void UpdateSelectedPost() { - //件数関連の場合、タイトル即時書き換え - if (SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.None && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Post && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus) + // 件数関連の場合、タイトル即時書き換え + if (this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.None && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Post && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus) { - SetMainWindowTitle(); + this.SetMainWindowTitle(); } - if (!StatusLabelUrl.Text.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - SetStatusLabelUrl(); + if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + this.SetStatusLabelUrl(); - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { if (tab.UnreadCount == 0) { @@ -5467,13 +4175,13 @@ private void UpdateSelectedPost() this.DispSelectedPost(); } - public string createDetailHtml(string orgdata) - => detailHtmlFormatHeader + orgdata + detailHtmlFormatFooter; + public string CreateDetailHtml(string orgdata) + => this.detailHtmlFormatPreparedTemplate.Replace("%CONTENT_HTML%", orgdata); private void DispSelectedPost() => this.DispSelectedPost(false); - private PostClass displayPost = new PostClass(); + private PostClass displayPost = new(); /// /// サムネイル表示に使用する CancellationToken の生成元 @@ -5499,7 +4207,7 @@ private void DispSelectedPost(bool forceupdate) this.SplitContainer3.Panel2Collapsed = true; - if (SettingManager.Common.PreviewEnable) + if (this.settings.Common.PreviewEnable) { var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource()); oldTokenSource?.Cancel(); @@ -5508,17 +4216,19 @@ private void DispSelectedPost(bool forceupdate) loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token)); } - async Task delayedTasks() + async Task DelayedTasks() { try { await Task.WhenAll(loadTasks); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + } } // サムネイルの読み込みを待たずに次に選択されたツイートを表示するため await しない - _ = delayedTasks(); + _ = DelayedTasks(); } private async void MatomeMenuItem_Click(object sender, EventArgs e) @@ -5542,9 +4252,9 @@ private async void ListTab_KeyDown(object sender, KeyEventArgs e) } if (e.Control || e.Shift || e.Alt) - this._anchorFlag = false; + tab.ClearAnchor(); - if (CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask)) + if (this.CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask)) { e.Handled = true; e.SuppressKeyPress = true; @@ -5594,11 +4304,19 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.Space, Keys.ProcessKey) .NotFocusedOn(FocusedControl.StatusText) - .Do(() => { this._anchorFlag = false; this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty); + }), ShortcutCommand.Create(Keys.G) .NotFocusedOn(FocusedControl.StatusText) - .Do(() => { this._anchorFlag = false; this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty); + }), ShortcutCommand.Create(Keys.Right, Keys.N) .FocusedOn(FocusedControl.ListTab) @@ -5619,7 +4337,7 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.Enter) .FocusedOn(FocusedControl.ListTab) - .Do(() => this.MakeReplyOrDirectStatus()), + .Do(() => this.ListItemDoubleClickAction()), ShortcutCommand.Create(Keys.R) .FocusedOn(FocusedControl.ListTab) @@ -5627,51 +4345,80 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.L) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.GoPost(forward: true); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.GoPost(forward: true); + }), ShortcutCommand.Create(Keys.H) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.GoPost(forward: false); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.GoPost(forward: false); + }), ShortcutCommand.Create(Keys.Z, Keys.Oemcomma) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.MoveTop(); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.MoveTop(); + }), ShortcutCommand.Create(Keys.S) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.GoNextTab(forward: true); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.GoNextTab(forward: true); + }), ShortcutCommand.Create(Keys.A) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.GoNextTab(forward: false); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.GoNextTab(forward: false); + }), // ] in_reply_to参照元へ戻る ShortcutCommand.Create(Keys.Oem4) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; return this.GoInReplyToPostTree(); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + return this.GoInReplyToPostTree(); + }), // [ in_reply_toへジャンプ ShortcutCommand.Create(Keys.Oem6) .FocusedOn(FocusedControl.ListTab) - .Do(() => { this._anchorFlag = false; this.GoBackInReplyToPostTree(); }), + .Do(() => + { + this.CurrentTab.ClearAnchor(); + this.GoBackInReplyToPostTree(); + }), ShortcutCommand.Create(Keys.Escape) .FocusedOn(FocusedControl.ListTab) - .Do(() => { - this._anchorFlag = false; + .Do(() => + { + this.CurrentTab.ClearAnchor(); var tab = this.CurrentTab; var tabtype = tab.TabType; if (tabtype == MyCommon.TabUsageType.Related || tabtype == MyCommon.TabUsageType.UserTimeline || tabtype == MyCommon.TabUsageType.PublicSearch || tabtype == MyCommon.TabUsageType.SearchResults) { - RemoveSpecifiedTab(tab.TabName, false); - SaveConfigsTabs(); + this.RemoveSpecifiedTab(tab.TabName, false); + this.SaveConfigsTabs(); } }), // 上下キー, PageUp/Downキー, Home/Endキー は既定の動作を残しつつアンカー初期化 ShortcutCommand.Create(Keys.Up, Keys.Down, Keys.PageUp, Keys.PageDown, Keys.Home, Keys.End) .FocusedOn(FocusedControl.ListTab) - .Do(() => this._anchorFlag = false, preventDefault: false), + .Do(() => this.CurrentTab.ClearAnchor(), preventDefault: false), // PreviewKeyDownEventArgs.IsInputKey を true にしてスクロールを発生させる ShortcutCommand.Create(Keys.Up, Keys.Down) @@ -5679,22 +4426,22 @@ private void InitializeShortcuts() .Do(() => { }), ShortcutCommand.Create(Keys.Control | Keys.R) - .Do(() => this.MakeReplyOrDirectStatus(isAuto: false, isReply: true)), + .Do(() => this.MakeReplyText()), ShortcutCommand.Create(Keys.Control | Keys.D) - .Do(() => this.doStatusDelete()), + .Do(() => this.DoStatusDelete()), ShortcutCommand.Create(Keys.Control | Keys.M) - .Do(() => this.MakeReplyOrDirectStatus(isAuto: false, isReply: false)), + .Do(() => this.MakeDirectMessageText()), ShortcutCommand.Create(Keys.Control | Keys.S) - .Do(() => this.FavoriteChange(FavAdd: true)), + .Do(() => this.FavoriteChange(favAdd: true)), ShortcutCommand.Create(Keys.Control | Keys.I) - .Do(() => this.doRepliedStatusOpen()), + .Do(() => this.DoRepliedStatusOpen()), ShortcutCommand.Create(Keys.Control | Keys.Q) - .Do(() => this.doQuoteOfficial()), + .Do(() => this.DoQuoteOfficial()), ShortcutCommand.Create(Keys.Control | Keys.B) .Do(() => this.ReadedStripMenuItem_Click(this.ReadedStripMenuItem, EventArgs.Empty)), @@ -5716,10 +4463,7 @@ private void InitializeShortcuts() .Do(() => this.ShowUserTimeline()), ShortcutCommand.Create(Keys.Control | Keys.H) - .Do(() => this.MoveToHomeToolStripMenuItem_Click(this.MoveToHomeToolStripMenuItem, EventArgs.Empty)), - - ShortcutCommand.Create(Keys.Control | Keys.G) - .Do(() => this.MoveToFavToolStripMenuItem_Click(this.MoveToFavToolStripMenuItem, EventArgs.Empty)), + .Do(() => this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty)), ShortcutCommand.Create(Keys.Control | Keys.O) .Do(() => this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty)), @@ -5746,47 +4490,47 @@ private void InitializeShortcuts() // タブダイレクト選択(Ctrl+1~8,Ctrl+9) ShortcutCommand.Create(Keys.Control | Keys.D1) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 1) + .OnlyWhen(() => this.statuses.Tabs.Count >= 1) .Do(() => this.ListTab.SelectedIndex = 0), ShortcutCommand.Create(Keys.Control | Keys.D2) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 2) + .OnlyWhen(() => this.statuses.Tabs.Count >= 2) .Do(() => this.ListTab.SelectedIndex = 1), ShortcutCommand.Create(Keys.Control | Keys.D3) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 3) + .OnlyWhen(() => this.statuses.Tabs.Count >= 3) .Do(() => this.ListTab.SelectedIndex = 2), ShortcutCommand.Create(Keys.Control | Keys.D4) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 4) + .OnlyWhen(() => this.statuses.Tabs.Count >= 4) .Do(() => this.ListTab.SelectedIndex = 3), ShortcutCommand.Create(Keys.Control | Keys.D5) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 5) + .OnlyWhen(() => this.statuses.Tabs.Count >= 5) .Do(() => this.ListTab.SelectedIndex = 4), ShortcutCommand.Create(Keys.Control | Keys.D6) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 6) + .OnlyWhen(() => this.statuses.Tabs.Count >= 6) .Do(() => this.ListTab.SelectedIndex = 5), ShortcutCommand.Create(Keys.Control | Keys.D7) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 7) + .OnlyWhen(() => this.statuses.Tabs.Count >= 7) .Do(() => this.ListTab.SelectedIndex = 6), ShortcutCommand.Create(Keys.Control | Keys.D8) .FocusedOn(FocusedControl.ListTab) - .OnlyWhen(() => this._statuses.Tabs.Count >= 8) + .OnlyWhen(() => this.statuses.Tabs.Count >= 8) .Do(() => this.ListTab.SelectedIndex = 7), ShortcutCommand.Create(Keys.Control | Keys.D9) .FocusedOn(FocusedControl.ListTab) - .Do(() => this.ListTab.SelectedIndex = this._statuses.Tabs.Count - 1), + .Do(() => this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1), ShortcutCommand.Create(Keys.Control | Keys.A) .FocusedOn(FocusedControl.StatusText) @@ -5806,39 +4550,42 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.Control | Keys.PageUp, Keys.Control | Keys.P) .FocusedOn(FocusedControl.StatusText) - .Do(() => { - if (ListTab.SelectedIndex == 0) + .Do(() => + { + if (this.ListTab.SelectedIndex == 0) { - ListTab.SelectedIndex = ListTab.TabCount - 1; + this.ListTab.SelectedIndex = this.ListTab.TabCount - 1; } else { - ListTab.SelectedIndex -= 1; + this.ListTab.SelectedIndex -= 1; } - StatusText.Focus(); + this.StatusText.Focus(); }), ShortcutCommand.Create(Keys.Control | Keys.PageDown, Keys.Control | Keys.N) .FocusedOn(FocusedControl.StatusText) - .Do(() => { - if (ListTab.SelectedIndex == ListTab.TabCount - 1) + .Do(() => + { + if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1) { - ListTab.SelectedIndex = 0; + this.ListTab.SelectedIndex = 0; } else { - ListTab.SelectedIndex += 1; + this.ListTab.SelectedIndex += 1; } - StatusText.Focus(); + this.StatusText.Focus(); }), ShortcutCommand.Create(Keys.Control | Keys.Y) .FocusedOn(FocusedControl.PostBrowser) - .Do(() => { - var multiline = !SettingManager.Local.StatusMultiline; - SettingManager.Local.StatusMultiline = multiline; - MultiLineMenuItem.Checked = multiline; - MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty); + .Do(() => + { + var multiline = !this.settings.Local.StatusMultiline; + this.settings.Local.StatusMultiline = multiline; + this.MultiLineMenuItem.Checked = multiline; + this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty); }), ShortcutCommand.Create(Keys.Shift | Keys.F3) @@ -5859,11 +4606,11 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.Shift | Keys.H) .FocusedOn(FocusedControl.ListTab) - .Do(() => this.GoTopEnd(GoTop: true)), + .Do(() => this.GoTopEnd(goTop: true)), ShortcutCommand.Create(Keys.Shift | Keys.L) .FocusedOn(FocusedControl.ListTab) - .Do(() => this.GoTopEnd(GoTop: false)), + .Do(() => this.GoTopEnd(goTop: false)), ShortcutCommand.Create(Keys.Shift | Keys.M) .FocusedOn(FocusedControl.ListTab) @@ -5900,11 +4647,11 @@ private void InitializeShortcuts() .Do(() => this.GoBackSelectPostChain()), ShortcutCommand.Create(Keys.Alt | Keys.R) - .Do(() => this.doReTweetOfficial(isConfirm: true)), + .Do(() => this.DoReTweetOfficial(isConfirm: true)), ShortcutCommand.Create(Keys.Alt | Keys.P) .OnlyWhen(() => this.CurrentPost != null) - .Do(() => this.doShowUserStatus(this.CurrentPost!.ScreenName, ShowInputDialog: false)), + .Do(() => this.DoShowUserStatus(this.CurrentPost!.ScreenName, showInputDialog: false)), ShortcutCommand.Create(Keys.Alt | Keys.Up) .Do(() => this.tweetDetailsView.ScrollDownPostBrowser(forward: false)), @@ -5928,7 +4675,7 @@ private void InitializeShortcuts() .Do(() => this.GoSamePostToAnotherTab(left: true)), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.R) - .Do(() => this.MakeReplyOrDirectStatus(isAuto: false, isReply: true, isAll: true)), + .Do(() => this.MakeReplyText(atAll: true)), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.C, Keys.Control | Keys.Shift | Keys.Insert) .Do(() => this.CopyIdUri()), @@ -5938,7 +4685,7 @@ private void InitializeShortcuts() .Do(() => this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus()), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.S) - .Do(() => this.FavoriteChange(FavAdd: false)), + .Do(() => this.FavoriteChange(favAdd: false)), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.B) .Do(() => this.UnreadStripMenuItem_Click(this.UnreadStripMenuItem, EventArgs.Empty)), @@ -5950,62 +4697,65 @@ private void InitializeShortcuts() .Do(() => this.ImageSelectMenuItem_Click(this.ImageSelectMenuItem, EventArgs.Empty)), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.H) - .Do(() => this.doMoveToRTHome()), + .Do(() => this.DoMoveToRTHome()), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Up) .FocusedOn(FocusedControl.StatusText) - .Do(() => { + .Do(() => + { var tab = this.CurrentTab; var selectedIndex = tab.SelectedIndex; if (selectedIndex != -1 && selectedIndex > 0) { var listView = this.CurrentListView; var idx = selectedIndex - 1; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); } }), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Down) .FocusedOn(FocusedControl.StatusText) - .Do(() => { + .Do(() => + { var tab = this.CurrentTab; var selectedIndex = tab.SelectedIndex; if (selectedIndex != -1 && selectedIndex < tab.AllCount - 1) { var listView = this.CurrentListView; var idx = selectedIndex + 1; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); } }), ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Space) .FocusedOn(FocusedControl.StatusText) - .Do(() => { - if (StatusText.SelectionStart > 0) + .Do(() => + { + if (this.StatusText.SelectionStart > 0) { - var endidx = StatusText.SelectionStart - 1; + var endidx = this.StatusText.SelectionStart - 1; var startstr = ""; - for (var i = StatusText.SelectionStart - 1; i >= 0; i--) + for (var i = this.StatusText.SelectionStart - 1; i >= 0; i--) { - var c = StatusText.Text[i]; + var c = this.StatusText.Text[i]; if (char.IsLetterOrDigit(c) || c == '_') { continue; } if (c == '@') { - startstr = StatusText.Text.Substring(i + 1, endidx - i); - var cnt = AtIdSupl.ItemCount; - ShowSuplDialog(StatusText, AtIdSupl, startstr.Length + 1, startstr); - if (AtIdSupl.ItemCount != cnt) + startstr = this.StatusText.Text.Substring(i + 1, endidx - i); + var cnt = this.AtIdSupl.ItemCount; + this.ShowSuplDialog(this.StatusText, this.AtIdSupl, startstr.Length + 1, startstr); + if (this.AtIdSupl.ItemCount != cnt) this.MarkSettingAtIdModified(); } else if (c == '#') { - startstr = StatusText.Text.Substring(i + 1, endidx - i); - ShowSuplDialog(StatusText, HashSupl, startstr.Length + 1, startstr); + startstr = this.StatusText.Text.Substring(i + 1, endidx - i); + this.ShowSuplDialog(this.StatusText, this.HashSupl, startstr.Length + 1, startstr); } else { @@ -6066,14 +4816,14 @@ private void InitializeShortcuts() ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R) .FocusedOn(FocusedControl.PostBrowser) - .Do(() => this.doReTweetUnofficial()), + .Do(() => this.DoReTweetUnofficial()), ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.T) .OnlyWhen(() => this.ExistCurrentPost) .Do(() => this.tweetDetailsView.DoTranslation()), ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R) - .Do(() => this.doReTweetUnofficial()), + .Do(() => this.DoReTweetUnofficial()), ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.C, Keys.Alt | Keys.Shift | Keys.Insert) .Do(() => this.CopyUserId()), @@ -6111,8 +4861,8 @@ internal bool CommonKeyDown(Keys keyData, FocusedControl focusedOn, out Task? as private void GoNextTab(bool forward) { - var idx = this._statuses.SelectedTabIndex; - var tabCount = this._statuses.Tabs.Count; + var idx = this.statuses.SelectedTabIndex; + var tabCount = this.statuses.Tabs.Count; if (forward) { idx += 1; @@ -6123,14 +4873,14 @@ private void GoNextTab(bool forward) idx -= 1; if (idx < 0) idx = tabCount - 1; } - ListTab.SelectedIndex = idx; + this.ListTab.SelectedIndex = idx; } private void CopyStot() { var sb = new StringBuilder(); var tab = this.CurrentTab; - var IsProtected = false; + var isProtected = false; var isDm = tab.TabType == MyCommon.TabUsageType.DirectMessage; foreach (var post in tab.SelectedPosts) { @@ -6147,7 +4897,7 @@ private void CopyStot() sb.AppendFormat("{0}:{1} [{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine); } } - if (IsProtected) + if (isProtected) { MessageBox.Show(Properties.Resources.CopyStotText1); } @@ -6236,7 +4986,7 @@ private void GoFav(bool forward) if (tab[idx].IsFav) { var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); break; } @@ -6260,13 +5010,13 @@ private void GoSamePostToAnotherTab(bool left) if (left) { // 左のタブへ - if (ListTab.SelectedIndex == 0) + if (this.ListTab.SelectedIndex == 0) { return; } else { - fIdx = ListTab.SelectedIndex - 1; + fIdx = this.ListTab.SelectedIndex - 1; } toIdx = -1; stp = -1; @@ -6274,21 +5024,21 @@ private void GoSamePostToAnotherTab(bool left) else { // 右のタブへ - if (ListTab.SelectedIndex == ListTab.TabCount - 1) + if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1) { return; } else { - fIdx = ListTab.SelectedIndex + 1; + fIdx = this.ListTab.SelectedIndex + 1; } - toIdx = ListTab.TabCount; + toIdx = this.ListTab.TabCount; stp = 1; } for (var tabidx = fIdx; tabidx != toIdx; tabidx += stp) { - var targetTab = this._statuses.Tabs[tabidx]; + var targetTab = this.statuses.Tabs[tabidx]; // Directタブは対象外 if (targetTab.TabType == MyCommon.TabUsageType.DirectMessage) @@ -6297,9 +5047,9 @@ private void GoSamePostToAnotherTab(bool left) var foundIndex = targetTab.IndexOf(selectedStatusId); if (foundIndex != -1) { - ListTab.SelectedIndex = tabidx; + this.ListTab.SelectedIndex = tabidx; var listView = this.CurrentListView; - SelectListItem(listView, foundIndex); + this.SelectListItem(listView, foundIndex); listView.EnsureVisible(foundIndex); return; } @@ -6350,7 +5100,7 @@ private void GoPost(bool forward) if (post.ScreenName == name) { var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); break; } @@ -6360,7 +5110,7 @@ private void GoPost(bool forward) if (post.RetweetedBy == name) { var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); break; } @@ -6393,32 +5143,31 @@ private void GoRelPost(bool forward) stp = -1; } - if (!_anchorFlag) + var anchorPost = tab.AnchorPost; + if (anchorPost == null) { var currentPost = this.CurrentPost; - if (currentPost == null) return; - _anchorPost = currentPost; - _anchorFlag = true; - } - else - { - if (_anchorPost == null) return; + if (currentPost == null) + return; + + anchorPost = currentPost; + tab.AnchorPost = currentPost; } for (var idx = fIdx; idx != toIdx; idx += stp) { var post = tab[idx]; - if (post.ScreenName == _anchorPost.ScreenName || - post.RetweetedBy == _anchorPost.ScreenName || - post.ScreenName == _anchorPost.RetweetedBy || - (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == _anchorPost.RetweetedBy) || - _anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) || - _anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) || - post.ReplyToList.Any(x => x.UserId == _anchorPost.UserId) || - post.ReplyToList.Any(x => x.UserId == _anchorPost.RetweetedByUserId)) + if (post.ScreenName == anchorPost.ScreenName || + post.RetweetedBy == anchorPost.ScreenName || + post.ScreenName == anchorPost.RetweetedBy || + (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == anchorPost.RetweetedBy) || + anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) || + anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) || + post.ReplyToList.Any(x => x.UserId == anchorPost.UserId) || + post.ReplyToList.Any(x => x.UserId == anchorPost.RetweetedByUserId)) { var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); break; } @@ -6427,41 +5176,45 @@ private void GoRelPost(bool forward) private void GoAnchor() { - if (_anchorPost == null) return; - var idx = this.CurrentTab.IndexOf(_anchorPost.StatusId); - if (idx == -1) return; + var anchorStatusId = this.CurrentTab.AnchorStatusId; + if (anchorStatusId == null) + return; + + var idx = this.CurrentTab.IndexOf(anchorStatusId.Value); + if (idx == -1) + return; var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); } - private void GoTopEnd(bool GoTop) + private void GoTopEnd(bool goTop) { var listView = this.CurrentListView; if (listView.VirtualListSize == 0) return; - ListViewItem _item; + ListViewItem item; int idx; - if (GoTop) + if (goTop) { - _item = listView.GetItemAt(0, 25); - if (_item == null) + item = listView.GetItemAt(0, 25); + if (item == null) idx = 0; else - idx = _item.Index; + idx = item.Index; } else { - _item = listView.GetItemAt(0, listView.ClientSize.Height - 1); - if (_item == null) + item = listView.GetItemAt(0, listView.ClientSize.Height - 1); + if (item == null) idx = listView.VirtualListSize - 1; else - idx = _item.Index; + idx = item.Index; } - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); } private void GoMiddle() @@ -6470,33 +5223,33 @@ private void GoMiddle() if (listView.VirtualListSize == 0) return; - ListViewItem _item; + ListViewItem item; int idx1; int idx2; int idx3; - _item = listView.GetItemAt(0, 0); - if (_item == null) + item = listView.GetItemAt(0, 0); + if (item == null) { idx1 = 0; } else { - idx1 = _item.Index; + idx1 = item.Index; } - _item = listView.GetItemAt(0, listView.ClientSize.Height - 1); - if (_item == null) + item = listView.GetItemAt(0, listView.ClientSize.Height - 1); + if (item == null) { idx2 = listView.VirtualListSize - 1; } else { - idx2 = _item.Index; + idx2 = item.Index; } idx3 = (idx1 + idx2) / 2; - SelectListItem(listView, idx3); + this.SelectListItem(listView, idx3); } private void GoLast() @@ -6504,14 +5257,14 @@ private void GoLast() var listView = this.CurrentListView; if (listView.VirtualListSize == 0) return; - if (_statuses.SortOrder == SortOrder.Ascending) + if (this.statuses.SortOrder == SortOrder.Ascending) { - SelectListItem(listView, listView.VirtualListSize - 1); + this.SelectListItem(listView, listView.VirtualListSize - 1); listView.EnsureVisible(listView.VirtualListSize - 1); } else { - SelectListItem(listView, 0); + this.SelectListItem(listView, 0); listView.EnsureVisible(0); } } @@ -6521,7 +5274,7 @@ private void MoveTop() var listView = this.CurrentListView; if (listView.SelectedIndices.Count == 0) return; var idx = listView.SelectedIndices[0]; - if (_statuses.SortOrder == SortOrder.Ascending) + if (this.statuses.SortOrder == SortOrder.Ascending) { listView.EnsureVisible(listView.VirtualListSize - 1); } @@ -6544,12 +5297,12 @@ private async Task GoInReplyToPostTree() { try { - var post = await tw.GetStatusApi(false, currentPost.StatusId); + var post = await this.tw.GetStatusApi(false, currentPost.StatusId); currentPost.InReplyToStatusId = post.InReplyToStatusId; currentPost.InReplyToUser = post.InReplyToUser; currentPost.IsReply = post.IsReply; - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); var index = curTabClass.SelectedIndex; this.CurrentListView.RedrawItems(index, index, false); @@ -6562,24 +5315,24 @@ private async Task GoInReplyToPostTree() if (!(this.ExistCurrentPost && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)) return; - if (replyChains == null || (replyChains.Count > 0 && replyChains.Peek().InReplyToId != currentPost.StatusId)) + if (this.replyChains == null || (this.replyChains.Count > 0 && this.replyChains.Peek().InReplyToId != currentPost.StatusId)) { - replyChains = new Stack(); + this.replyChains = new Stack(); } - replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass)); + this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass)); int inReplyToIndex; string inReplyToTabName; var inReplyToId = currentPost.InReplyToStatusId.Value; var inReplyToUser = currentPost.InReplyToUser; - var inReplyToPosts = from tab in _statuses.Tabs + var inReplyToPosts = from tab in this.statuses.Tabs orderby tab != curTabClass from post in tab.Posts.Values where post.StatusId == inReplyToId let index = tab.IndexOf(post.StatusId) where index != -1 - select new {Tab = tab, Index = index}; + select new { Tab = tab, Index = index }; var inReplyPost = inReplyToPosts.FirstOrDefault(); if (inReplyPost == null) @@ -6588,12 +5341,12 @@ from post in tab.Posts.Values { await Task.Run(async () => { - var post = await tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value) + var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value) .ConfigureAwait(false); post.IsRead = true; - _statuses.AddPost(post); - _statuses.DistributePosts(); + this.statuses.AddPost(post); + this.statuses.DistributePosts(); }); } catch (WebApiException ex) @@ -6615,7 +5368,7 @@ await Task.Run(async () => inReplyToTabName = inReplyPost.Tab.TabName; inReplyToIndex = inReplyPost.Index; - var tabIndex = this._statuses.Tabs.IndexOf(inReplyToTabName); + var tabIndex = this.statuses.Tabs.IndexOf(inReplyToTabName); var tabPage = this.ListTab.TabPages[tabIndex]; var listView = (DetailsListView)tabPage.Tag; @@ -6640,14 +5393,14 @@ private void GoBackInReplyToPostTree(bool parallel = false, bool isForward = tru { if (currentPost.InReplyToStatusId != null) { - var posts = from t in _statuses.Tabs + var posts = from t in this.statuses.Tabs from p in t.Posts where p.Value.StatusId != currentPost.StatusId && p.Value.InReplyToStatusId == currentPost.InReplyToStatusId let indexOf = t.IndexOf(p.Value.StatusId) where indexOf > -1 orderby isForward ? indexOf : indexOf * -1 orderby t != curTabClass - select new {Tab = t, Post = p.Value, Index = indexOf}; + select new { Tab = t, Post = p.Value, Index = indexOf }; try { var postList = posts.ToList(); @@ -6663,10 +5416,10 @@ where indexOf > -1 var post = postList.FirstOrDefault(pst => pst.Tab == curTabClass && isForward ? pst.Index > currentIndex : pst.Index < currentIndex); if (post == null) post = postList.FirstOrDefault(pst => pst.Tab != curTabClass); if (post == null) post = postList.First(); - var tabIndex = this._statuses.Tabs.IndexOf(post.Tab); + var tabIndex = this.statuses.Tabs.IndexOf(post.Tab); this.ListTab.SelectedIndex = tabIndex; var listView = this.CurrentListView; - SelectListItem(listView, post.Index); + this.SelectListItem(listView, post.Index); listView.EnsureVisible(post.Index); } catch (InvalidOperationException) @@ -6677,23 +5430,23 @@ where indexOf > -1 } else { - if (replyChains == null || replyChains.Count < 1) + if (this.replyChains == null || this.replyChains.Count < 1) { - var posts = from t in _statuses.Tabs + var posts = from t in this.statuses.Tabs from p in t.Posts where p.Value.InReplyToStatusId == currentPost.StatusId let indexOf = t.IndexOf(p.Value.StatusId) where indexOf > -1 orderby indexOf orderby t != curTabClass - select new {Tab = t, Index = indexOf}; + select new { Tab = t, Index = indexOf }; try { var post = posts.First(); - var tabIndex = this._statuses.Tabs.IndexOf(post.Tab); + var tabIndex = this.statuses.Tabs.IndexOf(post.Tab); this.ListTab.SelectedIndex = tabIndex; var listView = this.CurrentListView; - SelectListItem(listView, post.Index); + this.SelectListItem(listView, post.Index); listView.EnsureVisible(post.Index); } catch (InvalidOperationException) @@ -6703,41 +5456,41 @@ orderby indexOf } else { - var chainHead = replyChains.Pop(); + var chainHead = this.replyChains.Pop(); if (chainHead.InReplyToId == currentPost.StatusId) { var tab = chainHead.OriginalTab; - if (!this._statuses.Tabs.Contains(tab)) + if (!this.statuses.Tabs.Contains(tab)) { - replyChains = null; + this.replyChains = null; } else { var idx = tab.IndexOf(chainHead.OriginalId); if (idx == -1) { - replyChains = null; + this.replyChains = null; } else { - var tabIndex = this._statuses.Tabs.IndexOf(tab); + var tabIndex = this.statuses.Tabs.IndexOf(tab); try { this.ListTab.SelectedIndex = tabIndex; } catch (Exception) { - replyChains = null; + this.replyChains = null; } var listView = this.CurrentListView; - SelectListItem(listView, idx); + this.SelectListItem(listView, idx); listView.EnsureVisible(idx); } } } else { - replyChains = null; + this.replyChains = null; this.GoBackInReplyToPostTree(parallel); } } @@ -6758,13 +5511,13 @@ private void GoBackSelectPostChain() this.selectPostChains.Pop(); var (tab, post) = this.selectPostChains.Peek(); - if (!this._statuses.Tabs.Contains(tab)) + if (!this.statuses.Tabs.Contains(tab)) continue; // 該当タブが存在しないので無視 if (post != null) { idx = tab.IndexOf(post.StatusId); - if (idx == -1) continue; //該当ポストが存在しないので無視 + if (idx == -1) continue; // 該当ポストが存在しないので無視 } foundTab = tab; @@ -6781,21 +5534,21 @@ private void GoBackSelectPostChain() if (foundTab == null) { - //状態がおかしいので処理を中断 - //履歴が残り1つであればクリアしておく + // 状態がおかしいので処理を中断 + // 履歴が残り1つであればクリアしておく if (this.selectPostChains.Count == 1) this.selectPostChains.Clear(); return; } - var tabIndex = this._statuses.Tabs.IndexOf(foundTab); + var tabIndex = this.statuses.Tabs.IndexOf(foundTab); var tabPage = this.ListTab.TabPages[tabIndex]; var lst = (DetailsListView)tabPage.Tag; this.ListTab.SelectedIndex = tabIndex; if (idx > -1) { - SelectListItem(lst, idx); + this.SelectListItem(lst, idx); lst.EnsureVisible(idx); } lst.Focus(); @@ -6813,11 +5566,11 @@ private void PushSelectPostChain() var (tab, post) = this.selectPostChains.Peek(); if (tab == currentTab) { - if (post == currentPost) return; //最新の履歴と同一 - if (post == null) this.selectPostChains.Pop(); //置き換えるため削除 + if (post == currentPost) return; // 最新の履歴と同一 + if (post == null) this.selectPostChains.Pop(); // 置き換えるため削除 } } - if (count >= 2500) TrimPostChain(); + if (count >= 2500) this.TrimPostChain(); this.selectPostChains.Push((currentTab, currentPost)); } @@ -6840,7 +5593,7 @@ private bool GoStatus(long statusId) { if (statusId == 0) return false; - var tab = this._statuses.Tabs + var tab = this.statuses.Tabs .Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage) .Where(x => x.Contains(statusId)) .FirstOrDefault(); @@ -6850,7 +5603,7 @@ private bool GoStatus(long statusId) var index = tab.IndexOf(statusId); - var tabIndex = this._statuses.Tabs.IndexOf(tab); + var tabIndex = this.statuses.Tabs.IndexOf(tab); this.ListTab.SelectedIndex = tabIndex; var listView = this.CurrentListView; @@ -6864,13 +5617,13 @@ private bool GoDirectMessage(long statusId) { if (statusId == 0) return false; - var tab = this._statuses.DirectMessageTab; + var tab = this.statuses.DirectMessageTab; var index = tab.IndexOf(statusId); if (index == -1) return false; - var tabIndex = this._statuses.Tabs.IndexOf(tab); + var tabIndex = this.statuses.Tabs.IndexOf(tab); this.ListTab.SelectedIndex = tabIndex; var listView = this.CurrentListView; @@ -6881,31 +5634,28 @@ private bool GoDirectMessage(long statusId) } private void MyList_MouseClick(object sender, MouseEventArgs e) - => this._anchorFlag = false; + => this.CurrentTab.ClearAnchor(); private void StatusText_Enter(object sender, EventArgs e) { // フォーカスの戻り先を StatusText に設定 - this.Tag = StatusText; - StatusText.BackColor = _clInputBackcolor; + this.Tag = this.StatusText; + this.StatusText.BackColor = this.themeManager.ColorInputBackcolor; } public Color InputBackColor - { - get => _clInputBackcolor; - set => _clInputBackcolor = value; - } + => this.themeManager.ColorInputBackcolor; private void StatusText_Leave(object sender, EventArgs e) { // フォーカスがメニューに遷移しないならばフォーカスはタブに移ることを期待 - if (ListTab.SelectedTab != null && MenuStrip1.Tag == null) this.Tag = ListTab.SelectedTab.Tag; - StatusText.BackColor = Color.FromKnownColor(KnownColor.Window); + if (this.ListTab.SelectedTab != null && this.MenuStrip1.Tag == null) this.Tag = this.ListTab.SelectedTab.Tag; + this.StatusText.BackColor = Color.FromKnownColor(KnownColor.Window); } private async void StatusText_KeyDown(object sender, KeyEventArgs e) { - if (CommonKeyDown(e.KeyData, FocusedControl.StatusText, out var asyncTask)) + if (this.CommonKeyDown(e.KeyData, FocusedControl.StatusText, out var asyncTask)) { e.Handled = true; e.SuppressKeyPress = true; @@ -6921,106 +5671,84 @@ private void SaveConfigsAll(bool ifModified) { if (!ifModified) { - SaveConfigsCommon(); - SaveConfigsLocal(); - SaveConfigsTabs(); - SaveConfigsAtId(); + this.SaveConfigsCommon(); + this.SaveConfigsLocal(); + this.SaveConfigsTabs(); + this.SaveConfigsAtId(); } else { - if (ModifySettingCommon) SaveConfigsCommon(); - if (ModifySettingLocal) SaveConfigsLocal(); - if (ModifySettingAtId) SaveConfigsAtId(); + if (this.ModifySettingCommon) this.SaveConfigsCommon(); + if (this.ModifySettingLocal) this.SaveConfigsLocal(); + if (this.ModifySettingAtId) this.SaveConfigsAtId(); } } private void SaveConfigsAtId() { - if (_ignoreConfigSave || !SettingManager.Common.UseAtIdSupplement && AtIdSupl == null) return; + if (this.ignoreConfigSave || !this.settings.Common.UseAtIdSupplement && this.AtIdSupl == null) return; - ModifySettingAtId = false; - SettingManager.AtIdList.AtIdList = this.AtIdSupl.GetItemList(); - SettingManager.SaveAtIdList(); + this.ModifySettingAtId = false; + this.settings.AtIdList.AtIdList = this.AtIdSupl.GetItemList(); + this.settings.SaveAtIdList(); } private void SaveConfigsCommon() { - if (_ignoreConfigSave) return; + if (this.ignoreConfigSave) return; - ModifySettingCommon = false; - lock (_syncObject) + this.ModifySettingCommon = false; + lock (this.syncObject) { - SettingManager.Common.UserName = tw.Username; - SettingManager.Common.UserId = tw.UserId; - SettingManager.Common.Token = tw.AccessToken; - SettingManager.Common.TokenSecret = tw.AccessTokenSecret; - SettingManager.Common.SortOrder = (int)_statuses.SortOrder; - SettingManager.Common.SortColumn = this._statuses.SortMode switch + this.settings.Common.UserName = this.tw.Username; + this.settings.Common.UserId = this.tw.UserId; + this.settings.Common.Token = this.tw.AccessToken; + this.settings.Common.TokenSecret = this.tw.AccessTokenSecret; + this.settings.Common.SortOrder = (int)this.statuses.SortOrder; + this.settings.Common.SortColumn = this.statuses.SortMode switch { ComparerMode.Nickname => 1, // ニックネーム ComparerMode.Data => 2, // 本文 ComparerMode.Id => 3, // 時刻=発言Id ComparerMode.Name => 4, // 名前 ComparerMode.Source => 7, // Source - _ => throw new InvalidOperationException($"Invalid sort mode: {this._statuses.SortMode}"), + _ => throw new InvalidOperationException($"Invalid sort mode: {this.statuses.SortMode}"), }; - SettingManager.Common.HashTags = HashMgr.HashHistories; - if (HashMgr.IsPermanent) + this.settings.Common.HashTags = this.HashMgr.HashHistories; + if (this.HashMgr.IsPermanent) { - SettingManager.Common.HashSelected = HashMgr.UseHash; + this.settings.Common.HashSelected = this.HashMgr.UseHash; } else { - SettingManager.Common.HashSelected = ""; + this.settings.Common.HashSelected = ""; } - SettingManager.Common.HashIsHead = HashMgr.IsHead; - SettingManager.Common.HashIsPermanent = HashMgr.IsPermanent; - SettingManager.Common.HashIsNotAddToAtReply = HashMgr.IsNotAddToAtReply; - SettingManager.Common.UseImageService = ImageSelector.ServiceIndex; - SettingManager.Common.UseImageServiceName = ImageSelector.ServiceName; + this.settings.Common.HashIsHead = this.HashMgr.IsHead; + this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent; + this.settings.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply; + this.settings.Common.UseImageService = this.ImageSelector.ServiceIndex; + this.settings.Common.UseImageServiceName = this.ImageSelector.ServiceName; - SettingManager.SaveCommon(); + this.settings.SaveCommon(); } } private void SaveConfigsLocal() { - if (_ignoreConfigSave) return; - lock (_syncObject) - { - ModifySettingLocal = false; - SettingManager.Local.ScaleDimension = this.CurrentAutoScaleDimensions; - SettingManager.Local.FormSize = _mySize; - SettingManager.Local.FormLocation = _myLoc; - SettingManager.Local.SplitterDistance = _mySpDis; - SettingManager.Local.PreviewDistance = _mySpDis3; - SettingManager.Local.StatusMultiline = StatusText.Multiline; - SettingManager.Local.StatusTextHeight = _mySpDis2; - - SettingManager.Local.FontUnread = _fntUnread; - SettingManager.Local.ColorUnread = _clUnread; - SettingManager.Local.FontRead = _fntReaded; - SettingManager.Local.ColorRead = _clReaded; - SettingManager.Local.FontDetail = _fntDetail; - SettingManager.Local.ColorDetail = _clDetail; - SettingManager.Local.ColorDetailBackcolor = _clDetailBackcolor; - SettingManager.Local.ColorDetailLink = _clDetailLink; - SettingManager.Local.ColorFav = _clFav; - SettingManager.Local.ColorOWL = _clOWL; - SettingManager.Local.ColorRetweet = _clRetweet; - SettingManager.Local.ColorSelf = _clSelf; - SettingManager.Local.ColorAtSelf = _clAtSelf; - SettingManager.Local.ColorTarget = _clTarget; - SettingManager.Local.ColorAtTarget = _clAtTarget; - SettingManager.Local.ColorAtFromTarget = _clAtFromTarget; - SettingManager.Local.ColorAtTo = _clAtTo; - SettingManager.Local.ColorListBackcolor = _clListBackcolor; - SettingManager.Local.ColorInputBackcolor = _clInputBackcolor; - SettingManager.Local.ColorInputFont = _clInputFont; - SettingManager.Local.FontInputFont = _fntInputFont; - - if (_ignoreConfigSave) return; - SettingManager.SaveLocal(); + if (this.ignoreConfigSave) return; + lock (this.syncObject) + { + this.ModifySettingLocal = false; + this.settings.Local.ScaleDimension = this.CurrentAutoScaleDimensions; + this.settings.Local.FormSize = this.mySize; + this.settings.Local.FormLocation = this.myLoc; + this.settings.Local.SplitterDistance = this.mySpDis; + this.settings.Local.PreviewDistance = this.mySpDis3; + this.settings.Local.StatusMultiline = this.StatusText.Multiline; + this.settings.Local.StatusTextHeight = this.mySpDis2; + + if (this.ignoreConfigSave) return; + this.settings.SaveLocal(); } } @@ -7028,7 +5756,7 @@ private void SaveConfigsTabs() { var tabSettingList = new List(); - var tabs = this._statuses.Tabs.Append(this._statuses.MuteTab); + var tabs = this.statuses.Tabs.Append(this.statuses.MuteTab); foreach (var tab in tabs) { @@ -7065,12 +5793,23 @@ private void SaveConfigsTabs() tabSettingList.Add(tabSetting); } - SettingManager.Tabs.Tabs = tabSettingList; - SettingManager.SaveTabs(); + this.settings.Tabs.Tabs = tabSettingList; + this.settings.SaveTabs(); } private async void OpenURLFileMenuItem_Click(object sender, EventArgs e) { + static void ShowFormatErrorDialog(IWin32Window owner) + { + MessageBox.Show( + owner, + Properties.Resources.OpenURL_InvalidFormat, + Properties.Resources.OpenURL_Caption, + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + var ret = InputDialog.Show(this, Properties.Resources.OpenURL_InputText, Properties.Resources.OpenURL_Caption, out var inputText); if (ret != DialogResult.OK) return; @@ -7078,8 +5817,7 @@ private async void OpenURLFileMenuItem_Click(object sender, EventArgs e) var match = Twitter.StatusUrlRegex.Match(inputText); if (!match.Success) { - MessageBox.Show(this, Properties.Resources.OpenURL_InvalidFormat, - Properties.Resources.OpenURL_Caption, MessageBoxButtons.OK, MessageBoxIcon.Error); + ShowFormatErrorDialog(this); return; } @@ -7088,6 +5826,10 @@ private async void OpenURLFileMenuItem_Click(object sender, EventArgs e) var statusId = long.Parse(match.Groups["StatusId"].Value); await this.OpenRelatedTab(statusId); } + catch (OverflowException) + { + ShowFormatErrorDialog(this); + } catch (TabException ex) { MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error); @@ -7098,25 +5840,27 @@ private void SaveLogMenuItem_Click(object sender, EventArgs e) { var tab = this.CurrentTab; - var rslt = MessageBox.Show(string.Format(Properties.Resources.SaveLogMenuItem_ClickText1, Environment.NewLine), - Properties.Resources.SaveLogMenuItem_ClickText2, - MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + var rslt = MessageBox.Show( + string.Format(Properties.Resources.SaveLogMenuItem_ClickText1, Environment.NewLine), + Properties.Resources.SaveLogMenuItem_ClickText2, + MessageBoxButtons.YesNoCancel, + MessageBoxIcon.Question); if (rslt == DialogResult.Cancel) return; - SaveFileDialog1.FileName = $"{ApplicationSettings.AssemblyName}Posts{DateTimeUtc.Now.ToLocalTime():yyMMdd-HHmmss}.tsv"; - SaveFileDialog1.InitialDirectory = Application.ExecutablePath; - SaveFileDialog1.Filter = Properties.Resources.SaveLogMenuItem_ClickText3; - SaveFileDialog1.FilterIndex = 0; - SaveFileDialog1.Title = Properties.Resources.SaveLogMenuItem_ClickText4; - SaveFileDialog1.RestoreDirectory = true; + this.SaveFileDialog1.FileName = $"{ApplicationSettings.AssemblyName}Posts{DateTimeUtc.Now.ToLocalTime():yyMMdd-HHmmss}.tsv"; + this.SaveFileDialog1.InitialDirectory = Application.ExecutablePath; + this.SaveFileDialog1.Filter = Properties.Resources.SaveLogMenuItem_ClickText3; + this.SaveFileDialog1.FilterIndex = 0; + this.SaveFileDialog1.Title = Properties.Resources.SaveLogMenuItem_ClickText4; + this.SaveFileDialog1.RestoreDirectory = true; - if (SaveFileDialog1.ShowDialog() == DialogResult.OK) + if (this.SaveFileDialog1.ShowDialog() == DialogResult.OK) { - if (!SaveFileDialog1.ValidateNames) return; - using var sw = new StreamWriter(SaveFileDialog1.FileName, false, Encoding.UTF8); + if (!this.SaveFileDialog1.ValidateNames) return; + using var sw = new StreamWriter(this.SaveFileDialog1.FileName, false, Encoding.UTF8); if (rslt == DialogResult.Yes) { - //All + // All for (var idx = 0; idx < tab.AllCount; idx++) { var post = tab[idx]; @@ -7151,12 +5895,12 @@ private void SaveLogMenuItem_Click(object sender, EventArgs e) } } } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; } public bool TabRename(string origTabName, [NotNullWhen(true)] out string? newTabName) { - //タブ名変更 + // タブ名変更 newTabName = null; using (var inputName = new InputTabName()) { @@ -7165,29 +5909,29 @@ public bool TabRename(string origTabName, [NotNullWhen(true)] out string? newTab if (inputName.DialogResult == DialogResult.Cancel) return false; newTabName = inputName.TabName; } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; if (!MyCommon.IsNullOrEmpty(newTabName)) { - //新タブ名存在チェック - if (this._statuses.ContainsTab(newTabName)) + // 新タブ名存在チェック + if (this.statuses.ContainsTab(newTabName)) { var tmp = string.Format(Properties.Resources.Tabs_DoubleClickText1, newTabName); MessageBox.Show(tmp, Properties.Resources.Tabs_DoubleClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return false; } - var tabIndex = this._statuses.Tabs.IndexOf(origTabName); + var tabIndex = this.statuses.Tabs.IndexOf(origTabName); var tabPage = this.ListTab.TabPages[tabIndex]; // タブ名を変更 if (tabPage != null) tabPage.Text = newTabName; - _statuses.RenameTab(origTabName, newTabName); + this.statuses.RenameTab(origTabName, newTabName); - SaveConfigsCommon(); - SaveConfigsTabs(); - _rclickTabName = newTabName; + this.SaveConfigsCommon(); + this.SaveConfigsTabs(); + this.rclickTabName = newTabName; return true; } else @@ -7200,7 +5944,7 @@ private void ListTab_MouseClick(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Middle) { - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { if (this.ListTab.GetTabRect(index).Contains(e.Location)) { @@ -7217,22 +5961,22 @@ private void ListTab_DoubleClick(object sender, MouseEventArgs e) private void ListTab_MouseDown(object sender, MouseEventArgs e) { - if (SettingManager.Common.TabMouseLock) return; + if (this.settings.Common.TabMouseLock) return; if (e.Button == MouseButtons.Left) { - foreach (var i in Enumerable.Range(0, this._statuses.Tabs.Count)) + foreach (var i in Enumerable.Range(0, this.statuses.Tabs.Count)) { if (this.ListTab.GetTabRect(i).Contains(e.Location)) { - _tabDrag = true; - _tabMouseDownPoint = e.Location; + this.tabDrag = true; + this.tabMouseDownPoint = e.Location; break; } } } else { - _tabDrag = false; + this.tabDrag = false; } } @@ -7248,14 +5992,14 @@ private void ListTab_DragDrop(object sender, DragEventArgs e) { if (!e.Data.GetDataPresent(typeof(TabPage))) return; - _tabDrag = false; + this.tabDrag = false; var tn = ""; var bef = false; var cpos = new Point(e.X, e.Y); - var spos = ListTab.PointToClient(cpos); - foreach (var (tab, index) in this._statuses.Tabs.WithIndex()) + var spos = this.ListTab.PointToClient(cpos); + foreach (var (tab, index) in this.statuses.Tabs.WithIndex()) { - var rect = ListTab.GetTabRect(index); + var rect = this.ListTab.GetTabRect(index); if (rect.Contains(spos)) { tn = tab.TabName; @@ -7268,10 +6012,10 @@ private void ListTab_DragDrop(object sender, DragEventArgs e) } } - //タブのないところにドロップ->最後尾へ移動 + // タブのないところにドロップ->最後尾へ移動 if (MyCommon.IsNullOrEmpty(tn)) { - var lastTab = this._statuses.Tabs.Last(); + var lastTab = this.statuses.Tabs.Last(); tn = lastTab.TabName; bef = false; } @@ -7279,7 +6023,7 @@ private void ListTab_DragDrop(object sender, DragEventArgs e) var tp = (TabPage)e.Data.GetData(typeof(TabPage)); if (tp.Text == tn) return; - ReOrderTab(tp.Text, tn, bef); + this.ReOrderTab(tp.Text, tn, bef); } public void ReOrderTab(string targetTabText, string baseTabText, bool isBeforeBaseTab) @@ -7294,7 +6038,7 @@ public void ReOrderTab(string targetTabText, string baseTabText, bool isBeforeBa using (ControlTransaction.Layout(this.ListTab)) { - var tab = this._statuses.Tabs[targetIndex]; + var tab = this.statuses.Tabs[targetIndex]; var tabPage = this.ListTab.TabPages[targetIndex]; this.ListTab.TabPages.Remove(tabPage); @@ -7305,262 +6049,89 @@ public void ReOrderTab(string targetTabText, string baseTabText, bool isBeforeBa if (!isBeforeBaseTab) baseIndex++; - this._statuses.MoveTab(baseIndex, tab); - - ListTab.TabPages.Insert(baseIndex, tabPage); - } - - SaveConfigsTabs(); - } - - private void MakeReplyOrDirectStatus(bool isAuto = true, bool isReply = true, bool isAll = false) - { - //isAuto:true=先頭に挿入、false=カーソル位置に挿入 - //isReply:true=@,false=DM - if (!StatusText.Enabled) return; - if (!this.ExistCurrentPost) return; - - var tab = this.CurrentTab; - var selectedPosts = tab.SelectedPosts; - - // 複数あてリプライはReplyではなく通常ポスト - //↑仕様変更で全部リプライ扱いでOK(先頭ドット付加しない) - //090403暫定でドットを付加しないようにだけ修正。単独と複数の処理は統合できると思われる。 - //090513 all @ replies 廃止の仕様変更によりドット付加に戻し(syo68k) - - if (selectedPosts.Length > 0) - { - // アイテムが1件以上選択されている - if (selectedPosts.Length == 1 && !isAll && this.ExistCurrentPost) - { - var post = selectedPosts.Single(); - - // 単独ユーザー宛リプライまたはDM - if ((tab.TabType == MyCommon.TabUsageType.DirectMessage && isAuto) || (!isAuto && !isReply)) - { - // ダイレクトメッセージ - this.inReplyTo = null; - StatusText.Text = "D " + post.ScreenName + " " + StatusText.Text; - StatusText.SelectionStart = StatusText.Text.Length; - StatusText.Focus(); - return; - } - if (MyCommon.IsNullOrEmpty(StatusText.Text)) - { - //空の場合 - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; - var inReplyToScreenName = post.ScreenName; - this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); - - // ステータステキストが入力されていない場合先頭に@ユーザー名を追加する - StatusText.Text = "@" + post.ScreenName + " "; - } - else - { - //何か入力済の場合 - - if (isAuto) - { - //1件選んでEnter or DoubleClick - if (StatusText.Text.Contains("@" + post.ScreenName + " ")) - { - if (this.inReplyTo?.ScreenName == post.ScreenName) - { - //返信先書き換え - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; - var inReplyToScreenName = post.ScreenName; - this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); - } - return; - } - if (!StatusText.Text.StartsWith("@", StringComparison.Ordinal)) - { - //文頭@以外 - if (StatusText.Text.StartsWith(". ", StringComparison.Ordinal)) - { - // 複数リプライ - this.inReplyTo = null; - StatusText.Text = StatusText.Text.Insert(2, "@" + post.ScreenName + " "); - } - else - { - // 単独リプライ - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; - var inReplyToScreenName = post.ScreenName; - this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); - StatusText.Text = "@" + post.ScreenName + " " + StatusText.Text; - } - } - else - { - //文頭@ - // 複数リプライ - this.inReplyTo = null; - StatusText.Text = ". @" + post.ScreenName + " " + StatusText.Text; - } - } - else - { - //1件選んでCtrl-Rの場合(返信先操作せず) - var sidx = StatusText.SelectionStart; - var id = "@" + post.ScreenName + " "; - if (sidx > 0) - { - if (StatusText.Text.Substring(sidx - 1, 1) != " ") - { - id = " " + id; - } - } - StatusText.Text = StatusText.Text.Insert(sidx, id); - sidx += id.Length; - StatusText.SelectionStart = sidx; - StatusText.Focus(); - return; - } - } - } - else - { - // 複数リプライ - if (!isAuto && !isReply) return; - - //C-S-rか、複数の宛先を選択中にEnter/DoubleClick/C-r/C-S-r + this.statuses.MoveTab(baseIndex, tab); - if (isAuto) - { - //Enter or DoubleClick + this.ListTab.TabPages.Insert(baseIndex, tabPage); + } - var sTxt = StatusText.Text; - if (!sTxt.StartsWith(". ", StringComparison.Ordinal)) - { - sTxt = ". " + sTxt; - this.inReplyTo = null; - } - foreach (var post in selectedPosts) - { - if (!sTxt.Contains("@" + post.ScreenName + " ")) - sTxt = sTxt.Insert(2, "@" + post.ScreenName + " "); - } - StatusText.Text = sTxt; - } - else - { - //C-S-r or C-r + this.SaveConfigsTabs(); + } - if (selectedPosts.Length > 1) - { - //複数ポスト選択 + private void MakeDirectMessageText() + { + var selectedPosts = this.CurrentTab.SelectedPosts; + if (selectedPosts.Length > 1) + return; - var ids = ""; - var sidx = StatusText.SelectionStart; - foreach (var post in selectedPosts) - { - if (!ids.Contains("@" + post.ScreenName + " ") && post.UserId != tw.UserId) - { - ids += "@" + post.ScreenName + " "; - } - if (isAll) - { - foreach (var (_, screenName) in post.ReplyToList) - { - if (!ids.Contains("@" + screenName + " ") && - !screenName.Equals(tw.Username, StringComparison.CurrentCultureIgnoreCase)) - { - var m = Regex.Match(post.TextFromApi, "[@@](?" + screenName + ")([^a-zA-Z0-9]|$)", RegexOptions.IgnoreCase); - if (m.Success) - ids += "@" + m.Result("${id}") + " "; - else - ids += "@" + screenName + " "; - } - } - } - } - if (ids.Length == 0) return; - if (!StatusText.Text.StartsWith(". ", StringComparison.Ordinal)) - { - this.inReplyTo = null; - StatusText.Text = ". " + StatusText.Text; - sidx += 2; - } - if (sidx > 0) - { - if (StatusText.Text.Substring(sidx - 1, 1) != " ") - { - ids = " " + ids; - } - } - StatusText.Text = StatusText.Text.Insert(sidx, ids); - sidx += ids.Length; - StatusText.SelectionStart = sidx; - StatusText.Focus(); - return; - } - else - { - //1件のみ選択のC-S-r(返信元付加する可能性あり) + var post = selectedPosts.Single(); + var text = $"D {post.ScreenName} {this.StatusText.Text}"; - var ids = ""; - var sidx = StatusText.SelectionStart; - var post = selectedPosts.Single(); - if (!ids.Contains("@" + post.ScreenName + " ") && post.UserId != tw.UserId) - { - ids += "@" + post.ScreenName + " "; - } - foreach (var (_, screenName) in post.ReplyToList) - { - if (!ids.Contains("@" + screenName + " ") && - !screenName.Equals(tw.Username, StringComparison.CurrentCultureIgnoreCase)) - { - var m = Regex.Match(post.TextFromApi, "[@@](?" + screenName + ")([^a-zA-Z0-9]|$)", RegexOptions.IgnoreCase); - if (m.Success) - ids += "@" + m.Result("${id}") + " "; - else - ids += "@" + screenName + " "; - } - } - if (!MyCommon.IsNullOrEmpty(post.RetweetedBy)) - { - if (!ids.Contains("@" + post.RetweetedBy + " ") && post.RetweetedByUserId != tw.UserId) - { - ids += "@" + post.RetweetedBy + " "; - } - } - if (ids.Length == 0) return; - if (MyCommon.IsNullOrEmpty(StatusText.Text)) - { - //未入力の場合のみ返信先付加 - var inReplyToStatusId = post.RetweetedId ?? post.StatusId; - var inReplyToScreenName = post.ScreenName; - this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); - - StatusText.Text = ids; - StatusText.SelectionStart = ids.Length; - StatusText.Focus(); - return; - } + this.inReplyTo = null; + this.StatusText.Text = text; + this.StatusText.SelectionStart = text.Length; + this.StatusText.Focus(); + } - if (sidx > 0) - { - if (StatusText.Text.Substring(sidx - 1, 1) != " ") - { - ids = " " + ids; - } - } - StatusText.Text = StatusText.Text.Insert(sidx, ids); - sidx += ids.Length; - StatusText.SelectionStart = sidx; - StatusText.Focus(); - return; - } + private void MakeReplyText(bool atAll = false) + { + var selectedPosts = this.CurrentTab.SelectedPosts; + if (selectedPosts.Any(x => x.IsDm)) + { + this.MakeDirectMessageText(); + return; + } + + if (selectedPosts.Length == 1) + { + var post = selectedPosts.Single(); + var inReplyToStatusId = post.RetweetedId ?? post.StatusId; + var inReplyToScreenName = post.ScreenName; + this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); + } + else + { + this.inReplyTo = null; + } + + var selfScreenName = this.tw.Username; + var targetScreenNames = new List(); + foreach (var post in selectedPosts) + { + if (post.ScreenName != selfScreenName) + targetScreenNames.Add(post.ScreenName); + + if (atAll) + { + foreach (var (_, screenName) in post.ReplyToList) + { + if (screenName != selfScreenName) + targetScreenNames.Add(screenName); } } - StatusText.SelectionStart = StatusText.Text.Length; - StatusText.Focus(); } + + if (this.inReplyTo != null) + { + var (_, screenName) = this.inReplyTo.Value; + if (screenName == selfScreenName) + targetScreenNames.Insert(0, screenName); + } + + var text = this.StatusText.Text; + foreach (var screenName in targetScreenNames.AsEnumerable().Reverse()) + { + var atText = $"@{screenName} "; + if (!text.Contains(atText)) + text = atText + text; + } + + this.StatusText.Text = text; + this.StatusText.SelectionStart = text.Length; + this.StatusText.Focus(); } private void ListTab_MouseUp(object sender, MouseEventArgs e) - => this._tabDrag = false; + => this.tabDrag = false; private int iconCnt = 0; private int blinkCnt = 0; @@ -7574,58 +6145,58 @@ void EnableTasktrayAnimation() void DisableTasktrayAnimation() => this.TimerRefreshIcon.Enabled = false; - var busyTasks = this.workerSemaphore.CurrentCount != MAX_WORKER_THREADS; + var busyTasks = this.workerSemaphore.CurrentCount != MaxWorderThreads; if (busyTasks) { - iconCnt += 1; - if (iconCnt >= this.NIconRefresh.Length) - iconCnt = 0; + this.iconCnt += 1; + if (this.iconCnt >= this.iconAssets.IconTrayRefresh.Length) + this.iconCnt = 0; - NotifyIcon1.Icon = NIconRefresh[iconCnt]; - _myStatusError = false; + this.NotifyIcon1.Icon = this.iconAssets.IconTrayRefresh[this.iconCnt]; + this.myStatusError = false; EnableTasktrayAnimation(); return; } - var replyIconType = SettingManager.Common.ReplyIconState; + var replyIconType = this.settings.Common.ReplyIconState; var reply = false; if (replyIconType != MyCommon.REPLY_ICONSTATE.None) { - var replyTab = this._statuses.GetTabByType(); + var replyTab = this.statuses.GetTabByType(); if (replyTab != null && replyTab.UnreadCount > 0) reply = true; } if (replyIconType == MyCommon.REPLY_ICONSTATE.BlinkIcon && reply) { - blinkCnt += 1; - if (blinkCnt > 10) - blinkCnt = 0; + this.blinkCnt += 1; + if (this.blinkCnt > 10) + this.blinkCnt = 0; - if (blinkCnt == 0) - blink = !blink; + if (this.blinkCnt == 0) + this.blink = !this.blink; - NotifyIcon1.Icon = blink ? ReplyIconBlink : ReplyIcon; + this.NotifyIcon1.Icon = this.blink ? this.iconAssets.IconTrayReplyBlink : this.iconAssets.IconTrayReply; EnableTasktrayAnimation(); return; } DisableTasktrayAnimation(); - iconCnt = 0; - blinkCnt = 0; - blink = false; + this.iconCnt = 0; + this.blinkCnt = 0; + this.blink = false; // 優先度:リプライ→エラー→オフライン→アイドル // エラーは更新アイコンでクリアされる if (replyIconType == MyCommon.REPLY_ICONSTATE.StaticIcon && reply) - NotifyIcon1.Icon = ReplyIcon; - else if (_myStatusError) - NotifyIcon1.Icon = NIconAtRed; - else if (_myStatusOnline) - NotifyIcon1.Icon = NIconAt; + this.NotifyIcon1.Icon = this.iconAssets.IconTrayReply; + else if (this.myStatusError) + this.NotifyIcon1.Icon = this.iconAssets.IconTrayError; + else if (this.myStatusOnline) + this.NotifyIcon1.Icon = this.iconAssets.IconTray; else - NotifyIcon1.Icon = NIconAtSmoke; + this.NotifyIcon1.Icon = this.iconAssets.IconTrayOffline; } private void TimerRefreshIcon_Tick(object sender, EventArgs e) @@ -7633,23 +6204,23 @@ private void TimerRefreshIcon_Tick(object sender, EventArgs e) private void ContextMenuTabProperty_Opening(object sender, CancelEventArgs e) { - //右クリックの場合はタブ名が設定済。アプリケーションキーの場合は現在のタブを対象とする - if (MyCommon.IsNullOrEmpty(_rclickTabName) || sender != ContextMenuTabProperty) - _rclickTabName = this.CurrentTabName; + // 右クリックの場合はタブ名が設定済。アプリケーションキーの場合は現在のタブを対象とする + if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender != this.ContextMenuTabProperty) + this.rclickTabName = this.CurrentTabName; - if (_statuses == null) return; - if (_statuses.Tabs == null) return; + if (this.statuses == null) return; + if (this.statuses.Tabs == null) return; - if (!this._statuses.Tabs.TryGetValue(this._rclickTabName, out var tb)) + if (!this.statuses.Tabs.TryGetValue(this.rclickTabName, out var tb)) return; - NotifyDispMenuItem.Checked = tb.Notify; + this.NotifyDispMenuItem.Checked = tb.Notify; this.NotifyTbMenuItem.Checked = tb.Notify; - soundfileListup = true; - SoundFileComboBox.Items.Clear(); + this.soundfileListup = true; + this.SoundFileComboBox.Items.Clear(); this.SoundFileTbComboBox.Items.Clear(); - SoundFileComboBox.Items.Add(""); + this.SoundFileComboBox.Items.Add(""); this.SoundFileTbComboBox.Items.Add(""); var oDir = new DirectoryInfo(Application.StartupPath + Path.DirectorySeparatorChar); if (Directory.Exists(Path.Combine(Application.StartupPath, "Sounds"))) @@ -7658,23 +6229,23 @@ private void ContextMenuTabProperty_Opening(object sender, CancelEventArgs e) } foreach (var oFile in oDir.GetFiles("*.wav")) { - SoundFileComboBox.Items.Add(oFile.Name); + this.SoundFileComboBox.Items.Add(oFile.Name); this.SoundFileTbComboBox.Items.Add(oFile.Name); } - var idx = SoundFileComboBox.Items.IndexOf(tb.SoundFile); + var idx = this.SoundFileComboBox.Items.IndexOf(tb.SoundFile); if (idx == -1) idx = 0; - SoundFileComboBox.SelectedIndex = idx; + this.SoundFileComboBox.SelectedIndex = idx; this.SoundFileTbComboBox.SelectedIndex = idx; - soundfileListup = false; - UreadManageMenuItem.Checked = tb.UnreadManage; + this.soundfileListup = false; + this.UreadManageMenuItem.Checked = tb.UnreadManage; this.UnreadMngTbMenuItem.Checked = tb.UnreadManage; - TabMenuControl(_rclickTabName); + this.TabMenuControl(this.rclickTabName); } private void TabMenuControl(string tabName) { - var tabInfo = _statuses.GetTabByName(tabName)!; + var tabInfo = this.statuses.GetTabByName(tabName)!; this.FilterEditMenuItem.Enabled = true; this.EditRuleTbMenuItem.Enabled = true; @@ -7718,21 +6289,21 @@ private void ProtectTabMenuItem_Click(object sender, EventArgs e) this.DeleteTabMenuItem.Enabled = !checkState; this.DeleteTbMenuItem.Enabled = !checkState; - if (MyCommon.IsNullOrEmpty(_rclickTabName)) return; - _statuses.Tabs[_rclickTabName].Protected = checkState; + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return; + this.statuses.Tabs[this.rclickTabName].Protected = checkState; - SaveConfigsTabs(); + this.SaveConfigsTabs(); } private void UreadManageMenuItem_Click(object sender, EventArgs e) { - UreadManageMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; - this.UnreadMngTbMenuItem.Checked = UreadManageMenuItem.Checked; + this.UreadManageMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; + this.UnreadMngTbMenuItem.Checked = this.UreadManageMenuItem.Checked; - if (MyCommon.IsNullOrEmpty(_rclickTabName)) return; - ChangeTabUnreadManage(_rclickTabName, UreadManageMenuItem.Checked); + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return; + this.ChangeTabUnreadManage(this.rclickTabName, this.UreadManageMenuItem.Checked); - SaveConfigsTabs(); + this.SaveConfigsTabs(); } public void ChangeTabUnreadManage(string tabName, bool isManage) @@ -7741,10 +6312,10 @@ public void ChangeTabUnreadManage(string tabName, bool isManage) if (idx == -1) return; - var tab = this._statuses.Tabs[tabName]; + var tab = this.statuses.Tabs[tabName]; tab.UnreadManage = isManage; - if (SettingManager.Common.TabIconDisp) + if (this.settings.Common.TabIconDisp) { var tabPage = this.ListTab.TabPages[idx]; if (tab.UnreadCount > 0) @@ -7755,59 +6326,59 @@ public void ChangeTabUnreadManage(string tabName, bool isManage) if (this.CurrentTabName == tabName) { - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); this.CurrentListView.Refresh(); } - SetMainWindowTitle(); - SetStatusLabelUrl(); - if (!SettingManager.Common.TabIconDisp) ListTab.Refresh(); + this.SetMainWindowTitle(); + this.SetStatusLabelUrl(); + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); } private void NotifyDispMenuItem_Click(object sender, EventArgs e) { - NotifyDispMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; - this.NotifyTbMenuItem.Checked = NotifyDispMenuItem.Checked; + this.NotifyDispMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; + this.NotifyTbMenuItem.Checked = this.NotifyDispMenuItem.Checked; - if (MyCommon.IsNullOrEmpty(_rclickTabName)) return; + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return; - _statuses.Tabs[_rclickTabName].Notify = NotifyDispMenuItem.Checked; + this.statuses.Tabs[this.rclickTabName].Notify = this.NotifyDispMenuItem.Checked; - SaveConfigsTabs(); + this.SaveConfigsTabs(); } private void SoundFileComboBox_SelectedIndexChanged(object sender, EventArgs e) { - if (soundfileListup || MyCommon.IsNullOrEmpty(_rclickTabName)) return; + if (this.soundfileListup || MyCommon.IsNullOrEmpty(this.rclickTabName)) return; - _statuses.Tabs[_rclickTabName].SoundFile = (string)((ToolStripComboBox)sender).SelectedItem; + this.statuses.Tabs[this.rclickTabName].SoundFile = (string)((ToolStripComboBox)sender).SelectedItem; - SaveConfigsTabs(); + this.SaveConfigsTabs(); } private void DeleteTabMenuItem_Click(object sender, EventArgs e) { - if (MyCommon.IsNullOrEmpty(_rclickTabName) || sender == this.DeleteTbMenuItem) - _rclickTabName = this.CurrentTabName; + if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender == this.DeleteTbMenuItem) + this.rclickTabName = this.CurrentTabName; - RemoveSpecifiedTab(_rclickTabName, true); - SaveConfigsTabs(); + this.RemoveSpecifiedTab(this.rclickTabName, true); + this.SaveConfigsTabs(); } private void FilterEditMenuItem_Click(object sender, EventArgs e) { - if (MyCommon.IsNullOrEmpty(_rclickTabName)) _rclickTabName = _statuses.HomeTab.TabName; + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) this.rclickTabName = this.statuses.HomeTab.TabName; using (var fltDialog = new FilterDialog()) { fltDialog.Owner = this; - fltDialog.SetCurrent(_rclickTabName); + fltDialog.SetCurrent(this.rclickTabName); fltDialog.ShowDialog(this); } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; this.ApplyPostFilters(); - SaveConfigsTabs(); + this.SaveConfigsTabs(); } private async void AddTabMenuItem_Click(object sender, EventArgs e) @@ -7816,17 +6387,17 @@ private async void AddTabMenuItem_Click(object sender, EventArgs e) MyCommon.TabUsageType tabUsage; using (var inputName = new InputTabName()) { - inputName.TabName = _statuses.MakeTabName("MyTab"); + inputName.TabName = this.statuses.MakeTabName("MyTab"); inputName.IsShowUsage = true; inputName.ShowDialog(); if (inputName.DialogResult == DialogResult.Cancel) return; tabName = inputName.TabName; tabUsage = inputName.Usage; } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; if (!MyCommon.IsNullOrEmpty(tabName)) { - //List対応 + // List対応 ListElement? list = null; if (tabUsage == MyCommon.TabUsageType.Lists) { @@ -7854,26 +6425,26 @@ private async void AddTabMenuItem_Click(object sender, EventArgs e) return; } - if (!_statuses.AddTab(tab) || !AddNewTab(tab, startup: false)) + if (!this.statuses.AddTab(tab) || !this.AddNewTab(tab, startup: false)) { var tmp = string.Format(Properties.Resources.AddTabMenuItem_ClickText1, tabName); MessageBox.Show(tmp, Properties.Resources.AddTabMenuItem_ClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } else { - //成功 - SaveConfigsTabs(); + // 成功 + this.SaveConfigsTabs(); - var tabIndex = this._statuses.Tabs.Count - 1; + var tabIndex = this.statuses.Tabs.Count - 1; if (tabUsage == MyCommon.TabUsageType.PublicSearch) { - ListTab.SelectedIndex = tabIndex; + this.ListTab.SelectedIndex = tabIndex; this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus(); } if (tabUsage == MyCommon.TabUsageType.Lists) { - ListTab.SelectedIndex = tabIndex; + this.ListTab.SelectedIndex = tabIndex; await this.RefreshTabAsync(this.CurrentTab); } } @@ -7886,7 +6457,7 @@ private void TabMenuItem_Click(object sender, EventArgs e) foreach (var post in this.CurrentTab.SelectedPosts) { // タブ選択(or追加) - if (!SelectTab(out var tab)) + if (!this.SelectTab(out var tab)) return; using (var fltDialog = new FilterDialog()) @@ -7905,82 +6476,80 @@ private void TabMenuItem_Click(object sender, EventArgs e) fltDialog.ShowDialog(this); } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; } this.ApplyPostFilters(); - SaveConfigsTabs(); + this.SaveConfigsTabs(); } protected override bool ProcessDialogKey(Keys keyData) { - //TextBox1でEnterを押してもビープ音が鳴らないようにする + // TextBox1でEnterを押してもビープ音が鳴らないようにする if ((keyData & Keys.KeyCode) == Keys.Enter) { - if (StatusText.Focused) + if (this.StatusText.Focused) { - var _NewLine = false; - var _Post = false; + var newLine = false; + var post = false; - if (SettingManager.Common.PostCtrlEnter) //Ctrl+Enter投稿時 + if (this.settings.Common.PostCtrlEnter) // Ctrl+Enter投稿時 { - if (StatusText.Multiline) + if (this.StatusText.Multiline) { - if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) _NewLine = true; + if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true; - if ((keyData & Keys.Control) == Keys.Control) _Post = true; + if ((keyData & Keys.Control) == Keys.Control) post = true; } else { - if (((keyData & Keys.Control) == Keys.Control)) _Post = true; + if ((keyData & Keys.Control) == Keys.Control) post = true; } - } - else if (SettingManager.Common.PostShiftEnter) //SHift+Enter投稿時 + else if (this.settings.Common.PostShiftEnter) // SHift+Enter投稿時 { - if (StatusText.Multiline) + if (this.StatusText.Multiline) { - if ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) != Keys.Shift) _NewLine = true; + if ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) != Keys.Shift) newLine = true; - if ((keyData & Keys.Shift) == Keys.Shift) _Post = true; + if ((keyData & Keys.Shift) == Keys.Shift) post = true; } else { - if (((keyData & Keys.Shift) == Keys.Shift)) _Post = true; + if ((keyData & Keys.Shift) == Keys.Shift) post = true; } - } - else //Enter投稿時 + else // Enter投稿時 { - if (StatusText.Multiline) + if (this.StatusText.Multiline) { - if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) _NewLine = true; + if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true; if (((keyData & Keys.Control) != Keys.Control && (keyData & Keys.Shift) != Keys.Shift) || - ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) == Keys.Shift)) _Post = true; + ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) == Keys.Shift)) post = true; } else { if (((keyData & Keys.Shift) == Keys.Shift) || (((keyData & Keys.Control) != Keys.Control) && - ((keyData & Keys.Shift) != Keys.Shift))) _Post = true; + ((keyData & Keys.Shift) != Keys.Shift))) post = true; } } - if (_NewLine) + if (newLine) { - var pos1 = StatusText.SelectionStart; - if (StatusText.SelectionLength > 0) + var pos1 = this.StatusText.SelectionStart; + if (this.StatusText.SelectionLength > 0) { - StatusText.Text = StatusText.Text.Remove(pos1, StatusText.SelectionLength); //選択状態文字列削除 + this.StatusText.Text = this.StatusText.Text.Remove(pos1, this.StatusText.SelectionLength); // 選択状態文字列削除 } - StatusText.Text = StatusText.Text.Insert(pos1, Environment.NewLine); //改行挿入 - StatusText.SelectionStart = pos1 + Environment.NewLine.Length; //カーソルを改行の次の文字へ移動 + this.StatusText.Text = this.StatusText.Text.Insert(pos1, Environment.NewLine); // 改行挿入 + this.StatusText.SelectionStart = pos1 + Environment.NewLine.Length; // カーソルを改行の次の文字へ移動 return true; } - else if (_Post) + else if (post) { - PostButton_Click(this.PostButton, EventArgs.Empty); + this.PostButton_Click(this.PostButton, EventArgs.Empty); return true; } } @@ -8004,7 +6573,7 @@ protected override bool ProcessDialogKey(Keys keyData) } private void ReplyAllStripMenuItem_Click(object sender, EventArgs e) - => this.MakeReplyOrDirectStatus(false, true, true); + => this.MakeReplyText(atAll: true); private void IDRuleMenuItem_Click(object sender, EventArgs e) { @@ -8028,9 +6597,9 @@ private void IDRuleMenuItem_Click(object sender, EventArgs e) { atids.Add("@" + screenName); } - var cnt = AtIdSupl.ItemCount; - AtIdSupl.AddRangeItem(atids.ToArray()); - if (AtIdSupl.ItemCount != cnt) + var cnt = this.AtIdSupl.ItemCount; + this.AtIdSupl.AddRangeItem(atids.ToArray()); + if (this.AtIdSupl.ItemCount != cnt) this.MarkSettingAtIdModified(); } } @@ -8050,8 +6619,8 @@ private void SourceRuleMenuItem_Click(object sender, EventArgs e) public void AddFilterRuleByScreenName(params string[] screenNameArray) { - //タブ選択(or追加) - if (!SelectTab(out var tab)) return; + // タブ選択(or追加) + if (!this.SelectTab(out var tab)) return; bool mv; bool mk; @@ -8080,7 +6649,7 @@ public void AddFilterRuleByScreenName(params string[] screenNameArray) } this.ApplyPostFilters(); - SaveConfigsTabs(); + this.SaveConfigsTabs(); } public void AddFilterRuleBySource(params string[] sourceArray) @@ -8126,8 +6695,8 @@ private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab) { tab = null; - //振り分け先タブ選択 - using (var dialog = new TabsDialog(_statuses)) + // 振り分け先タブ選択 + using (var dialog = new TabsDialog(this.statuses)) { if (dialog.ShowDialog(this) == DialogResult.Cancel) return false; @@ -8135,26 +6704,26 @@ private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab) } this.CurrentTabPage.Focus(); - //新規タブを選択→タブ作成 + // 新規タブを選択→タブ作成 if (tab == null) { string tabName; using (var inputName = new InputTabName()) { - inputName.TabName = _statuses.MakeTabName("MyTab"); + inputName.TabName = this.statuses.MakeTabName("MyTab"); inputName.ShowDialog(); if (inputName.DialogResult == DialogResult.Cancel) return false; tabName = inputName.TabName; } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; if (!MyCommon.IsNullOrEmpty(tabName)) { var newTab = new FilterTabModel(tabName); - if (!_statuses.AddTab(newTab) || !AddNewTab(newTab, startup: false)) + if (!this.statuses.AddTab(newTab) || !this.AddNewTab(newTab, startup: false)) { var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText2, tabName); MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText3, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); - //もう一度タブ名入力 + // もう一度タブ名入力 } else { @@ -8165,7 +6734,7 @@ private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab) } else { - //既存タブを選択 + // 既存タブを選択 return true; } } @@ -8175,18 +6744,18 @@ private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab) private void MoveOrCopy(out bool move, out bool mark) { { - //移動するか? - var _tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText4, Environment.NewLine); - if (MessageBox.Show(_tmp, Properties.Resources.IDRuleMenuItem_ClickText5, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + // 移動するか? + var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText4, Environment.NewLine); + if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText5, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) move = false; else move = true; } if (!move) { - //マークするか? - var _tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText6, Environment.NewLine); - if (MessageBox.Show(_tmp, Properties.Resources.IDRuleMenuItem_ClickText7, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + // マークするか? + var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText6, Environment.NewLine); + if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText7, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) mark = true; else mark = false; @@ -8205,10 +6774,10 @@ private void CopyURLMenuItem_Click(object sender, EventArgs e) private void SelectAllMenuItem_Click(object sender, EventArgs e) { - if (StatusText.Focused) + if (this.StatusText.Focused) { // 発言欄でのCtrl+A - StatusText.SelectAll(); + this.StatusText.SelectAll(); } else { @@ -8219,7 +6788,7 @@ private void SelectAllMenuItem_Click(object sender, EventArgs e) private void MoveMiddle() { - ListViewItem _item; + ListViewItem item; int idx1; int idx2; @@ -8228,17 +6797,17 @@ private void MoveMiddle() var idx = listView.SelectedIndices[0]; - _item = listView.GetItemAt(0, 25); - if (_item == null) + item = listView.GetItemAt(0, 25); + if (item == null) idx1 = 0; else - idx1 = _item.Index; + idx1 = item.Index; - _item = listView.GetItemAt(0, listView.ClientSize.Height - 1); - if (_item == null) + item = listView.GetItemAt(0, listView.ClientSize.Height - 1); + if (item == null) idx2 = listView.VirtualListSize - 1; else - idx2 = _item.Index; + idx2 = item.Index; idx -= Math.Abs(idx1 - idx2) / 2; if (idx < 0) idx = 0; @@ -8284,17 +6853,17 @@ private async void OpenURLMenuItem_Click(object sender, EventArgs e) { // ツイートに含まれる URL が複数ある場合 // => OpenURL を表示しユーザーが選択したリンクを開く - this.UrlDialog.ClearUrl(); + this.urlDialog.ClearUrl(); foreach (var link in links) - this.UrlDialog.AddUrl(link); + this.urlDialog.AddUrl(link); - if (this.UrlDialog.ShowDialog(this) != DialogResult.OK) + if (this.urlDialog.ShowDialog(this) != DialogResult.OK) return; - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; - selectedUrl = this.UrlDialog.SelectedUrl; + selectedUrl = this.urlDialog.SelectedUrl; // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく isReverseSettings = MyCommon.IsKeyDown(Keys.Control); @@ -8305,8 +6874,8 @@ private async void OpenURLMenuItem_Click(object sender, EventArgs e) private void ClearTabMenuItem_Click(object sender, EventArgs e) { - if (MyCommon.IsNullOrEmpty(_rclickTabName)) return; - ClearTab(_rclickTabName, true); + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return; + this.ClearTab(this.rclickTabName, true); } private void ClearTab(string tabName, bool showWarning) @@ -8320,74 +6889,71 @@ private void ClearTab(string tabName, bool showWarning) } } - _statuses.ClearTabIds(tabName); + this.statuses.ClearTabIds(tabName); if (this.CurrentTabName == tabName) { - _anchorPost = null; - _anchorFlag = false; - this.PurgeListViewItemCache(); + this.CurrentTab.ClearAnchor(); + this.listCache?.PurgeCache(); + this.listCache?.UpdateListSize(); } - var tabIndex = this._statuses.Tabs.IndexOf(tabName); + var tabIndex = this.statuses.Tabs.IndexOf(tabName); var tabPage = this.ListTab.TabPages[tabIndex]; tabPage.ImageIndex = -1; - var listView = (DetailsListView)tabPage.Tag; - listView.VirtualListSize = 0; - - if (!SettingManager.Common.TabIconDisp) ListTab.Refresh(); + if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh(); - SetMainWindowTitle(); - SetStatusLabelUrl(); + this.SetMainWindowTitle(); + this.SetStatusLabelUrl(); } private static long followers = 0; private void SetMainWindowTitle() { - //メインウインドウタイトルの書き換え + // メインウインドウタイトルの書き換え var ttl = new StringBuilder(256); var ur = 0; var al = 0; - if (SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.None && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Post && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver && - SettingManager.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus) + if (this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.None && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Post && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver && + this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus) { - foreach (var tab in _statuses.Tabs) + foreach (var tab in this.statuses.Tabs) { ur += tab.UnreadCount; al += tab.AllCount; } } - if (SettingManager.Common.DispUsername) ttl.Append(tw.Username).Append(" - "); + if (this.settings.Common.DispUsername) ttl.Append(this.tw.Username).Append(" - "); ttl.Append(ApplicationSettings.ApplicationName); ttl.Append(" "); - switch (SettingManager.Common.DispLatestPost) + switch (this.settings.Common.DispLatestPost) { case MyCommon.DispTitleEnum.Ver: ttl.Append("Ver:").Append(MyCommon.GetReadableVersion()); break; case MyCommon.DispTitleEnum.Post: - if (_history != null && _history.Count > 1) - ttl.Append(_history[_history.Count - 2].status.Replace("\r\n", " ")); + if (this.history != null && this.history.Count > 1) + ttl.Append(this.history[this.history.Count - 2].Status.Replace("\r\n", " ")); break; case MyCommon.DispTitleEnum.UnreadRepCount: - ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText1, _statuses.MentionTab.UnreadCount + _statuses.DirectMessageTab.UnreadCount); + ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText1, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount); break; case MyCommon.DispTitleEnum.UnreadAllCount: ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText2, ur); break; case MyCommon.DispTitleEnum.UnreadAllRepCount: - ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText3, ur, _statuses.MentionTab.UnreadCount + _statuses.DirectMessageTab.UnreadCount); + ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText3, ur, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount); break; case MyCommon.DispTitleEnum.UnreadCountAllCount: ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText4, ur, al); break; case MyCommon.DispTitleEnum.OwnStatus: - if (followers == 0 && tw.FollowersCount > 0) followers = tw.FollowersCount; - ttl.AppendFormat(Properties.Resources.OwnStatusTitle, tw.StatusesCount, tw.FriendsCount, tw.FollowersCount, tw.FollowersCount - followers); + if (followers == 0 && this.tw.FollowersCount > 0) followers = this.tw.FollowersCount; + ttl.AppendFormat(Properties.Resources.OwnStatusTitle, this.tw.StatusesCount, this.tw.FriendsCount, this.tw.FollowersCount, this.tw.FollowersCount - followers); break; } @@ -8397,17 +6963,17 @@ private void SetMainWindowTitle() } catch (AccessViolationException) { - //原因不明。ポスト内容に依存か?たまーに発生するが再現せず。 + // 原因不明。ポスト内容に依存か?たまーに発生するが再現せず。 } } private string GetStatusLabelText() { - //ステータス欄にカウント表示 - //タブ未読数/タブ発言数 全未読数/総発言数 (未読@+未読DM数) - if (_statuses == null) return ""; - var tbRep = _statuses.MentionTab; - var tbDm = _statuses.DirectMessageTab; + // ステータス欄にカウント表示 + // タブ未読数/タブ発言数 全未読数/総発言数 (未読@+未読DM数) + if (this.statuses == null) return ""; + var tbRep = this.statuses.MentionTab; + var tbDm = this.statuses.DirectMessageTab; if (tbRep == null || tbDm == null) return ""; var urat = tbRep.UnreadCount + tbDm.UnreadCount; var ur = 0; @@ -8417,7 +6983,7 @@ private string GetStatusLabelText() var slbl = new StringBuilder(256); try { - foreach (var tab in _statuses.Tabs) + foreach (var tab in this.statuses.Tabs) { ur += tab.UnreadCount; al += tab.AllCount; @@ -8433,19 +6999,19 @@ private string GetStatusLabelText() return ""; } - UnreadCounter = ur; - UnreadAtCounter = urat; + this.unreadCounter = ur; + this.unreadAtCounter = urat; - var homeTab = this._statuses.HomeTab; + var homeTab = this.statuses.HomeTab; - slbl.AppendFormat(Properties.Resources.SetStatusLabelText1, tur, tal, ur, al, urat, _postTimestamps.Count, _favTimestamps.Count, homeTab.TweetsPerHour); - if (SettingManager.Common.TimelinePeriod == 0) + slbl.AppendFormat(Properties.Resources.SetStatusLabelText1, tur, tal, ur, al, urat, this.postTimestamps.Count, this.favTimestamps.Count, homeTab.TweetsPerHour); + if (this.settings.Common.TimelinePeriod == 0) { slbl.Append(Properties.Resources.SetStatusLabelText2); } else { - slbl.Append(SettingManager.Common.TimelinePeriod + Properties.Resources.SetStatusLabelText3); + slbl.Append(this.settings.Common.TimelinePeriod + Properties.Resources.SetStatusLabelText3); } return slbl.ToString(); } @@ -8461,7 +7027,7 @@ private async void TwitterApiStatus_AccessLimitUpdated(object sender, EventArgs else { var endpointName = ((TwitterApiStatus.AccessLimitUpdatedEventArgs)e).EndpointName; - SetApiStatusLabel(endpointName); + this.SetApiStatusLabel(endpointName); } } catch (ObjectDisposedException) @@ -8483,7 +7049,7 @@ private void SetApiStatusLabel(string? endpointName = null) // 表示中のタブに応じて更新 endpointName = tabType switch { - MyCommon.TabUsageType.Home => "/statuses/home_timeline", + MyCommon.TabUsageType.Home => GetTimelineRequest.EndpointName, MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline", MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline", MyCommon.TabUsageType.Favorites => "/favorites/list", @@ -8499,18 +7065,26 @@ private void SetApiStatusLabel(string? endpointName = null) else { // 表示中のタブに関連する endpoint であれば更新 - var update = endpointName switch - { - "/statuses/home_timeline" => tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined, - "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions, - "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites, - "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage, - "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline, - "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists, - "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch, - "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related, - _ => false, - }; + bool update; + if (endpointName == GetTimelineRequest.EndpointName) + { + update = tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined; + } + else + { + update = endpointName switch + { + "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions, + "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites, + "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage, + "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline, + "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists, + "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch, + "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related, + _ => false, + }; + } + if (update) { this.toolStripApiGauge.ApiEndpoint = endpointName; @@ -8531,57 +7105,57 @@ private void SetNotifyIconText() // タスクトレイアイコンのツールチップテキスト書き換え // Tween [未読/@] ur.Remove(0, ur.Length); - if (SettingManager.Common.DispUsername) + if (this.settings.Common.DispUsername) { - ur.Append(tw.Username); + ur.Append(this.tw.Username); ur.Append(" - "); } ur.Append(ApplicationSettings.ApplicationName); #if DEBUG ur.Append("(Debug Build)"); #endif - if (UnreadCounter != -1 && UnreadAtCounter != -1) + if (this.unreadCounter != -1 && this.unreadAtCounter != -1) { ur.Append(" ["); - ur.Append(UnreadCounter); + ur.Append(this.unreadCounter); ur.Append("/@"); - ur.Append(UnreadAtCounter); + ur.Append(this.unreadAtCounter); ur.Append("]"); } - NotifyIcon1.Text = ur.ToString(); + this.NotifyIcon1.Text = ur.ToString(); } - internal void CheckReplyTo(string StatusText) + internal void CheckReplyTo(string statusText) { MatchCollection m; - //ハッシュタグの保存 - m = Regex.Matches(StatusText, Twitter.HASHTAG, RegexOptions.IgnoreCase); + // ハッシュタグの保存 + m = Regex.Matches(statusText, Twitter.Hashtag, RegexOptions.IgnoreCase); var hstr = ""; foreach (Match hm in m) { if (!hstr.Contains("#" + hm.Result("$3") + " ")) { hstr += "#" + hm.Result("$3") + " "; - HashSupl.AddItem("#" + hm.Result("$3")); + this.HashSupl.AddItem("#" + hm.Result("$3")); } } - if (!MyCommon.IsNullOrEmpty(HashMgr.UseHash) && !hstr.Contains(HashMgr.UseHash + " ")) + if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && !hstr.Contains(this.HashMgr.UseHash + " ")) { - hstr += HashMgr.UseHash; + hstr += this.HashMgr.UseHash; } - if (!MyCommon.IsNullOrEmpty(hstr)) HashMgr.AddHashToHistory(hstr.Trim(), false); + if (!MyCommon.IsNullOrEmpty(hstr)) this.HashMgr.AddHashToHistory(hstr.Trim(), false); // 本当にリプライ先指定すべきかどうかの判定 - m = Regex.Matches(StatusText, "(^|[ -/:-@[-^`{-~])(?@[a-zA-Z0-9_]+)"); + m = Regex.Matches(statusText, "(^|[ -/:-@[-^`{-~])(?@[a-zA-Z0-9_]+)"); - if (SettingManager.Common.UseAtIdSupplement) + if (this.settings.Common.UseAtIdSupplement) { - var bCnt = AtIdSupl.ItemCount; + var bCnt = this.AtIdSupl.ItemCount; foreach (Match mid in m) { - AtIdSupl.AddItem(mid.Result("${id}")); + this.AtIdSupl.AddItem(mid.Result("${id}")); } - if (bCnt != AtIdSupl.ItemCount) + if (bCnt != this.AtIdSupl.ItemCount) this.MarkSettingAtIdModified(); } @@ -8598,15 +7172,15 @@ internal void CheckReplyTo(string StatusText) if (m != null) { var inReplyToScreenName = this.inReplyTo.Value.ScreenName; - if (StatusText.StartsWith("@", StringComparison.Ordinal)) + if (statusText.StartsWith("@", StringComparison.Ordinal)) { - if (StatusText.StartsWith("@" + inReplyToScreenName, StringComparison.Ordinal)) return; + if (statusText.StartsWith("@" + inReplyToScreenName, StringComparison.Ordinal)) return; } else { foreach (Match mid in m) { - if (StatusText.Contains("RT " + mid.Result("${id}") + ":") && mid.Result("${id}") == "@" + inReplyToScreenName) return; + if (statusText.Contains("RT " + mid.Result("${id}") + ":") && mid.Result("${id}") == "@" + inReplyToScreenName) return; } } } @@ -8616,46 +7190,46 @@ internal void CheckReplyTo(string StatusText) private void TweenMain_Resize(object sender, EventArgs e) { - if (!_initialLayout && SettingManager.Common.MinimizeToTray && WindowState == FormWindowState.Minimized) + if (!this.initialLayout && this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized) { this.Visible = false; } - if (_initialLayout && SettingManager.Local != null && this.WindowState == FormWindowState.Normal && this.Visible) + if (this.initialLayout && this.settings.Local != null && this.WindowState == FormWindowState.Normal && this.Visible) { // 現在の DPI と設定保存時の DPI との比を取得する - var configScaleFactor = SettingManager.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); + var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); - this.ClientSize = ScaleBy(configScaleFactor, SettingManager.Local.FormSize); + this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize); // Splitterの位置設定 - var splitterDistance = ScaleBy(configScaleFactor.Height, SettingManager.Local.SplitterDistance); + var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance); if (splitterDistance > this.SplitContainer1.Panel1MinSize && splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth) { this.SplitContainer1.SplitterDistance = splitterDistance; } - //発言欄複数行 - StatusText.Multiline = SettingManager.Local.StatusMultiline; - if (StatusText.Multiline) + // 発言欄複数行 + this.StatusText.Multiline = this.settings.Local.StatusMultiline; + if (this.StatusText.Multiline) { - var statusTextHeight = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight); - var dis = SplitContainer2.Height - statusTextHeight - SplitContainer2.SplitterWidth; - if (dis > SplitContainer2.Panel1MinSize && dis < SplitContainer2.Height - SplitContainer2.Panel2MinSize - SplitContainer2.SplitterWidth) + var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight); + var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; + if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth) { - SplitContainer2.SplitterDistance = SplitContainer2.Height - statusTextHeight - SplitContainer2.SplitterWidth; + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; } - StatusText.Height = statusTextHeight; + this.StatusText.Height = statusTextHeight; } else { - if (SplitContainer2.Height - SplitContainer2.Panel2MinSize - SplitContainer2.SplitterWidth > 0) + if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0) { - SplitContainer2.SplitterDistance = SplitContainer2.Height - SplitContainer2.Panel2MinSize - SplitContainer2.SplitterWidth; + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth; } } - var previewDistance = ScaleBy(configScaleFactor.Width, SettingManager.Local.PreviewDistance); + var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance); if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth) { this.SplitContainer3.SplitterDistance = previewDistance; @@ -8664,32 +7238,32 @@ private void TweenMain_Resize(object sender, EventArgs e) // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない this.SplitContainer3.Panel2Collapsed = true; - _initialLayout = false; + this.initialLayout = false; } if (this.WindowState != FormWindowState.Minimized) { - _formWindowState = this.WindowState; + this.formWindowState = this.WindowState; } } private void PlaySoundMenuItem_CheckedChanged(object sender, EventArgs e) { - PlaySoundMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; - this.PlaySoundFileMenuItem.Checked = PlaySoundMenuItem.Checked; - if (PlaySoundMenuItem.Checked) + this.PlaySoundMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; + this.PlaySoundFileMenuItem.Checked = this.PlaySoundMenuItem.Checked; + if (this.PlaySoundMenuItem.Checked) { - SettingManager.Common.PlaySound = true; + this.settings.Common.PlaySound = true; } else { - SettingManager.Common.PlaySound = false; + this.settings.Common.PlaySound = false; } this.MarkSettingCommonModified(); } private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e) { - if (this._initialLayout) + if (this.initialLayout) return; int splitterDistance; @@ -8700,7 +7274,7 @@ private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e) break; case FormWindowState.Maximized: // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する - var normalContainerHeight = this._mySize.Height - this.ToolStripContainer1.TopToolStripPanel.Height - this.ToolStripContainer1.BottomToolStripPanel.Height; + var normalContainerHeight = this.mySize.Height - this.ToolStripContainer1.TopToolStripPanel.Height - this.ToolStripContainer1.BottomToolStripPanel.Height; splitterDistance = this.SplitContainer1.SplitterDistance - (this.SplitContainer1.Height - normalContainerHeight); splitterDistance = Math.Min(splitterDistance, normalContainerHeight - this.SplitContainer1.SplitterWidth - this.SplitContainer1.Panel2MinSize); break; @@ -8708,11 +7282,11 @@ private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e) return; } - this._mySpDis = splitterDistance; + this.mySpDis = splitterDistance; this.MarkSettingLocalModified(); } - private async Task doRepliedStatusOpen() + private async Task DoRepliedStatusOpen() { var currentPost = this.CurrentPost; if (this.ExistCurrentPost && currentPost != null && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null) @@ -8722,13 +7296,13 @@ private async Task doRepliedStatusOpen() await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value)); return; } - if (this._statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost)) + if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost)) { MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi); } else { - foreach (var tb in _statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch)) + foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch)) { if (tb == null || !tb.Contains(currentPost.InReplyToStatusId.Value)) break; repPost = tb.Posts[currentPost.InReplyToStatusId.Value]; @@ -8741,18 +7315,18 @@ private async Task doRepliedStatusOpen() } private async void RepliedStatusOpenMenuItem_Click(object sender, EventArgs e) - => await this.doRepliedStatusOpen(); + => await this.DoRepliedStatusOpen(); private void SplitContainer2_Panel2_Resize(object sender, EventArgs e) { - if (this._initialLayout) + if (this.initialLayout) return; // SettingLocal の反映が完了するまで multiline の判定を行わない var multiline = this.SplitContainer2.Panel2.Height > this.SplitContainer2.Panel2MinSize + 2; if (multiline != this.StatusText.Multiline) { this.StatusText.Multiline = multiline; - SettingManager.Local.StatusMultiline = multiline; + this.settings.Local.StatusMultiline = multiline; this.MarkSettingLocalModified(); } } @@ -8764,81 +7338,81 @@ private void StatusText_MultilineChanged(object sender, EventArgs e) else this.StatusText.ScrollBars = ScrollBars.None; - if (!this._initialLayout) + if (!this.initialLayout) this.MarkSettingLocalModified(); } private void MultiLineMenuItem_Click(object sender, EventArgs e) { - //発言欄複数行 + // 発言欄複数行 var menuItemChecked = ((ToolStripMenuItem)sender).Checked; - StatusText.Multiline = menuItemChecked; - SettingManager.Local.StatusMultiline = menuItemChecked; + this.StatusText.Multiline = menuItemChecked; + this.settings.Local.StatusMultiline = menuItemChecked; if (menuItemChecked) { - if (SplitContainer2.Height - _mySpDis2 - SplitContainer2.SplitterWidth < 0) - SplitContainer2.SplitterDistance = 0; + if (this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth < 0) + this.SplitContainer2.SplitterDistance = 0; else - SplitContainer2.SplitterDistance = SplitContainer2.Height - _mySpDis2 - SplitContainer2.SplitterWidth; + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth; } else { - SplitContainer2.SplitterDistance = SplitContainer2.Height - SplitContainer2.Panel2MinSize - SplitContainer2.SplitterWidth; + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth; } this.MarkSettingLocalModified(); } - private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) + private async Task UrlConvertAsync(MyCommon.UrlConverter converterType) { - if (Converter_Type == MyCommon.UrlConverter.Bitly || Converter_Type == MyCommon.UrlConverter.Jmp) + if (converterType == MyCommon.UrlConverter.Bitly || converterType == MyCommon.UrlConverter.Jmp) { // OAuth2 アクセストークンまたは API キー (旧方式) のいずれも設定されていなければ短縮しない - if (MyCommon.IsNullOrEmpty(SettingManager.Common.BitlyAccessToken) && - (MyCommon.IsNullOrEmpty(SettingManager.Common.BilyUser) || MyCommon.IsNullOrEmpty(SettingManager.Common.BitlyPwd))) + if (MyCommon.IsNullOrEmpty(this.settings.Common.BitlyAccessToken) && + (MyCommon.IsNullOrEmpty(this.settings.Common.BilyUser) || MyCommon.IsNullOrEmpty(this.settings.Common.BitlyPwd))) { MessageBox.Show(this, Properties.Resources.UrlConvert_BitlyAuthRequired, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } } - //Converter_Type=Nicomsの場合は、nicovideoのみ短縮する - //参考資料 RFC3986 Uniform Resource Identifier (URI): Generic Syntax - //Appendix A. Collected ABNF for URI - //http://www.ietf.org/rfc/rfc3986.txt + // Converter_Type=Nicomsの場合は、nicovideoのみ短縮する + // 参考資料 RFC3986 Uniform Resource Identifier (URI): Generic Syntax + // Appendix A. Collected ABNF for URI + // http://www.ietf.org/rfc/rfc3986.txt const string nico = @"^https?://[a-z]+\.(nicovideo|niconicommons|nicolive)\.jp/[a-z]+/[a-z0-9]+$"; string result; - if (StatusText.SelectionLength > 0) + if (this.StatusText.SelectionLength > 0) { - var tmp = StatusText.SelectedText; + var tmp = this.StatusText.SelectedText; // httpから始まらない場合、ExcludeStringで指定された文字列で始まる場合は対象としない if (tmp.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { // 文字列が選択されている場合はその文字列について処理 - //nico.ms使用、nicovideoにマッチしたら変換 - if (SettingManager.Common.Nicoms && Regex.IsMatch(tmp, nico)) + // nico.ms使用、nicovideoにマッチしたら変換 + if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico)) { - result = nicoms.Shorten(tmp); + result = Nicoms.Shorten(tmp); } - else if (Converter_Type != MyCommon.UrlConverter.Nicoms) + else if (converterType != MyCommon.UrlConverter.Nicoms) { // 短縮URL変換 try { var srcUri = new Uri(tmp); - var resultUri = await ShortUrl.Instance.ShortenUrlAsync(Converter_Type, srcUri); + var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri); result = resultUri.AbsoluteUri; } catch (WebApiException e) { - this.StatusLabel.Text = Converter_Type + ":" + e.Message; + this.StatusLabel.Text = converterType + ":" + e.Message; return false; } catch (UriFormatException e) { - this.StatusLabel.Text = Converter_Type + ":" + e.Message; + this.StatusLabel.Text = converterType + ":" + e.Message; return false; } } @@ -8849,27 +7423,28 @@ private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) if (!MyCommon.IsNullOrEmpty(result)) { - var undotmp = new urlUndo(); - // 短縮 URL が生成されるまでの間に投稿欄から元の URL が削除されていたら中断する var origUrlIndex = this.StatusText.Text.IndexOf(tmp, StringComparison.Ordinal); if (origUrlIndex == -1) return false; - StatusText.Select(origUrlIndex, tmp.Length); - StatusText.SelectedText = result; + this.StatusText.Select(origUrlIndex, tmp.Length); + this.StatusText.SelectedText = result; - //undoバッファにセット - undotmp.Before = tmp; - undotmp.After = result; + // undoバッファにセット + var undo = new UrlUndo + { + Before = tmp, + After = result, + }; - if (urlUndoBuffer == null) + if (this.urlUndoBuffer == null) { - urlUndoBuffer = new List(); - UrlUndoToolStripMenuItem.Enabled = true; + this.urlUndoBuffer = new List(); + this.UrlUndoToolStripMenuItem.Enabled = true; } - urlUndoBuffer.Add(undotmp); + this.urlUndoBuffer.Add(undo); } } } @@ -8881,30 +7456,29 @@ private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) @"(?/[a-z0-9!*//();:&=+$/%#\-_.,~@]*[a-z0-9)=#/]?)?" + @"(?\?[a-z0-9!*//();:&=+$/%#\-_.,~@?]*[a-z0-9_&=#/])?)"; // 正規表現にマッチしたURL文字列をtinyurl化 - foreach (Match mt in Regex.Matches(StatusText.Text, url, RegexOptions.IgnoreCase)) + foreach (Match mt in Regex.Matches(this.StatusText.Text, url, RegexOptions.IgnoreCase)) { - if (StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal) == -1) + if (this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal) == -1) continue; var tmp = mt.Result("${url}"); if (tmp.StartsWith("w", StringComparison.OrdinalIgnoreCase)) tmp = "http://" + tmp; - var undotmp = new urlUndo(); - //選んだURLを選択(?) - StatusText.Select(StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal), mt.Result("${url}").Length); + // 選んだURLを選択(?) + this.StatusText.Select(this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal), mt.Result("${url}").Length); - //nico.ms使用、nicovideoにマッチしたら変換 - if (SettingManager.Common.Nicoms && Regex.IsMatch(tmp, nico)) + // nico.ms使用、nicovideoにマッチしたら変換 + if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico)) { - result = nicoms.Shorten(tmp); + result = Nicoms.Shorten(tmp); } - else if (Converter_Type != MyCommon.UrlConverter.Nicoms) + else if (converterType != MyCommon.UrlConverter.Nicoms) { // 短縮URL変換 try { var srcUri = new Uri(tmp); - var resultUri = await ShortUrl.Instance.ShortenUrlAsync(Converter_Type, srcUri); + var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri); result = resultUri.AbsoluteUri; } catch (HttpRequestException e) @@ -8913,17 +7487,17 @@ private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) // のように長いので「:」が含まれていればそれ以降のみを抽出する var message = e.Message.Split(new[] { ':' }, count: 2).Last(); - this.StatusLabel.Text = Converter_Type + ":" + message; + this.StatusLabel.Text = converterType + ":" + message; continue; } catch (WebApiException e) { - this.StatusLabel.Text = Converter_Type + ":" + e.Message; + this.StatusLabel.Text = converterType + ":" + e.Message; continue; } catch (UriFormatException e) { - this.StatusLabel.Text = Converter_Type + ":" + e.Message; + this.StatusLabel.Text = converterType + ":" + e.Message; continue; } } @@ -8939,19 +7513,22 @@ private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) if (origUrlIndex == -1) return false; - StatusText.Select(origUrlIndex, mt.Result("${url}").Length); - StatusText.SelectedText = result; - //undoバッファにセット - undotmp.Before = mt.Result("${url}"); - undotmp.After = result; + this.StatusText.Select(origUrlIndex, mt.Result("${url}").Length); + this.StatusText.SelectedText = result; + // undoバッファにセット + var undo = new UrlUndo + { + Before = mt.Result("${url}"), + After = result, + }; - if (urlUndoBuffer == null) + if (this.urlUndoBuffer == null) { - urlUndoBuffer = new List(); - UrlUndoToolStripMenuItem.Enabled = true; + this.urlUndoBuffer = new List(); + this.UrlUndoToolStripMenuItem.Enabled = true; } - urlUndoBuffer.Add(undotmp); + this.urlUndoBuffer.Add(undo); } } } @@ -8959,20 +7536,20 @@ private async Task UrlConvertAsync(MyCommon.UrlConverter Converter_Type) return true; } - private void doUrlUndo() + private void DoUrlUndo() { - if (urlUndoBuffer != null) + if (this.urlUndoBuffer != null) { - var tmp = StatusText.Text; - foreach (var data in urlUndoBuffer) + var tmp = this.StatusText.Text; + foreach (var data in this.urlUndoBuffer) { tmp = tmp.Replace(data.After, data.Before); } - StatusText.Text = tmp; - urlUndoBuffer = null; - UrlUndoToolStripMenuItem.Enabled = false; - StatusText.SelectionStart = 0; - StatusText.SelectionLength = 0; + this.StatusText.Text = tmp; + this.urlUndoBuffer = null; + this.UrlUndoToolStripMenuItem.Enabled = false; + this.StatusText.SelectionStart = 0; + this.StatusText.SelectionLength = 0; } } @@ -8987,7 +7564,7 @@ private async void UxnuMenuItem_Click(object sender, EventArgs e) private async void UrlConvertAutoToolStripMenuItem_Click(object sender, EventArgs e) { - if (!await UrlConvertAsync(SettingManager.Common.AutoShortUrlFirst)) + if (!await this.UrlConvertAsync(this.settings.Common.AutoShortUrlFirst)) { var rnd = new Random(); @@ -8997,35 +7574,35 @@ private async void UrlConvertAutoToolStripMenuItem_Click(object sender, EventArg { svc = (MyCommon.UrlConverter)rnd.Next(System.Enum.GetNames(typeof(MyCommon.UrlConverter)).Length); } - while (svc == SettingManager.Common.AutoShortUrlFirst || svc == MyCommon.UrlConverter.Nicoms || svc == MyCommon.UrlConverter.Unu); - await UrlConvertAsync(svc); + while (svc == this.settings.Common.AutoShortUrlFirst || svc == MyCommon.UrlConverter.Nicoms || svc == MyCommon.UrlConverter.Unu); + await this.UrlConvertAsync(svc); } } private void UrlUndoToolStripMenuItem_Click(object sender, EventArgs e) - => this.doUrlUndo(); + => this.DoUrlUndo(); private void NewPostPopMenuItem_CheckStateChanged(object sender, EventArgs e) { this.NotifyFileMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; this.NewPostPopMenuItem.Checked = this.NotifyFileMenuItem.Checked; - SettingManager.Common.NewAllPop = NewPostPopMenuItem.Checked; + this.settings.Common.NewAllPop = this.NewPostPopMenuItem.Checked; this.MarkSettingCommonModified(); } private void ListLockMenuItem_CheckStateChanged(object sender, EventArgs e) { - ListLockMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; - this.LockListFileMenuItem.Checked = ListLockMenuItem.Checked; - SettingManager.Common.ListLock = ListLockMenuItem.Checked; + this.ListLockMenuItem.Checked = ((ToolStripMenuItem)sender).Checked; + this.LockListFileMenuItem.Checked = this.ListLockMenuItem.Checked; + this.settings.Common.ListLock = this.ListLockMenuItem.Checked; this.MarkSettingCommonModified(); } private void MenuStrip1_MenuActivate(object sender, EventArgs e) { // フォーカスがメニューに移る (MenuStrip1.Tag フラグを立てる) - MenuStrip1.Tag = new object(); - MenuStrip1.Select(); // StatusText がフォーカスを持っている場合 Leave が発生 + this.MenuStrip1.Tag = new object(); + this.MenuStrip1.Select(); // StatusText がフォーカスを持っている場合 Leave が発生 } private void MenuStrip1_MenuDeactivate(object sender, EventArgs e) @@ -9044,143 +7621,74 @@ private void MenuStrip1_MenuDeactivate(object sender, EventArgs e) ((Control)this.Tag).Select(); } // フォーカスがメニューに遷移したかどうかを表すフラグを降ろす - MenuStrip1.Tag = null; + this.MenuStrip1.Tag = null; } private void MyList_ColumnReordered(object sender, ColumnReorderedEventArgs e) { - var lst = (DetailsListView)sender; - if (SettingManager.Local == null) return; - - if (_iconCol) + if (this.Use2ColumnsMode) { - SettingManager.Local.Width1 = lst.Columns[0].Width; - SettingManager.Local.Width3 = lst.Columns[1].Width; + e.Cancel = true; + return; } - else - { - var darr = new int[lst.Columns.Count]; - for (var i = 0; i < lst.Columns.Count; i++) - { - darr[lst.Columns[i].DisplayIndex] = i; - } - MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex); - for (var i = 0; i < lst.Columns.Count; i++) - { - switch (darr[i]) - { - case 0: - SettingManager.Local.DisplayIndex1 = i; - break; - case 1: - SettingManager.Local.DisplayIndex2 = i; - break; - case 2: - SettingManager.Local.DisplayIndex3 = i; - break; - case 3: - SettingManager.Local.DisplayIndex4 = i; - break; - case 4: - SettingManager.Local.DisplayIndex5 = i; - break; - case 5: - SettingManager.Local.DisplayIndex6 = i; - break; - case 6: - SettingManager.Local.DisplayIndex7 = i; - break; - case 7: - SettingManager.Local.DisplayIndex8 = i; - break; - } - } - SettingManager.Local.Width1 = lst.Columns[0].Width; - SettingManager.Local.Width2 = lst.Columns[1].Width; - SettingManager.Local.Width3 = lst.Columns[2].Width; - SettingManager.Local.Width4 = lst.Columns[3].Width; - SettingManager.Local.Width5 = lst.Columns[4].Width; - SettingManager.Local.Width6 = lst.Columns[5].Width; - SettingManager.Local.Width7 = lst.Columns[6].Width; - SettingManager.Local.Width8 = lst.Columns[7].Width; - } + var lst = (DetailsListView)sender; + var columnsCount = lst.Columns.Count; + + var darr = new int[columnsCount]; + for (var i = 0; i < columnsCount; i++) + darr[lst.Columns[i].DisplayIndex] = i; + + MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex); + + for (var i = 0; i < columnsCount; i++) + this.settings.Local.ColumnsOrder[darr[i]] = i; + this.MarkSettingLocalModified(); - _isColumnChanged = true; + this.isColumnChanged = true; } private void MyList_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { var lst = (DetailsListView)sender; - if (SettingManager.Local == null) return; + if (this.settings.Local == null) return; var modified = false; - if (_iconCol) + if (this.Use2ColumnsMode) { - if (SettingManager.Local.Width1 != lst.Columns[0].Width) + if (this.settings.Local.ColumnsWidth[0] != lst.Columns[0].Width) { - SettingManager.Local.Width1 = lst.Columns[0].Width; + this.settings.Local.ColumnsWidth[0] = lst.Columns[0].Width; modified = true; } - if (SettingManager.Local.Width3 != lst.Columns[1].Width) + if (this.settings.Local.ColumnsWidth[2] != lst.Columns[1].Width) { - SettingManager.Local.Width3 = lst.Columns[1].Width; + this.settings.Local.ColumnsWidth[2] = lst.Columns[1].Width; modified = true; } } else { - if (SettingManager.Local.Width1 != lst.Columns[0].Width) - { - SettingManager.Local.Width1 = lst.Columns[0].Width; - modified = true; - } - if (SettingManager.Local.Width2 != lst.Columns[1].Width) - { - SettingManager.Local.Width2 = lst.Columns[1].Width; - modified = true; - } - if (SettingManager.Local.Width3 != lst.Columns[2].Width) - { - SettingManager.Local.Width3 = lst.Columns[2].Width; - modified = true; - } - if (SettingManager.Local.Width4 != lst.Columns[3].Width) + var columnsCount = lst.Columns.Count; + for (var i = 0; i < columnsCount; i++) { - SettingManager.Local.Width4 = lst.Columns[3].Width; - modified = true; - } - if (SettingManager.Local.Width5 != lst.Columns[4].Width) - { - SettingManager.Local.Width5 = lst.Columns[4].Width; - modified = true; - } - if (SettingManager.Local.Width6 != lst.Columns[5].Width) - { - SettingManager.Local.Width6 = lst.Columns[5].Width; - modified = true; - } - if (SettingManager.Local.Width7 != lst.Columns[6].Width) - { - SettingManager.Local.Width7 = lst.Columns[6].Width; - modified = true; - } - if (SettingManager.Local.Width8 != lst.Columns[7].Width) - { - SettingManager.Local.Width8 = lst.Columns[7].Width; + if (this.settings.Local.ColumnsWidth[i] == lst.Columns[i].Width) + continue; + + this.settings.Local.ColumnsWidth[i] = lst.Columns[i].Width; modified = true; } } if (modified) { this.MarkSettingLocalModified(); - this._isColumnChanged = true; + this.isColumnChanged = true; } } private void SplitContainer2_SplitterMoved(object sender, SplitterEventArgs e) { - if (StatusText.Multiline) _mySpDis2 = StatusText.Height; + if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height; this.MarkSettingLocalModified(); } @@ -9188,9 +7696,9 @@ private void TweenMain_DragDrop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { - if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く + if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く { - SelectMedia_DragDrop(e); + this.SelectMedia_DragDrop(e); } } else if (e.Data.GetDataPresent("UniformResourceLocatorW")) @@ -9217,7 +7725,7 @@ private void TweenMain_DragDrop(object sender, DragEventArgs e) else if (e.Data.GetDataPresent(DataFormats.StringFormat)) { var data = (string)e.Data.GetData(DataFormats.StringFormat, true); - if (data != null) StatusText.Text += data; + if (data != null) this.StatusText.Text += data; } } @@ -9271,9 +7779,9 @@ private void TweenMain_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { - if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く + if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く { - SelectMedia_DragEnter(e); + this.SelectMedia_DragEnter(e); return; } } @@ -9303,7 +7811,7 @@ private void TweenMain_DragOver(object sender, DragEventArgs e) public bool IsNetworkAvailable() { var nw = MyCommon.IsNetworkAvailable(); - _myStatusOnline = nw; + this.myStatusOnline = nw; return nw; } @@ -9335,8 +7843,8 @@ public async Task OpenUriAsync(Uri uri, bool isReverseSettings = false) // ユーザープロフィールURL // フラグが立っている場合は設定と逆の動作をする - if( SettingManager.Common.OpenUserTimeline && !isReverseSettings || - !SettingManager.Common.OpenUserTimeline && isReverseSettings ) + if (this.settings.Common.OpenUserTimeline && !isReverseSettings || + !this.settings.Common.OpenUserTimeline && isReverseSettings) { var userUriMatch = Regex.Match(uriStr, "^https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)$"); if (userUriMatch.Success) @@ -9369,41 +7877,67 @@ private async Task OpenInternalUriAsync(Uri uri) } } - private void ListTabSelect(TabPage _tab) + private void ListTabSelect(TabPage tabPage) { - SetListProperty(); + this.SetListProperty(); + + var previousTabName = this.CurrentTabName; + if (this.listViewState.TryGetValue(previousTabName, out var previousListViewState)) + previousListViewState.Save(this.ListLockMenuItem.Checked); + + this.listCache?.PurgeCache(); - this.PurgeListViewItemCache(); + this.statuses.SelectTab(tabPage.Text); - this._statuses.SelectTab(_tab.Text); + this.InitializeTimelineListView(); + + var tab = this.CurrentTab; + tab.ClearAnchor(); var listView = this.CurrentListView; - _anchorPost = null; - _anchorFlag = false; + var currentListViewState = this.listViewState[tab.TabName]; + currentListViewState.Restore(); - if (_iconCol) + if (this.Use2ColumnsMode) { - listView.Columns[1].Text = ColumnText[2]; + listView.Columns[1].Text = this.columnText[2]; } else { for (var i = 0; i < listView.Columns.Count; i++) { - listView.Columns[i].Text = ColumnText[i]; + listView.Columns[i].Text = this.columnText[i]; } } } + private void InitializeTimelineListView() + { + var listView = this.CurrentListView; + var tab = this.CurrentTab; + + var newCache = new TimelineListViewCache(listView, tab, this.settings.Common); + (this.listCache, var oldCache) = (newCache, this.listCache); + oldCache?.Dispose(); + + var newDrawer = new TimelineListViewDrawer(listView, tab, this.listCache, this.iconCache, this.themeManager) + { + IconSize = this.settings.Common.IconSize, + }; + (this.listDrawer, var oldDrawer) = (newDrawer, this.listDrawer); + oldDrawer?.Dispose(); + } + private void ListTab_Selecting(object sender, TabControlCancelEventArgs e) => this.ListTabSelect(e.TabPage); - private void SelectListItem(DetailsListView LView, int Index) + private void SelectListItem(DetailsListView lView, int index) { - //単一 + // 単一 var bnd = new Rectangle(); var flg = false; - var item = LView.FocusedItem; + var item = lView.FocusedItem; if (item != null) { bnd = item.Bounds; @@ -9412,51 +7946,19 @@ private void SelectListItem(DetailsListView LView, int Index) do { - LView.SelectedIndices.Clear(); + lView.SelectedIndices.Clear(); } - while (LView.SelectedIndices.Count > 0); - item = LView.Items[Index]; + while (lView.SelectedIndices.Count > 0); + item = lView.Items[index]; item.Selected = true; item.Focused = true; - if (flg) LView.Invalidate(bnd); - } - - private void SelectListItem(DetailsListView LView , int[]? Index, int focusedIndex, int selectionMarkIndex) - { - //複数 - var bnd = new Rectangle(); - var flg = false; - var item = LView.FocusedItem; - if (item != null) - { - bnd = item.Bounds; - flg = true; - } - - if (Index != null) - { - LView.SelectItems(Index); - } - if (selectionMarkIndex > -1 && LView.VirtualListSize > selectionMarkIndex) - { - LView.SelectionMark = selectionMarkIndex; - } - if (focusedIndex > -1 && LView.VirtualListSize > focusedIndex) - { - LView.Items[focusedIndex].Focused = true; - } - else if (Index != null && Index.Length != 0) - { - LView.Items[Index.Last()].Focused = true; - } - - if (flg) LView.Invalidate(bnd); + if (flg) lView.Invalidate(bnd); } private async void TweenMain_Shown(object sender, EventArgs e) { - NotifyIcon1.Visible = true; + this.NotifyIcon1.Visible = true; if (this.IsNetworkAvailable()) { @@ -9474,10 +7976,10 @@ private async void TweenMain_Shown(object sender, EventArgs e) this.RefreshTabAsync(), }; - if (SettingManager.Common.StartupFollowers) + if (this.settings.Common.StartupFollowers) loadTasks.Add(this.RefreshFollowerIdsAsync()); - if (SettingManager.Common.GetFav) + if (this.settings.Common.GetFav) loadTasks.Add(this.RefreshTabAsync()); var allTasks = Task.WhenAll(loadTasks); @@ -9492,16 +7994,16 @@ private async void TweenMain_Shown(object sender, EventArgs e) i += 1; if (i > 24) break; // 120秒間初期処理が終了しなかったら強制的に打ち切る - if (MyCommon._endingFlag) + if (MyCommon.EndingFlag) return; } - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; if (ApplicationSettings.VersionInfoUrl != null) { - //バージョンチェック(引数:起動時チェックの場合はtrue・・・チェック結果のメッセージを表示しない) - if (SettingManager.Common.StartupVersion) + // バージョンチェック(引数:起動時チェックの場合はtrue・・・チェック結果のメッセージを表示しない) + if (this.settings.Common.StartupVersion) await this.CheckNewVersion(true); } else @@ -9516,16 +8018,16 @@ private async void TweenMain_Shown(object sender, EventArgs e) if (MyCommon.TwitterApiInfo.AccessLevel == TwitterApiAccessLevel.ReadWrite) { MessageBox.Show(Properties.Resources.ReAuthorizeText); - SettingStripMenuItem_Click(this.SettingStripMenuItem, EventArgs.Empty); + this.SettingStripMenuItem_Click(this.SettingStripMenuItem, EventArgs.Empty); } // 取得失敗の場合は再試行する var reloadTasks = new List(); - if (!tw.GetFollowersSuccess && SettingManager.Common.StartupFollowers) + if (!this.tw.GetFollowersSuccess && this.settings.Common.StartupFollowers) reloadTasks.Add(this.RefreshFollowerIdsAsync()); - if (!tw.GetNoRetweetSuccess) + if (!this.tw.GetNoRetweetSuccess) reloadTasks.Add(this.RefreshNoRetweetIdsAsync()); if (this.tw.Configuration.PhotoSizeLimit == 0) @@ -9534,66 +8036,66 @@ private async void TweenMain_Shown(object sender, EventArgs e) await Task.WhenAll(reloadTasks); } - _initial = false; + this.initial = false; this.timelineScheduler.Enabled = true; } - private async Task doGetFollowersMenu() + private async Task DoGetFollowersMenu() { await this.RefreshFollowerIdsAsync(); this.DispSelectedPost(true); } private async void GetFollowersAllToolStripMenuItem_Click(object sender, EventArgs e) - => await this.doGetFollowersMenu(); + => await this.DoGetFollowersMenu(); private void ReTweetUnofficialStripMenuItem_Click(object sender, EventArgs e) - => this.doReTweetUnofficial(); + => this.DoReTweetUnofficial(); - private async Task doReTweetOfficial(bool isConfirm) + private async Task DoReTweetOfficial(bool isConfirm) { - //公式RT + // 公式RT if (this.ExistCurrentPost) { var selectedPosts = this.CurrentTab.SelectedPosts; - if (selectedPosts.Any(x => !x.CanRetweetBy(this.twitterApi.CurrentUserId))) + if (selectedPosts.Any(x => !x.CanRetweetBy(this.tw.UserId))) { if (selectedPosts.Any(x => x.IsProtect)) MessageBox.Show("Protected."); - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; return; } if (selectedPosts.Length > 15) { MessageBox.Show(Properties.Resources.RetweetLimitText); - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; return; } else if (selectedPosts.Length > 1) { - var QuestionText = Properties.Resources.RetweetQuestion2; - if (_DoFavRetweetFlags) QuestionText = Properties.Resources.FavoriteRetweetQuestionText1; - switch (MessageBox.Show(QuestionText, "Retweet", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question)) + var questionText = Properties.Resources.RetweetQuestion2; + if (this.doFavRetweetFlags) questionText = Properties.Resources.FavoriteRetweetQuestionText1; + switch (MessageBox.Show(questionText, "Retweet", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question)) { case DialogResult.Cancel: case DialogResult.No: - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; return; } } else { - if (!SettingManager.Common.RetweetNoConfirm) + if (!this.settings.Common.RetweetNoConfirm) { - var Questiontext = Properties.Resources.RetweetQuestion1; - if (_DoFavRetweetFlags) Questiontext = Properties.Resources.FavoritesRetweetQuestionText2; - if (isConfirm && MessageBox.Show(Questiontext, "Retweet", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel) + var questiontext = Properties.Resources.RetweetQuestion1; + if (this.doFavRetweetFlags) questiontext = Properties.Resources.FavoritesRetweetQuestionText2; + if (isConfirm && MessageBox.Show(questiontext, "Retweet", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel) { - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; return; } } @@ -9606,16 +8108,16 @@ private async Task doReTweetOfficial(bool isConfirm) } private async void ReTweetStripMenuItem_Click(object sender, EventArgs e) - => await this.doReTweetOfficial(true); + => await this.DoReTweetOfficial(true); private async Task FavoritesRetweetOfficial() { if (!this.ExistCurrentPost) return; - _DoFavRetweetFlags = true; - var retweetTask = this.doReTweetOfficial(true); - if (_DoFavRetweetFlags) + this.doFavRetweetFlags = true; + var retweetTask = this.DoReTweetOfficial(true); + if (this.doFavRetweetFlags) { - _DoFavRetweetFlags = false; + this.doFavRetweetFlags = false; var favoriteTask = this.FavoriteChange(true, false); await Task.WhenAll(retweetTask, favoriteTask); @@ -9631,12 +8133,12 @@ private async Task FavoritesRetweetUnofficial() var post = this.CurrentPost; if (this.ExistCurrentPost && post != null && !post.IsDm) { - _DoFavRetweetFlags = true; + this.doFavRetweetFlags = true; var favoriteTask = this.FavoriteChange(true); - if (!post.IsProtect && _DoFavRetweetFlags) + if (!post.IsProtect && this.doFavRetweetFlags) { - _DoFavRetweetFlags = false; - doReTweetUnofficial(); + this.doFavRetweetFlags = false; + this.DoReTweetUnofficial(); } await favoriteTask; @@ -9686,9 +8188,9 @@ private void DumpPostClassToolStripMenuItem_Click(object sender, EventArgs e) private void MenuItemHelp_DropDownOpening(object sender, EventArgs e) { if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock, Keys.Control, Keys.Shift)) - DebugModeToolStripMenuItem.Visible = true; + this.DebugModeToolStripMenuItem.Visible = true; else - DebugModeToolStripMenuItem.Visible = false; + this.DebugModeToolStripMenuItem.Visible = false; } private void UrlMultibyteSplitMenuItem_CheckedChanged(object sender, EventArgs e) @@ -9698,56 +8200,56 @@ private void PreventSmsCommandMenuItem_CheckedChanged(object sender, EventArgs e => this.preventSmsCommand = ((ToolStripMenuItem)sender).Checked; private void UrlAutoShortenMenuItem_CheckedChanged(object sender, EventArgs e) - => SettingManager.Common.UrlConvertAuto = ((ToolStripMenuItem)sender).Checked; + => this.settings.Common.UrlConvertAuto = ((ToolStripMenuItem)sender).Checked; private void IdeographicSpaceToSpaceMenuItem_Click(object sender, EventArgs e) { - SettingManager.Common.WideSpaceConvert = ((ToolStripMenuItem)sender).Checked; + this.settings.Common.WideSpaceConvert = ((ToolStripMenuItem)sender).Checked; this.MarkSettingCommonModified(); } private void FocusLockMenuItem_CheckedChanged(object sender, EventArgs e) { - SettingManager.Common.FocusLockToStatusText = ((ToolStripMenuItem)sender).Checked; + this.settings.Common.FocusLockToStatusText = ((ToolStripMenuItem)sender).Checked; this.MarkSettingCommonModified(); } private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e) { - UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit; - PreventSmsCommandMenuItem.Checked = this.preventSmsCommand; - UrlAutoShortenMenuItem.Checked = SettingManager.Common.UrlConvertAuto; - IdeographicSpaceToSpaceMenuItem.Checked = SettingManager.Common.WideSpaceConvert; - MultiLineMenuItem.Checked = SettingManager.Local.StatusMultiline; - FocusLockMenuItem.Checked = SettingManager.Common.FocusLockToStatusText; + this.UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit; + this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand; + this.UrlAutoShortenMenuItem.Checked = this.settings.Common.UrlConvertAuto; + this.IdeographicSpaceToSpaceMenuItem.Checked = this.settings.Common.WideSpaceConvert; + this.MultiLineMenuItem.Checked = this.settings.Local.StatusMultiline; + this.FocusLockMenuItem.Checked = this.settings.Common.FocusLockToStatusText; } private void ContextMenuPostMode_Opening(object sender, CancelEventArgs e) { - UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit; - PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand; - UrlAutoShortenPullDownMenuItem.Checked = SettingManager.Common.UrlConvertAuto; - IdeographicSpaceToSpacePullDownMenuItem.Checked = SettingManager.Common.WideSpaceConvert; - MultiLinePullDownMenuItem.Checked = SettingManager.Local.StatusMultiline; - FocusLockPullDownMenuItem.Checked = SettingManager.Common.FocusLockToStatusText; + this.UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit; + this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand; + this.UrlAutoShortenPullDownMenuItem.Checked = this.settings.Common.UrlConvertAuto; + this.IdeographicSpaceToSpacePullDownMenuItem.Checked = this.settings.Common.WideSpaceConvert; + this.MultiLinePullDownMenuItem.Checked = this.settings.Local.StatusMultiline; + this.FocusLockPullDownMenuItem.Checked = this.settings.Common.FocusLockToStatusText; } private void TraceOutToolStripMenuItem_Click(object sender, EventArgs e) { - if (TraceOutToolStripMenuItem.Checked) + if (this.TraceOutToolStripMenuItem.Checked) MyCommon.TraceFlag = true; else MyCommon.TraceFlag = false; } private void TweenMain_Deactivate(object sender, EventArgs e) - => this.StatusText_Leave(StatusText, EventArgs.Empty); // 画面が非アクティブになったら、発言欄の背景色をデフォルトへ + => this.StatusText_Leave(this.StatusText, EventArgs.Empty); // 画面が非アクティブになったら、発言欄の背景色をデフォルトへ private void TabRenameMenuItem_Click(object sender, EventArgs e) { - if (MyCommon.IsNullOrEmpty(_rclickTabName)) return; + if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return; - _ = TabRename(_rclickTabName, out _); + _ = this.TabRename(this.rclickTabName, out _); } private async void BitlyToolStripMenuItem_Click(object sender, EventArgs e) @@ -9815,7 +8317,7 @@ internal async Task FollowCommand(string id) { try { - var task = this.twitterApi.FriendshipsCreate(id).IgnoreResponse(); + var task = this.tw.Api.FriendshipsCreate(id).IgnoreResponse(); await dialog.WaitForAsync(this, task); } catch (WebApiException ex) @@ -9856,7 +8358,7 @@ internal async Task RemoveCommand(string id, bool skipInput) { try { - var task = this.twitterApi.FriendshipsDestroy(id).IgnoreResponse(); + var task = this.tw.Api.FriendshipsDestroy(id).IgnoreResponse(); await dialog.WaitForAsync(this, task); } catch (WebApiException ex) @@ -9900,7 +8402,7 @@ internal async Task ShowFriendship(string id) try { - var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id); + var task = this.tw.Api.FriendshipsShow(this.tw.Username, id); var friendship = await dialog.WaitForAsync(this, task); isFollowing = friendship.Relationship.Source.Following; @@ -9950,7 +8452,7 @@ internal async Task ShowFriendship(string[] ids) try { - var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id); + var task = this.tw.Api.FriendshipsShow(this.tw.Username, id); var friendship = await dialog.WaitForAsync(this, task); isFollowing = friendship.Relationship.Source.Following; @@ -9993,7 +8495,8 @@ internal async Task ShowFriendship(string[] ids) if (isFollowing) { if (MessageBox.Show( - Properties.Resources.GetFriendshipInfo7 + System.Environment.NewLine + result, Properties.Resources.GetFriendshipInfo8, + Properties.Resources.GetFriendshipInfo7 + System.Environment.NewLine + result, + Properties.Resources.GetFriendshipInfo8, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes) @@ -10009,7 +8512,7 @@ internal async Task ShowFriendship(string[] ids) } private async void OwnStatusMenuItem_Click(object sender, EventArgs e) - => await this.doShowUserStatus(tw.Username, false); + => await this.DoShowUserStatus(this.tw.Username, false); // TwitterIDでない固定文字列を調べる(文字列検証のみ 実際に取得はしない) // URLから切り出した文字列を渡す @@ -10022,12 +8525,12 @@ public bool IsTwitterId(string name) return !this.tw.Configuration.NonUsernamePaths.Contains(name, StringComparer.InvariantCultureIgnoreCase); } - private void doQuoteOfficial() + private void DoQuoteOfficial() { var post = this.CurrentPost; if (this.ExistCurrentPost && post != null) { - if (post.IsDm || !StatusText.Enabled) + if (post.IsDm || !this.StatusText.Enabled) return; if (post.IsProtect) @@ -10040,20 +8543,20 @@ private void doQuoteOfficial() this.inReplyTo = null; - StatusText.Text += " " + MyCommon.GetStatusUrl(post); + this.StatusText.Text += " " + MyCommon.GetStatusUrl(post); (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection; - StatusText.Focus(); + this.StatusText.Focus(); } } - private void doReTweetUnofficial() + private void DoReTweetUnofficial() { - //RT @id:内容 + // RT @id:内容 var post = this.CurrentPost; if (this.ExistCurrentPost && post != null) { - if (post.IsDm || !StatusText.Enabled) + if (post.IsDm || !this.StatusText.Enabled) return; if (post.IsProtect) @@ -10071,28 +8574,28 @@ private void doReTweetUnofficial() var inReplyToScreenName = post.ScreenName; this.inReplyTo = (inReplyToStatusId, inReplyToScreenName); - StatusText.Text += " RT @" + post.ScreenName + ": " + rtdata; + this.StatusText.Text += " RT @" + post.ScreenName + ": " + rtdata; (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection; - StatusText.Focus(); + this.StatusText.Focus(); } } private void QuoteStripMenuItem_Click(object sender, EventArgs e) - => this.doQuoteOfficial(); + => this.DoQuoteOfficial(); private async void SearchButton_Click(object sender, EventArgs e) { - //公式検索 + // 公式検索 var pnl = ((Control)sender).Parent; if (pnl == null) return; var tbName = pnl.Parent.Text; - var tb = (PublicSearchTabModel)_statuses.Tabs[tbName]; + var tb = (PublicSearchTabModel)this.statuses.Tabs[tbName]; var cmb = (ComboBox)pnl.Controls["comboSearch"]; var cmbLang = (ComboBox)pnl.Controls["comboLang"]; cmb.Text = cmb.Text.Trim(); // 検索式演算子 OR についてのみ大文字しか認識しないので強制的に大文字とする - var Quote = false; + var quote = false; var buf = new StringBuilder(); var c = cmb.Text.ToCharArray(); for (var cnt = 0; cnt < cmb.Text.Length; cnt++) @@ -10104,11 +8607,11 @@ private async void SearchButton_Click(object sender, EventArgs e) } if (c[cnt] == '"') { - Quote = !Quote; + quote = !quote; } else { - if (!Quote && cmb.Text.Substring(cnt, 4).Equals(" or ", StringComparison.OrdinalIgnoreCase)) + if (!quote && cmb.Text.Substring(cnt, 4).Equals(" or ", StringComparison.OrdinalIgnoreCase)) { buf.Append(" OR "); cnt += 3; @@ -10128,7 +8631,7 @@ private async void SearchButton_Click(object sender, EventArgs e) if (MyCommon.IsNullOrEmpty(cmb.Text)) { listView.Focus(); - SaveConfigsTabs(); + this.SaveConfigsTabs(); return; } if (queryChanged) @@ -10138,10 +8641,10 @@ private async void SearchButton_Click(object sender, EventArgs e) cmb.Items.Insert(0, tb.SearchWords); cmb.Text = tb.SearchWords; cmb.SelectAll(); - this.PurgeListViewItemCache(); - listView.VirtualListSize = 0; - _statuses.ClearTabIds(tbName); - SaveConfigsTabs(); //検索条件の保存 + this.statuses.ClearTabIds(tbName); + this.listCache?.PurgeCache(); + this.listCache?.UpdateListSize(); + this.SaveConfigsTabs(); // 検索条件の保存 } listView.Focus(); @@ -10158,34 +8661,30 @@ private async void RefreshMoreStripMenuItem_Click(object sender, EventArgs e) /// 非表示のタブについて -1 が返ることを常に考慮して下さい /// public int GetTabPageIndex(string tabName) - => this._statuses.Tabs.IndexOf(tabName); + => this.statuses.Tabs.IndexOf(tabName); private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e) { - if (_statuses.RemovedTab.Count == 0) + if (this.statuses.RemovedTab.Count == 0) { MessageBox.Show("There isn't removed tab.", "Undo", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } else { - DetailsListView? listView; - - var tb = _statuses.RemovedTab.Pop(); + var tb = this.statuses.RemovedTab.Pop(); if (tb.TabType == MyCommon.TabUsageType.Related) { - var relatedTab = _statuses.GetTabByType(MyCommon.TabUsageType.Related); + var relatedTab = this.statuses.GetTabByType(MyCommon.TabUsageType.Related); if (relatedTab != null) { // 関連発言なら既存のタブを置き換える tb.TabName = relatedTab.TabName; this.ClearTab(tb.TabName, false); - this._statuses.ReplaceTab(tb); + this.statuses.ReplaceTab(tb); - var tabIndex = this._statuses.Tabs.IndexOf(tb); - var tabPage = this.ListTab.TabPages[tabIndex]; - listView = (DetailsListView)tabPage.Tag; + var tabIndex = this.statuses.Tabs.IndexOf(tb); this.ListTab.SelectedIndex = tabIndex; } else @@ -10194,19 +8693,16 @@ private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e) var renamed = TabName; for (var i = 2; i <= 100; i++) { - if (!_statuses.ContainsTab(renamed)) + if (!this.statuses.ContainsTab(renamed)) break; renamed = TabName + i; } tb.TabName = renamed; - _statuses.AddTab(tb); - AddNewTab(tb, startup: false); + this.statuses.AddTab(tb); + this.AddNewTab(tb, startup: false); - var tabIndex = this._statuses.Tabs.Count - 1; - var tabPage = this.ListTab.TabPages[tabIndex]; - - listView = (DetailsListView)tabPage.Tag; + var tabIndex = this.statuses.Tabs.Count - 1; this.ListTab.SelectedIndex = tabIndex; } } @@ -10215,53 +8711,49 @@ private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e) var renamed = tb.TabName; for (var i = 1; i < int.MaxValue; i++) { - if (!_statuses.ContainsTab(renamed)) + if (!this.statuses.ContainsTab(renamed)) break; renamed = tb.TabName + "(" + i + ")"; } tb.TabName = renamed; - _statuses.AddTab(tb); - AddNewTab(tb, startup: false); - - var tabIndex = this._statuses.Tabs.Count - 1; - var tabPage = this.ListTab.TabPages[tabIndex]; + this.statuses.AddTab(tb); + this.AddNewTab(tb, startup: false); - listView = (DetailsListView)tabPage.Tag; + var tabIndex = this.statuses.Tabs.Count - 1; this.ListTab.SelectedIndex = tabIndex; } - SaveConfigsTabs(); - - if (listView != null) - { - using (ControlTransaction.Update(listView)) - { - listView.VirtualListSize = tb.AllCount; - } - } + this.SaveConfigsTabs(); } } - private async Task doMoveToRTHome() + private async Task DoMoveToRTHome() { var post = this.CurrentPost; if (post != null && post.RetweetedId != null) await MyCommon.OpenInBrowserAsync(this, "https://twitter.com/" + post.RetweetedBy); } - private async void MoveToRTHomeMenuItem_Click(object sender, EventArgs e) - => await this.doMoveToRTHome(); + private async void RetweetedByOpenInBrowserMenuItem_Click(object sender, EventArgs e) + => await this.DoMoveToRTHome(); - private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e) + private void AuthorListManageMenuItem_Click(object sender, EventArgs e) { var screenName = this.CurrentPost?.ScreenName; if (screenName != null) this.ListManageUserContext(screenName); } + private void RetweetedByListManageMenuItem_Click(object sender, EventArgs e) + { + var screenName = this.CurrentPost?.RetweetedBy; + if (screenName != null) + this.ListManageUserContext(screenName); + } + public void ListManageUserContext(string screenName) { - using var listSelectForm = new MyLists(screenName, this.twitterApi); + using var listSelectForm = new MyLists(screenName, this.tw.Api); listSelectForm.ShowDialog(this); } @@ -10291,32 +8783,32 @@ private void PublicSearchQueryMenuItem_Click(object sender, EventArgs e) } private void StatusLabel_DoubleClick(object sender, EventArgs e) - => MessageBox.Show(StatusLabel.TextHistory, "Logs", MessageBoxButtons.OK, MessageBoxIcon.None); + => MessageBox.Show(this.StatusLabel.TextHistory, "Logs", MessageBoxButtons.OK, MessageBoxIcon.None); private void HashManageMenuItem_Click(object sender, EventArgs e) { DialogResult rslt; try { - rslt = HashMgr.ShowDialog(); + rslt = this.HashMgr.ShowDialog(); } catch (Exception) { return; } - this.TopMost = SettingManager.Common.AlwaysTop; + this.TopMost = this.settings.Common.AlwaysTop; if (rslt == DialogResult.Cancel) return; - if (!MyCommon.IsNullOrEmpty(HashMgr.UseHash)) + if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash)) { - HashStripSplitButton.Text = HashMgr.UseHash; - HashTogglePullDownMenuItem.Checked = true; - HashToggleMenuItem.Checked = true; + this.HashStripSplitButton.Text = this.HashMgr.UseHash; + this.HashTogglePullDownMenuItem.Checked = true; + this.HashToggleMenuItem.Checked = true; } else { - HashStripSplitButton.Text = "#[-]"; - HashTogglePullDownMenuItem.Checked = false; - HashToggleMenuItem.Checked = false; + this.HashStripSplitButton.Text = "#[-]"; + this.HashTogglePullDownMenuItem.Checked = false; + this.HashToggleMenuItem.Checked = false; } this.MarkSettingCommonModified(); @@ -10325,18 +8817,18 @@ private void HashManageMenuItem_Click(object sender, EventArgs e) private void HashToggleMenuItem_Click(object sender, EventArgs e) { - HashMgr.ToggleHash(); - if (!MyCommon.IsNullOrEmpty(HashMgr.UseHash)) + this.HashMgr.ToggleHash(); + if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash)) { - HashStripSplitButton.Text = HashMgr.UseHash; - HashToggleMenuItem.Checked = true; - HashTogglePullDownMenuItem.Checked = true; + this.HashStripSplitButton.Text = this.HashMgr.UseHash; + this.HashToggleMenuItem.Checked = true; + this.HashTogglePullDownMenuItem.Checked = true; } else { - HashStripSplitButton.Text = "#[-]"; - HashToggleMenuItem.Checked = false; - HashTogglePullDownMenuItem.Checked = false; + this.HashStripSplitButton.Text = "#[-]"; + this.HashToggleMenuItem.Checked = false; + this.HashTogglePullDownMenuItem.Checked = false; } this.MarkSettingCommonModified(); this.StatusText_TextChanged(this.StatusText, EventArgs.Empty); @@ -10347,49 +8839,47 @@ private void HashStripSplitButton_ButtonClick(object sender, EventArgs e) public void SetPermanentHashtag(string hashtag) { - HashMgr.SetPermanentHash("#" + hashtag); - HashStripSplitButton.Text = HashMgr.UseHash; - HashTogglePullDownMenuItem.Checked = true; - HashToggleMenuItem.Checked = true; - //使用ハッシュタグとして設定 + this.HashMgr.SetPermanentHash("#" + hashtag); + this.HashStripSplitButton.Text = this.HashMgr.UseHash; + this.HashTogglePullDownMenuItem.Checked = true; + this.HashToggleMenuItem.Checked = true; + // 使用ハッシュタグとして設定 this.MarkSettingCommonModified(); } private void MenuItemOperate_DropDownOpening(object sender, EventArgs e) { + var tab = this.CurrentTab; + var post = this.CurrentPost; if (!this.ExistCurrentPost) { this.ReplyOpMenuItem.Enabled = false; this.ReplyAllOpMenuItem.Enabled = false; this.DmOpMenuItem.Enabled = false; - this.ShowProfMenuItem.Enabled = false; - this.ShowUserTimelineToolStripMenuItem.Enabled = false; - this.ListManageMenuItem.Enabled = false; - this.OpenFavOpMenuItem.Enabled = false; this.CreateTabRuleOpMenuItem.Enabled = false; this.CreateIdRuleOpMenuItem.Enabled = false; this.CreateSourceRuleOpMenuItem.Enabled = false; this.ReadOpMenuItem.Enabled = false; this.UnreadOpMenuItem.Enabled = false; + this.AuthorMenuItem.Visible = false; + this.RetweetedByMenuItem.Visible = false; } else { this.ReplyOpMenuItem.Enabled = true; this.ReplyAllOpMenuItem.Enabled = true; this.DmOpMenuItem.Enabled = true; - this.ShowProfMenuItem.Enabled = true; - this.ShowUserTimelineToolStripMenuItem.Enabled = true; - this.ListManageMenuItem.Enabled = true; - this.OpenFavOpMenuItem.Enabled = true; this.CreateTabRuleOpMenuItem.Enabled = true; this.CreateIdRuleOpMenuItem.Enabled = true; this.CreateSourceRuleOpMenuItem.Enabled = true; this.ReadOpMenuItem.Enabled = true; this.UnreadOpMenuItem.Enabled = true; + this.AuthorMenuItem.Visible = true; + this.AuthorMenuItem.Text = $"@{post!.ScreenName}"; + this.RetweetedByMenuItem.Visible = post.RetweetedByUserId != null; + this.RetweetedByMenuItem.Text = $"@{post.RetweetedBy}"; } - var tab = this.CurrentTab; - var post = this.CurrentPost; if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm) { this.FavOpMenuItem.Enabled = false; @@ -10407,9 +8897,9 @@ private void MenuItemOperate_DropDownOpening(object sender, EventArgs e) this.FavOpMenuItem.Enabled = true; this.UnFavOpMenuItem.Enabled = true; this.OpenStatusOpMenuItem.Enabled = true; - this.ShowRelatedStatusesMenuItem2.Enabled = true; //PublicSearchの時問題出るかも + this.ShowRelatedStatusesMenuItem2.Enabled = true; // PublicSearchの時問題出るかも - if (!post.CanRetweetBy(this.twitterApi.CurrentUserId)) + if (!post.CanRetweetBy(this.tw.UserId)) { this.RtOpMenuItem.Enabled = false; this.RtUnOpMenuItem.Enabled = false; @@ -10437,19 +8927,11 @@ private void MenuItemOperate_DropDownOpening(object sender, EventArgs e) } if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null) { - OpenRepSourceOpMenuItem.Enabled = false; - } - else - { - OpenRepSourceOpMenuItem.Enabled = true; - } - if (!this.ExistCurrentPost || post == null || MyCommon.IsNullOrEmpty(post.RetweetedBy)) - { - OpenRterHomeMenuItem.Enabled = false; + this.OpenRepSourceOpMenuItem.Enabled = false; } else { - OpenRterHomeMenuItem.Enabled = true; + this.OpenRepSourceOpMenuItem.Enabled = true; } if (this.ExistCurrentPost && post != null) @@ -10466,7 +8948,7 @@ public Twitter TwitterInstance private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e) { - if (this._initialLayout) + if (this.initialLayout) return; int splitterDistance; @@ -10477,7 +8959,7 @@ private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e) break; case FormWindowState.Maximized: // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する - var normalContainerWidth = this._mySize.Width - SystemInformation.Border3DSize.Width * 2; + var normalContainerWidth = this.mySize.Width - SystemInformation.Border3DSize.Width * 2; splitterDistance = this.SplitContainer3.SplitterDistance - (this.SplitContainer3.Width - normalContainerWidth); splitterDistance = Math.Min(splitterDistance, normalContainerWidth - this.SplitContainer3.SplitterWidth - this.SplitContainer3.Panel2MinSize); break; @@ -10485,25 +8967,25 @@ private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e) return; } - this._mySpDis3 = splitterDistance; + this.mySpDis3 = splitterDistance; this.MarkSettingLocalModified(); } private void MenuItemEdit_DropDownOpening(object sender, EventArgs e) { - if (_statuses.RemovedTab.Count == 0) + if (this.statuses.RemovedTab.Count == 0) { - UndoRemoveTabMenuItem.Enabled = false; + this.UndoRemoveTabMenuItem.Enabled = false; } else { - UndoRemoveTabMenuItem.Enabled = true; + this.UndoRemoveTabMenuItem.Enabled = true; } if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch) - PublicSearchQueryMenuItem.Enabled = true; + this.PublicSearchQueryMenuItem.Enabled = true; else - PublicSearchQueryMenuItem.Enabled = false; + this.PublicSearchQueryMenuItem.Enabled = false; var post = this.CurrentPost; if (!this.ExistCurrentPost || post == null) @@ -10529,11 +9011,11 @@ private void NotifyIcon1_MouseMove(object sender, MouseEventArgs e) private async void UserStatusToolStripMenuItem_Click(object sender, EventArgs e) => await this.ShowUserStatus(this.CurrentPost?.ScreenName ?? ""); - private async Task doShowUserStatus(string id, bool ShowInputDialog) + private async Task DoShowUserStatus(string id, bool showInputDialog) { TwitterUser? user = null; - if (ShowInputDialog) + if (showInputDialog) { using var inputName = new InputTabName(); inputName.FormTitle = "Show UserStatus"; @@ -10554,7 +9036,7 @@ private async Task doShowUserStatus(string id, bool ShowInputDialog) try { - var task = this.twitterApi.UsersShow(id); + var task = this.tw.Api.UsersShow(id); user = await dialog.WaitForAsync(this, task); } catch (WebApiException ex) @@ -10568,12 +9050,12 @@ private async Task doShowUserStatus(string id, bool ShowInputDialog) return; } - await this.doShowUserStatus(user); + await this.DoShowUserStatus(user); } - private async Task doShowUserStatus(TwitterUser user) + private async Task DoShowUserStatus(TwitterUser user) { - using var userDialog = new UserInfoDialog(this, this.twitterApi); + using var userDialog = new UserInfoDialog(this, this.tw.Api); var showUserTask = userDialog.ShowUserAsync(user); userDialog.ShowDialog(this); @@ -10584,13 +9066,13 @@ private async Task doShowUserStatus(TwitterUser user) await showUserTask; } - internal Task ShowUserStatus(string id, bool ShowInputDialog) - => this.doShowUserStatus(id, ShowInputDialog); + internal Task ShowUserStatus(string id, bool showInputDialog) + => this.DoShowUserStatus(id, showInputDialog); internal Task ShowUserStatus(string id) - => this.doShowUserStatus(id, true); + => this.DoShowUserStatus(id, true); - private async void ShowProfileMenuItem_Click(object sender, EventArgs e) + private async void AuthorShowProfileMenuItem_Click(object sender, EventArgs e) { var post = this.CurrentPost; if (post != null) @@ -10599,6 +9081,15 @@ private async void ShowProfileMenuItem_Click(object sender, EventArgs e) } } + private async void RetweetedByShowProfileMenuItem_Click(object sender, EventArgs e) + { + var retweetedBy = this.CurrentPost?.RetweetedBy; + if (retweetedBy != null) + { + await this.ShowUserStatus(retweetedBy, false); + } + } + private async void RtCountMenuItem_Click(object sender, EventArgs e) { var post = this.CurrentPost; @@ -10614,7 +9105,7 @@ private async void RtCountMenuItem_Click(object sender, EventArgs e) try { - var task = this.twitterApi.StatusesShow(statusId); + var task = this.tw.Api.StatusesShow(statusId); status = await dialog.WaitForAsync(this, task); } catch (WebApiException ex) @@ -10631,45 +9122,11 @@ private async void RtCountMenuItem_Click(object sender, EventArgs e) MessageBox.Show(status.RetweetCount + Properties.Resources.RtCountText1); } - private readonly HookGlobalHotkey _hookGlobalHotkey; - public TweenMain() - { - _hookGlobalHotkey = new HookGlobalHotkey(this); - - // この呼び出しは、Windows フォーム デザイナで必要です。 - InitializeComponent(); - - // InitializeComponent() 呼び出しの後で初期化を追加します。 - - if (!this.DesignMode) - { - // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策 - this.StatusText.Dock = DockStyle.Fill; - } - - this.tweetDetailsView.Owner = this; - - this._hookGlobalHotkey.HotkeyPressed += _hookGlobalHotkey_HotkeyPressed; - this.gh.NotifyClicked += GrowlHelper_Callback; - - // メイリオフォント指定時にタブの最小幅が広くなる問題の対策 - this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40); - - this.ImageSelector.Visible = false; - this.ImageSelector.Enabled = false; - this.ImageSelector.FilePickDialog = OpenFileDialog1; - - this.workerProgress = new Progress(x => this.StatusLabel.Text = x); - - this.ReplaceAppName(); - this.InitializeShortcuts(); - } - - private void _hookGlobalHotkey_HotkeyPressed(object sender, KeyEventArgs e) + private void HookGlobalHotkey_HotkeyPressed(object sender, KeyEventArgs e) { if ((this.WindowState == FormWindowState.Normal || this.WindowState == FormWindowState.Maximized) && this.Visible && Form.ActiveForm == this) { - //アイコン化 + // アイコン化 this.Visible = false; } else if (Form.ActiveForm == null) @@ -10688,15 +9145,15 @@ private void SplitContainer2_MouseDoubleClick(object sender, MouseEventArgs e) #region "画像投稿" private void ImageSelectMenuItem_Click(object sender, EventArgs e) { - if (ImageSelector.Visible) - ImageSelector.EndSelection(); + if (this.ImageSelector.Visible) + this.ImageSelector.EndSelection(); else - ImageSelector.BeginSelection(); + this.ImageSelector.BeginSelection(); } private void SelectMedia_DragEnter(DragEventArgs e) { - if (ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true)) + if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true)) { e.Effect = DragDropEffects.Copy; return; @@ -10708,20 +9165,20 @@ private void SelectMedia_DragDrop(DragEventArgs e) { this.Activate(); this.BringToFront(); - ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false)); - StatusText.Focus(); + this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false)); + this.StatusText.Focus(); } private void ImageSelector_BeginSelecting(object sender, EventArgs e) { - TimelinePanel.Visible = false; - TimelinePanel.Enabled = false; + this.TimelinePanel.Visible = false; + this.TimelinePanel.Enabled = false; } private void ImageSelector_EndSelecting(object sender, EventArgs e) { - TimelinePanel.Visible = true; - TimelinePanel.Enabled = true; + this.TimelinePanel.Visible = true; + this.TimelinePanel.Enabled = true; this.CurrentListView.Focus(); } @@ -10733,7 +9190,7 @@ private void ImageSelector_FilePickDialogClosed(object sender, EventArgs e) private void ImageSelector_SelectedServiceChanged(object sender, EventArgs e) { - if (ImageSelector.Visible) + if (this.ImageSelector.Visible) { this.MarkSettingCommonModified(); this.StatusText_TextChanged(this.StatusText, EventArgs.Empty); @@ -10770,6 +9227,11 @@ private void ProcClipboardFromStatusTextWhenCtrlPlusV() this.ImageSelector.BeginSelection(image); } } + else if (Clipboard.ContainsFileDropList()) + { + var files = Clipboard.GetFileDropList().Cast().ToArray(); + this.ImageSelector.BeginSelection(files); + } } catch (ExternalException ex) { @@ -10780,21 +9242,23 @@ private void ProcClipboardFromStatusTextWhenCtrlPlusV() private void ListManageToolStripMenuItem_Click(object sender, EventArgs e) { - using var form = new ListManage(tw); + using var form = new ListManage(this.tw); form.ShowDialog(this); } private bool ModifySettingCommon { get; set; } + private bool ModifySettingLocal { get; set; } + private bool ModifySettingAtId { get; set; } private void MenuItemCommand_DropDownOpening(object sender, EventArgs e) { var post = this.CurrentPost; if (this.ExistCurrentPost && post != null && !post.IsDm) - RtCountMenuItem.Enabled = true; + this.RtCountMenuItem.Enabled = true; else - RtCountMenuItem.Enabled = false; + this.RtCountMenuItem.Enabled = false; } private void CopyUserIdStripMenuItem_Click(object sender, EventArgs e) @@ -10838,7 +9302,7 @@ private async void ShowRelatedStatusesMenuItem_Click(object sender, EventArgs e) /// 名前の重複が多すぎてタブを作成できない場合 public async Task OpenRelatedTab(long statusId) { - var post = this._statuses[statusId]; + var post = this.statuses[statusId]; if (post == null) { try @@ -10862,13 +9326,13 @@ public async Task OpenRelatedTab(long statusId) /// 名前の重複が多すぎてタブを作成できない場合 private async Task OpenRelatedTab(PostClass post) { - var tabRelated = this._statuses.GetTabByType(); + var tabRelated = this.statuses.GetTabByType(); if (tabRelated != null) { this.RemoveSpecifiedTab(tabRelated.TabName, confirm: false); } - var tabName = this._statuses.MakeTabName("Related Tweets"); + var tabName = this.statuses.MakeTabName("Related Tweets"); tabRelated = new RelatedPostsTabModel(tabName, post) { @@ -10876,14 +9340,14 @@ private async Task OpenRelatedTab(PostClass post) Notify = false, }; - this._statuses.AddTab(tabRelated); + this.statuses.AddTab(tabRelated); this.AddNewTab(tabRelated, startup: false); - this.ListTab.SelectedIndex = this._statuses.Tabs.IndexOf(tabName); + this.ListTab.SelectedIndex = this.statuses.Tabs.IndexOf(tabName); await this.RefreshTabAsync(tabRelated); - var tabIndex = this._statuses.Tabs.IndexOf(tabRelated.TabName); + var tabIndex = this.statuses.Tabs.IndexOf(tabRelated.TabName); if (tabIndex != -1) { @@ -10905,14 +9369,14 @@ private async Task OpenRelatedTab(PostClass post) private void CacheInfoMenuItem_Click(object sender, EventArgs e) { var buf = new StringBuilder(); - buf.AppendFormat("キャッシュエントリ保持数 : {0}" + Environment.NewLine, IconCache.CacheCount); - buf.AppendFormat("キャッシュエントリ破棄数 : {0}" + Environment.NewLine, IconCache.CacheRemoveCount); + buf.AppendFormat("キャッシュエントリ保持数 : {0}" + Environment.NewLine, this.iconCache.CacheCount); + buf.AppendFormat("キャッシュエントリ破棄数 : {0}" + Environment.NewLine, this.iconCache.CacheRemoveCount); MessageBox.Show(buf.ToString(), "アイコンキャッシュ使用状況"); } private void TweenRestartMenuItem_Click(object sender, EventArgs e) { - MyCommon._endingFlag = true; + MyCommon.EndingFlag = true; try { this.Close(); @@ -10925,7 +9389,7 @@ private void TweenRestartMenuItem_Click(object sender, EventArgs e) } private async void OpenOwnHomeMenuItem_Click(object sender, EventArgs e) - => await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + tw.Username); + => await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + this.tw.Username); private bool ExistCurrentPost { @@ -10936,9 +9400,12 @@ private bool ExistCurrentPost } } - private async void ShowUserTimelineToolStripMenuItem_Click(object sender, EventArgs e) + private async void AuthorShowUserTimelineMenuItem_Click(object sender, EventArgs e) => await this.ShowUserTimeline(); + private async void RetweetedByShowUserTimelineMenuItem_Click(object sender, EventArgs e) + => await this.ShowRetweeterTimeline(); + private string GetUserIdFromCurPostOrInput(string caption) { var id = this.CurrentPost?.ScreenName ?? ""; @@ -10962,7 +9429,7 @@ private string GetUserIdFromCurPostOrInput(string caption) private async void UserTimelineToolStripMenuItem_Click(object sender, EventArgs e) { - var id = GetUserIdFromCurPostOrInput("Show UserTimeline"); + var id = this.GetUserIdFromCurPostOrInput("Show UserTimeline"); if (!MyCommon.IsNullOrEmpty(id)) { await this.AddNewTabForUserTimeline(id); @@ -10986,7 +9453,7 @@ private void SystemEvents_TimeChanged(object sender, EventArgs e) if (curTimeOffset != prevTimeOffset) { // タイムゾーンの変更を反映 - this.PurgeListViewItemCache(); + this.listCache?.PurgeCache(); this.CurrentListView.Refresh(); this.DispSelectedPost(forceupdate: true); @@ -11001,18 +9468,18 @@ private void TimelineRefreshEnableChange(bool isEnable) } private void StopRefreshAllMenuItem_CheckedChanged(object sender, EventArgs e) - => this.TimelineRefreshEnableChange(!StopRefreshAllMenuItem.Checked); + => this.TimelineRefreshEnableChange(!this.StopRefreshAllMenuItem.Checked); private async Task OpenUserAppointUrl() { - if (SettingManager.Common.UserAppointUrl != null) + if (this.settings.Common.UserAppointUrl != null) { - if (SettingManager.Common.UserAppointUrl.Contains("{ID}") || SettingManager.Common.UserAppointUrl.Contains("{STATUS}")) + if (this.settings.Common.UserAppointUrl.Contains("{ID}") || this.settings.Common.UserAppointUrl.Contains("{STATUS}")) { var post = this.CurrentPost; if (post != null) { - var xUrl = SettingManager.Common.UserAppointUrl; + var xUrl = this.settings.Common.UserAppointUrl; xUrl = xUrl.Replace("{ID}", post.ScreenName); var statusId = post.RetweetedId ?? post.StatusId; @@ -11023,7 +9490,7 @@ private async Task OpenUserAppointUrl() } else { - await MyCommon.OpenInBrowserAsync(this, SettingManager.Common.UserAppointUrl); + await MyCommon.OpenInBrowserAsync(this, this.settings.Common.UserAppointUrl); } } } @@ -11055,17 +9522,17 @@ await this.InvokeAsync(() => private void ReplaceAppName() { - MatomeMenuItem.Text = MyCommon.ReplaceAppName(MatomeMenuItem.Text); - AboutMenuItem.Text = MyCommon.ReplaceAppName(AboutMenuItem.Text); + this.MatomeMenuItem.Text = MyCommon.ReplaceAppName(this.MatomeMenuItem.Text); + this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text); } - private void tweetThumbnail1_ThumbnailLoading(object sender, EventArgs e) + private void TweetThumbnail_ThumbnailLoading(object sender, EventArgs e) => this.SplitContainer3.Panel2Collapsed = false; - private async void tweetThumbnail1_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e) + private async void TweetThumbnail_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e) => await this.OpenThumbnailPicture(e.Thumbnail); - private async void tweetThumbnail1_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e) + private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e) => await MyCommon.OpenInBrowserAsync(this, e.ImageUrl); private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail) @@ -11090,13 +9557,13 @@ private void PostButton_KeyDown(object sender, KeyEventArgs e) private void ContextMenuColumnHeader_Opening(object sender, CancelEventArgs e) { - this.IconSizeNoneToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.IconNone; - this.IconSize16ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon16; - this.IconSize24ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon24; - this.IconSize48ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon48; - this.IconSize48_2ToolStripMenuItem.Checked = SettingManager.Common.IconSize == MyCommon.IconSizes.Icon48_2; + this.IconSizeNoneToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.IconNone; + this.IconSize16ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon16; + this.IconSize24ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon24; + this.IconSize48ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon48; + this.IconSize48_2ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2; - this.LockListSortOrderToolStripMenuItem.Checked = SettingManager.Common.SortOrderLock; + this.LockListSortOrderToolStripMenuItem.Checked = this.settings.Common.SortOrderLock; } private void IconSizeNoneToolStripMenuItem_Click(object sender, EventArgs e) @@ -11116,18 +9583,18 @@ private void IconSize48_2ToolStripMenuItem_Click(object sender, EventArgs e) private void ChangeListViewIconSize(MyCommon.IconSizes iconSize) { - if (SettingManager.Common.IconSize == iconSize) return; + if (this.settings.Common.IconSize == iconSize) return; - var oldIconCol = _iconCol; + var oldIconCol = this.Use2ColumnsMode; - SettingManager.Common.IconSize = iconSize; - ApplyListViewIconSize(iconSize); + this.settings.Common.IconSize = iconSize; + this.ApplyListViewIconSize(iconSize); - if (_iconCol != oldIconCol) + if (this.Use2ColumnsMode != oldIconCol) { - foreach (TabPage tp in ListTab.TabPages) + foreach (TabPage tp in this.ListTab.TabPages) { - ResetColumns((DetailsListView)tp.Tag); + this.ResetColumns((DetailsListView)tp.Tag); } } @@ -11138,13 +9605,13 @@ private void ChangeListViewIconSize(MyCommon.IconSizes iconSize) private void LockListSortToolStripMenuItem_Click(object sender, EventArgs e) { var state = this.LockListSortOrderToolStripMenuItem.Checked; - if (SettingManager.Common.SortOrderLock == state) return; + if (this.settings.Common.SortOrderLock == state) return; - SettingManager.Common.SortOrderLock = state; + this.settings.Common.SortOrderLock = state; this.MarkSettingCommonModified(); } - private void tweetDetailsView_StatusChanged(object sender, TweetDetailsViewStatusChengedEventArgs e) + private void TweetDetailsView_StatusChanged(object sender, TweetDetailsViewStatusChengedEventArgs e) { if (!MyCommon.IsNullOrEmpty(e.StatusText)) { diff --git a/OpenTween/Tween.en.resx b/OpenTween/Tween.en.resx index 60d9549b5..98bab48e0 100644 --- a/OpenTween/Tween.en.resx +++ b/OpenTween/Tween.en.resx @@ -14,6 +14,24 @@ Create &New Tab... 278, 24 Twitter API Usage + 200, 22 + 197, 22 + Add/Remove from &Lists + 223, 22 + Add/Remove from &Lists + 259, 22 + 197, 22 + Open in browser (&H) + 223, 22 + Open in browser (&H) + 197, 22 + User &Profile + 223, 22 + User &Profile + 197, 22 + User's updates + 223, 22 + User's updates 218, 24 347, 24 Change Unread state ... @@ -131,12 +149,8 @@ 247, 24 Stick &List - 347, 24 - Manage &Lists 294, 24 Edit Lists - 258, 24 - Add to &Lists 570, 149 242, 24 @@ -164,12 +178,6 @@ &Tab 574, 27 - 245, 24 - Open Favorites (&G) - 245, 24 - Open &Home page - 245, 24 - Open &Retweeter Home 334, 22 Input field is &Multiline 445, 24 @@ -182,18 +190,12 @@ Enable Alert popup(&Q) 248, 24 Enable Alert Popup(&Q) - 306, 24 - Favorites(&G) - 306, 24 - &Home 347, 24 &Open as ... 294, 24 Open your profile page 306, 24 Reply to(&I) - 306, 24 - &Retweeter Home 306, 24 Status(&O) Open URL(&L)... @@ -387,6 +389,24 @@ @&Reply 258, 24 @ &Reply + 200, 22 + 197, 22 + Add/Remove from &Lists + 255, 22 + Add/Remove from &Lists + 259, 22 + 181, 22 + Open in browser (&H) + 255, 22 + Open in browser (&H) + 181, 22 + User &Profile + 255, 22 + User &Profile + 181, 22 + User's updates + 255, 22 + User's updates 258, 24 258, 24 294, 24 @@ -407,18 +427,18 @@ Settings (&O)... 278, 24 Shortcut keys - 258, 24 + 199, 22 + User &Profile + 259, 22 User &Profile - 347, 24 - User &Profile 258, 24 Related Posts (&G) 347, 24 Related Posts (&G) - 258, 24 - &User's updates - 347, 24 - User's updates + 199, 22 + User's updates + 259, 22 + User's updates Select a wav file. 122, 22 &Source diff --git a/OpenTween/Tween.resx b/OpenTween/Tween.resx index 73a72e978..320f436d1 100644 --- a/OpenTween/Tween.resx +++ b/OpenTween/Tween.resx @@ -22,6 +22,26 @@ System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ApiUsageInfoMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorListManageContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorListManageMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorOpenInBrowserContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorOpenInBrowserMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorShowProfileContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorShowProfileMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorShowUserTimelineContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorShowUserTimelineMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 BitlyToolStripMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 CacheInfoMenuItem @@ -154,12 +174,8 @@ 2 ListLockMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ListManageMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ListManageToolStripMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ListManageUserContextToolStripMenuItem2 - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ListTab TimelinePanel System.Windows.Forms.TabControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -192,12 +208,6 @@ ToolStripContainer1.TopToolStripPanel System.Windows.Forms.MenuStrip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 0 - MoveToFavToolStripMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - MoveToHomeToolStripMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - MoveToRTHomeMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 MultiLineMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 MultiLinePullDownMenuItem @@ -212,20 +222,14 @@ System.Windows.Forms.NotifyIcon, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 NotifyTbMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - OpenFavOpMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenFileDialog1 System.Windows.Forms.OpenFileDialog, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - OpenHomeOpMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenOpMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenOwnHomeMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenRepSourceOpMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - OpenRterHomeMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenStatusOpMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 OpenURLFileMenuItem @@ -250,8 +254,6 @@ 1 PostModeMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - PostStateImageList - System.Windows.Forms.ImageList, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 PreventSmsCommandMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 PreventSmsCommandPullDownMenuItem @@ -296,6 +298,26 @@ System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ReplyStripMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByListManageContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByListManageMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByOpenInBrowserContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByOpenInBrowserMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByShowProfileContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByShowProfileMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByShowUserTimelineContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByShowUserTimelineMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ReTweetStripMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ReTweetUnofficialStripMenuItem @@ -322,18 +344,18 @@ System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ShortcutKeyListMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ShowProfileContextMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ShowProfileMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ShowProfMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ShowRelatedStatusesMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ShowRelatedStatusesMenuItem2 System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ShowUserTimelineContextMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ShowUserTimelineToolStripMenuItem - System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ShowUserTimelineMenuItem + System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 SoundFileComboBox System.Windows.Forms.ToolStripComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 SoundFileTbComboBox @@ -578,6 +600,30 @@ タブ作成(&N)... 251, 22 Twitter API 使用情報 + 224, 22 + %Author% + 206, 22 + リスト管理(&L) + 247, 22 + リスト管理(&L) + 244, 22 + %Author% + 206, 22 + ブラウザで開く(&H) + Ctrl+H + 247, 22 + ブラウザで開く(&H) + 206, 22 + プロフィール表示 + + Alt+P + 247, 22 + プロフィール表示 + 206, 22 + ユーザーのタイムラインを表示 + Ctrl+U + 247, 22 + ユーザーのタイムラインを表示 228, 22 bit.ly 200, 22 @@ -728,12 +774,8 @@ MiddleCenter 175, 22 新着時リスト固定(&L) - 313, 22 - リスト管理(&L) 257, 22 リスト編集 - 241, 22 - リスト管理(&L) Bottom Fill Disable @@ -775,12 +817,6 @@ 0 MenuStrip1 531, 17 - 205, 22 - Favを開く(&G) - 205, 22 - ホームを開く(&H) - 205, 22 - RTした人のホームを開く(&R) Ctrl+Y 246, 22 発言欄複数行入力(&M) @@ -797,13 +833,7 @@ 395, 55 199, 22 新着通知表示(&Q) - Ctrl+G - 289, 22 - Favを開く(&G) 681, 93 - Ctrl+H - 289, 22 - ホームを開く(&H) 313, 22 開く(&O) 257, 22 @@ -811,9 +841,6 @@ Ctrl+I 289, 22 返信元ステータスを開く(&I) - Ctrl+Shift+H - 289, 22 - RTした人のホームを開く(&R) Ctrl+O 289, 22 ステータスを開く(&O) @@ -843,139 +870,6 @@ Post 225, 22 投稿設定 - - AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w - LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 - ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAACW - HQAAAk1TRnQBSQFMAgEBDwEAAZABAQGQAQEBEAEAARABAAT/ASEBAAj/AUIBTQE2BwABNgMAASgDAAFA - AwABQAMAAQEBAAEgBgABQP8AIwADwAH/A8AB/xAAA8AB/yQAA8AB/wPAAf8QAAPAAf8kAAPAAf8DwAH/ - EAADwAH/YAABTgGZAv8BTgGZAv8QAAFDAZkBQwH/A8AB/yAAAU4BmQL/AU4BmQL/EAABQwGZAUMB/wPA - Af8gAAFOAZkC/wFOAZkC/xAAAUMBmQFDAf8DwAH/WAADwAH/AU4BmQL/AU4BmQL/A8AB/wPAAf8DwAH/ - CAABQwGZAUMB/wPAAf8DwAH/FAADwAH/AU4BmQL/AU4BmQL/A8AB/wPAAf8DwAH/CAABQwGZAUMB/wPA - Af8DwAH/FAADwAH/AU4BmQL/AU4BmQL/A8AB/wPAAf8DwAH/CAABQwGZAUMB/wPAAf8DwAH/UAABTgGZ - Av8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/BAABQwGZAUMB/wFDAZkBQwH/AUMBmQFD - Af8DwAH/A8AB/wwAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/A8AB/wQAAUMBmQFD - Af8BQwGZAUMB/wFDAZkBQwH/A8AB/wPAAf8MAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFO - AZkC/wPAAf8EAAFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wPAAf8DwAH/TAABTgGZAv8BTgGZAv8BTgGZ - Av8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZ - AUMB/wPAAf8MAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf8BQwGZAUMB/wFD - AZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/A8AB/wwAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/ - AU4BmQL/AU4BmQL/A8AB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8DwAH/ - TAABTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/AUMBmQFDAf8BQwGZAUMB/wQA - AUMBmQFDAf8BQwGZAUMB/wPAAf8MAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPA - Af8BQwGZAUMB/wFDAZkBQwH/BAABQwGZAUMB/wFDAZkBQwH/A8AB/wwAAU4BmQL/AU4BmQL/AU4BmQL/ - AU4BmQL/AU4BmQL/AU4BmQL/A8AB/wFDAZkBQwH/AUMBmQFDAf8EAAFDAZkBQwH/AUMBmQFDAf8DwAH/ - TAABTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8EAAFDAZkBQwH/AUMBmQFDAf8BQwGZ - AUMB/wFDAZkBQwH/AUMBmQFDAf8QAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wQA - AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/xAAAU4BmQL/AU4BmQL/AU4BmQL/ - AU4BmQL/AU4BmQL/AU4BmQL/BAABQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/ - cAABQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf80AAFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/zQAAUMBmQFD - Af8BQwGZAUMB/wFDAZkBQwH/WAADwAH/A8AB/wPAAf8DwAH/A8AB/wPAAf9MAAPAAf8DwAH/FAADwAH/ - A8AB/wPAAf8DwAH/A8AB/wPAAf8MAAPAAf8DwAH/UAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8DwAH/SAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/DAABmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8DwAH/CAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/TAABmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/RAAB/wGZAUMC/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf8DwAH/ - CAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/BAAB/wGZAUMC/wGZAUMC/wGZ - AUMC/wGZAUMB/wPAAf8DwAH/SAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/ - QAAB/wGZAUMC/wGZAUMB/wQAAf8BmQFDAv8BmQFDAv8BmQFDAf8DwAH/CAABmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8BmQKBAf8DwAL/AZkBQwL/AZkBQwH/BAAB/wGZAUMC/wGZAUMC/wGZAUMB/wPA - Af9IAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/0QAAf8BmQFDAf8MAAH/AZkBQwL/ - AZkBQwH/A8AB/wPAAf8EAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wQAAf8BmQFD - Af8MAAH/AZkBQwL/AZkBQwH/A8AB/wPAAf9IAAGZAoEB/wQAA8AB/wGZAoEB/1wAAf8BmQFDAv8BmQFD - Af8DwAH/CAABmQKBAf8EAAPAAf8BmQKBAf8cAAH/AZkBQwL/AZkBQwH/A8AB/0wAAZkCgQH/AZkCgQH/ - ZAAB/wGZAUMB/xAAAZkCgQH/AZkCgQH/JAAB/wGZAUMB//8AcQADwAH/PAADwAH/PAADwAH/JAADwAH/ - A8AB/xAAA8AB/zgAAUMBmQFDAf8DwAH/OAABQwGZAUMB/wPAAf84AAFDAZkBQwH/A8AB/yAAAU4BmQL/ - AU4BmQL/EAABQwGZAUMB/wPAAf84AAFDAZkBQwH/A8AB/wPAAf80AAFDAZkBQwH/A8AB/wPAAf80AAFD - AZkBQwH/A8AB/wPAAf8UAAPAAf8BTgGZAv8BTgGZAv8DwAH/A8AB/wPAAf8IAAFDAZkBQwH/A8AB/wPA - Af8wAAFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wPAAf8DwAH/LAABQwGZAUMB/wFDAZkBQwH/AUMBmQFD - Af8DwAH/A8AB/ywAAUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/A8AB/wPAAf8MAAFOAZkC/wFOAZkC/wFO - AZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf8EAAFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wPAAf8DwAH/ - KAABQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/A8AB/ygAAUMBmQFDAf8BQwGZ - AUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wPAAf8oAAFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFD - AZkBQwH/AUMBmQFDAf8DwAH/DAABTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/ - AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/wPAAf8oAAFDAZkBQwH/AUMBmQFD - Af8EAAFDAZkBQwH/AUMBmQFDAf8DwAH/KAABQwGZAUMB/wFDAZkBQwH/BAABQwGZAUMB/wFDAZkBQwH/ - A8AB/ygAAUMBmQFDAf8BQwGZAUMB/wQAAUMBmQFDAf8BQwGZAUMB/wPAAf8MAAFOAZkC/wFOAZkC/wFO - AZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf8BQwGZAUMB/wFDAZkBQwH/BAABQwGZAUMB/wFDAZkBQwH/ - A8AB/ygAAUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/ywAAUMBmQFDAf8BQwGZ - AUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB/ywAAUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFD - Af8BQwGZAUMB/xAAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/BAABQwGZAUMB/wFD - AZkBQwH/AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/MAABQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf80AAFD - AZkBQwH/AUMBmQFDAf8BQwGZAUMB/zQAAUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/NAABQwGZAUMB/wFD - AZkBQwH/AUMBmQFDAf8YAAPAAf8DwAH/A8AB/wPAAf8DwAH/A8AB/0wAA8AB/wPAAf8UAAPAAf8DwAH/ - A8AB/wPAAf8DwAH/A8AB/wwAA8AB/wPAAf9QAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZ - AoEB/wPAAf9IAAH/AZkBQwL/AZkBQwH/A8AB/wPAAf8MAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZ - AoEB/wGZAoEB/wPAAf8IAAH/AZkBQwL/AZkBQwH/A8AB/wPAAf9MAAGZAoEB/wGZAoEB/wGZAoEB/wGZ - AoEB/wGZAoEB/wGZAoEB/wPAAf9EAAH/AZkBQwL/AZkBQwL/AZkBQwL/AZkBQwH/A8AB/wPAAf8IAAGZ - AoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wPAAf8EAAH/AZkBQwL/AZkBQwL/AZkBQwL/ - AZkBQwH/A8AB/wPAAf9IAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wPAAf9AAAH/ - AZkBQwL/AZkBQwH/BAAB/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf8IAAGZAoEB/wGZAoEB/wGZAoEB/wGZ - AoEB/wGZAoEB/wGZAoEB/wPAAv8BmQFDAv8BmQFDAf8EAAH/AZkBQwL/AZkBQwL/AZkBQwH/A8AB/0gA - AZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/RAAB/wGZAUMB/wwAAf8BmQFDAv8BmQFD - Af8DwAH/A8AB/wQAAZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/AZkCgQH/BAAB/wGZAUMB/wwA - Af8BmQFDAv8BmQFDAf8DwAH/A8AB/0gAAZkCgQH/BAADwAH/AZkCgQH/XAAB/wGZAUMC/wGZAUMB/wPA - Af8IAAGZAoEB/wQAA8AB/wGZAoEB/xwAAf8BmQFDAv8BmQFDAf8DwAH/TAABmQKBAf8BmQKBAf9kAAH/ - AZkBQwH/EAABmQKBAf8BmQKBAf8kAAH/AZkBQwH//wBZAAPAAf8DwAH/OAADwAH/A8AB/zgAA8AB/wPA - Af9QAAPAAf8gAAFOAZkC/wFOAZkC/zgAAU4BmQL/AU4BmQL/OAABTgGZAv8BTgGZAv9QAAFDAZkBQwH/ - A8AB/xgAA8AB/wFOAZkC/wFOAZkC/wPAAf8DwAH/A8AB/ygAA8AB/wFOAZkC/wFOAZkC/wPAAf8DwAH/ - A8AB/ygAA8AB/wFOAZkC/wFOAZkC/wPAAf8DwAH/A8AB/0gAAUMBmQFDAf8DwAH/A8AB/xAAAU4BmQL/ - AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/A8AB/yQAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/ - AU4BmQL/AU4BmQL/A8AB/yQAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/A8AB/0QA - AUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/A8AB/wPAAf8MAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFO - AZkC/wFOAZkC/wPAAf8kAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf8kAAFO - AZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf9AAAFDAZkBQwH/AUMBmQFDAf8BQwGZ - AUMB/wFDAZkBQwH/AUMBmQFDAf8DwAH/DAABTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZ - Av8DwAH/JAABTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/JAABTgGZAv8BTgGZ - Av8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8DwAH/QAABQwGZAUMB/wFDAZkBQwH/BAABQwGZAUMB/wFD - AZkBQwH/A8AB/wwAAU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/AU4BmQL/KAABTgGZAv8BTgGZ - Av8BTgGZAv8BTgGZAv8BTgGZAv8BTgGZAv8oAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFO - AZkC/0QAAUMBmQFDAf8BQwGZAUMB/wFDAZkBQwH/AUMBmQFDAf8BQwGZAUMB//AAAUMBmQFDAf8BQwGZ - AUMB/wFDAZkBQwH/GAADwAH/A8AB/wPAAf8DwAH/A8AB/wPAAf9MAAPAAf8DwAH/FAADwAH/A8AB/wPA - Af8DwAH/A8AB/wPAAf8MAAPAAf8DwAH/UAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8DwAH/SAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/DAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8DwAH/CAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/TAABmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8DwAH/RAAB/wGZAUMC/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf8DwAH/CAABmQKB - Af8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/BAAB/wGZAUMC/wGZAUMC/wGZAUMC/wGZ - AUMB/wPAAf8DwAH/SAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/QAAB/wGZ - AUMC/wGZAUMB/wQAAf8BmQFDAv8BmQFDAv8BmQFDAf8DwAH/CAABmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8DwAL/AZkBQwL/AZkBQwH/BAAB/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf9IAAGZ - AoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/0QAAf8BmQFDAf8MAAH/AZkBQwL/AZkBQwH/ - A8AB/wPAAf8EAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wQAAf8BmQFDAf8MAAH/ - AZkBQwL/AZkBQwH/A8AB/wPAAf9IAAGZAoEB/wQAA8AB/wGZAoEB/1wAAf8BmQFDAv8BmQFDAf8DwAH/ - CAABmQKBAf8EAAPAAf8BmQKBAf8cAAH/AZkBQwL/AZkBQwH/A8AB/0wAAZkCgQH/AZkCgQH/ZAAB/wGZ - AUMB/xAAAZkCgQH/AZkCgQH/JAAB/wGZAUMB//8A/wAaAAPAAf8DwAH/9AABTgGZAv8BTgGZAv/wAAPA - Af8BTgGZAv8BTgGZAv8DwAH/A8AB/wPAAf/kAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFO - AZkC/wPAAf/kAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf/kAAFOAZkC/wFO - AZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wPAAf/kAAFOAZkC/wFOAZkC/wFOAZkC/wFOAZkC/wFO - AZkC/wFOAZkC//8ALQADwAH/A8AB/wPAAf8DwAH/A8AB/wPAAf9MAAPAAf8DwAH/FAADwAH/A8AB/wPA - Af8DwAH/A8AB/wPAAf8MAAPAAf8DwAH/UAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8DwAH/SAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/DAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8DwAH/CAAB/wGZAUMC/wGZAUMB/wPAAf8DwAH/TAABmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8DwAH/RAAB/wGZAUMC/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf8DwAH/CAABmQKB - Af8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/BAAB/wGZAUMC/wGZAUMC/wGZAUMC/wGZ - AUMB/wPAAf8DwAH/SAABmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8BmQKBAf8DwAH/QAAB/wGZ - AUMC/wGZAUMB/wQAAf8BmQFDAv8BmQFDAv8BmQFDAf8DwAH/CAABmQKBAf8BmQKBAf8BmQKBAf8BmQKB - Af8BmQKBAf8BmQKBAf8DwAL/AZkBQwL/AZkBQwH/BAAB/wGZAUMC/wGZAUMC/wGZAUMB/wPAAf9IAAGZ - AoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/0QAAf8BmQFDAf8MAAH/AZkBQwL/AZkBQwH/ - A8AB/wPAAf8EAAGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wGZAoEB/wQAAf8BmQFDAf8MAAH/ - AZkBQwL/AZkBQwH/A8AB/wPAAf9IAAGZAoEB/wQAA8AB/wGZAoEB/1wAAf8BmQFDAv8BmQFDAf8DwAH/ - CAABmQKBAf8EAAPAAf8BmQKBAf8cAAH/AZkBQwL/AZkBQwH/A8AB/0wAAZkCgQH/AZkCgQH/ZAAB/wGZ - AUMB/xAAAZkCgQH/AZkCgQH/JAAB/wGZAUMB/0gAAUIBTQE+BwABPgMAASgDAAFAAwABQAMAAQEBAAEB - BgABAhYAA/8BAAb/AgAB8wHfAfMB3wHzAd8CAAHnAZ8B5wGfAecBnwIAAYEBjwGBAY8BgQGPAgABAQEH - AQEBBwEBAQcDAAEHAQABBwEAAQcDAAFHAQABRwEAAUcCAAECAQ8BAgEPAQIBDwIAAf8BHwH/AR8B/wEf - AgABgQL/Ac8BgQHPAgABAQL/AYcBAQGHAgABAQL/AQMBAQEDAgABAQH/Af4BQwEAAUMCAAEDAf8B/gHh - AQIB4QIAAacC/wHxAacB8QIAAc8C/wH7Ac8B+wIACf8B3wH/Ad8B/wHfAfMB3wH/AZ8B/wGfAf8BnwHn - AZ8B/wGPAf8BjwH/AY8BgQGPAf8BBwH/AQcB/wEHAQEBBwH+AQcB/gEHAf4BBwEAAQcB/gFHAf4BRwH+ - AUcBAAFHAf4BDwH+AQ8B/gEPAQIBDwH/AR8B/wEfAf8BHwH/AR8BgQL/Ac8BgQHPAv8BAQL/AYcBAQGH - Av8BAQL/AQMBAQEDAv8BAQH/Af4BQwEAAUMC/wEDAf8B/gHhAQIB4QL/AacC/wHxAacB8QL/Ac8C/wH7 - Ac8B+wr/AfMB/wHzAf8B8wL/Ad8B5wH/AecB/wHnAv8BnwGBAf8BgQH/AYEC/wGPAQEB/wEBAf8BAQL/ - AQcBAQH/AQEB/wEBAf8B/gEHAQEB/wEBAf8BAQH/Af4BRwEDAf8BAwH/AQMB/wH+AQ8H/wEfAYEC/wHP - AYEBzwL/AQEC/wGHAQEBhwL/AQEC/wEDAQEBAwL/AQEB/wH+AUMBAAFDAv8BAwH/Af4B4QECAeEC/wGn - Av8B8QGnAfEC/wHPAv8B+wHPAfsQ/wHzB/8B5wf/AYEH/wEBB/8BAQf/AQEH/wEDCf8BgQL/Ac8BgQHP - Av8BAQL/AYcBAQGHAv8BAQL/AQMBAQEDAv8BAQH/Af4BQwEAAUMC/wEDAf8B/gHhAQIB4QL/AacC/wHx - AacB8QL/Ac8C/wH7Ac8B+wL/Cw== - - 363, 17 246, 22 SMSコマンドを回避する 257, 22 @@ -1028,6 +922,27 @@ @返信(&R) 241, 22 @返信(&R) + 224, 22 + %RetweetedBy% + 206, 22 + リスト管理(&L) + 230, 22 + リスト管理(&L) + 244, 22 + %RetweetedBy% + 206, 22 + ブラウザで開く(&H) + Ctrl+Shift+H + 230, 22 + ブラウザで開く(&H) + 206, 22 + プロフィール表示 + 230, 22 + プロフィール表示 + 206, 22 + ユーザーのタイムラインを表示 + 230, 22 + ユーザーのタイムラインを表示 241, 22 Re&tweet 241, 22 @@ -1056,21 +971,18 @@ 設定(&O)... 251, 22 ショートカットキー一覧 - 241, 22 + 223, 22 + プロフィール表示 + 243, 22 プロフィール表示 - - Alt+P - 313, 22 - プロフィール表示 241, 22 関連発言表示(&G) 313, 22 関連発言表示(&G) - 241, 22 + 223, 22 ユーザーのタイムラインを表示 - Ctrl+U - 313, 22 - ユーザーのタイムラインを表示 + 243, 22 + ユーザーのタイムラインを表示 121, 23 再生するwavファイルを指定してください 121, 23 diff --git a/OpenTween/TweenAboutBox.cs b/OpenTween/TweenAboutBox.cs index eb27e7bc8..ca8654278 100644 --- a/OpenTween/TweenAboutBox.cs +++ b/OpenTween/TweenAboutBox.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -32,9 +32,9 @@ using System.Data; using System.Drawing; using System.Linq; +using System.Reflection; using System.Text; using System.Windows.Forms; -using System.Reflection; namespace OpenTween { @@ -49,23 +49,24 @@ private void TweenAboutBox_Load(object sender, EventArgs e) this.Text = MyCommon.ReplaceAppName(this.Text); // バージョン情報ボックスに表示されたテキストをすべて初期化します。 - // TODO: [プロジェクト] メニューの下にある [プロジェクト プロパティ] ダイアログの [アプリケーション] ペインで、アプリケーションのアセンブリ情報を + // TODO: [プロジェクト] メニューの下にある [プロジェクト プロパティ] ダイアログの [アプリケーション] ペインで、アプリケーションのアセンブリ情報を // カスタマイズします。 this.LabelProductName.Text = ApplicationSettings.ApplicationName; this.LabelVersion.Text = string.Format(Properties.Resources.TweenAboutBox_LoadText2, MyCommon.GetReadableVersion()); - this.LabelCopyright.Text = GetApplicationAttribute().Copyright; + this.LabelCopyright.Text = this.GetApplicationAttribute().Copyright; this.LabelCompanyName.Text = Application.CompanyName; - this.TextBoxDescription.Text = GetApplicationAttribute().Description; + this.TextBoxDescription.Text = this.GetApplicationAttribute().Description; this.ChangeLog.Text = Properties.Resources.ChangeLog; this.TextBoxDescription.Text = string.Format(Properties.Resources.Description, ApplicationSettings.FeedbackTwitterName, ApplicationSettings.FeedbackEmailAddress) + Environment.NewLine + Environment.NewLine + "This software was created by The OpenTween Project, based on Tween v1.1.0.0, which is licensed under GPLv3."; } - protected T GetApplicationAttribute() where T : Attribute + protected T GetApplicationAttribute() + where T : Attribute { var currentAssembly = Assembly.GetExecutingAssembly(); - return (T) Attribute.GetCustomAttribute(currentAssembly, typeof(T)); + return (T)Attribute.GetCustomAttribute(currentAssembly, typeof(T)); } private void OKButton_Click(object sender, EventArgs e) diff --git a/OpenTween/TweetDetailsView.Designer.cs b/OpenTween/TweetDetailsView.Designer.cs index c772718b0..a4c6843e4 100644 --- a/OpenTween/TweetDetailsView.Designer.cs +++ b/OpenTween/TweetDetailsView.Designer.cs @@ -45,7 +45,9 @@ private void InitializeComponent() this.IconNameToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ReloadIconToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SaveIconPictureToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.NameLabel = new System.Windows.Forms.Label(); + this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); + this.AuthorNameLinkLabel = new System.Windows.Forms.LinkLabel(); + this.RetweetedByLinkLabel = new System.Windows.Forms.LinkLabel(); this.PostBrowser = new System.Windows.Forms.WebBrowser(); this.ContextMenuPostBrowser = new System.Windows.Forms.ContextMenuStrip(this.components); this.SelectionSearchContextMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -81,6 +83,7 @@ private void InitializeComponent() this.TableLayoutPanel1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.UserPicture)).BeginInit(); this.ContextMenuUserPicture.SuspendLayout(); + this.flowLayoutPanel1.SuspendLayout(); this.ContextMenuPostBrowser.SuspendLayout(); this.ContextMenuSource.SuspendLayout(); this.SuspendLayout(); @@ -89,7 +92,7 @@ private void InitializeComponent() // resources.ApplyResources(this.TableLayoutPanel1, "TableLayoutPanel1"); this.TableLayoutPanel1.Controls.Add(this.UserPicture, 0, 0); - this.TableLayoutPanel1.Controls.Add(this.NameLabel, 1, 0); + this.TableLayoutPanel1.Controls.Add(this.flowLayoutPanel1, 1, 0); this.TableLayoutPanel1.Controls.Add(this.PostBrowser, 1, 1); this.TableLayoutPanel1.Controls.Add(this.DateTimeLabel, 2, 0); this.TableLayoutPanel1.Controls.Add(this.SourceLinkLabel, 3, 0); @@ -102,12 +105,11 @@ private void InitializeComponent() this.UserPicture.BackColor = System.Drawing.Color.White; this.UserPicture.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.UserPicture.ContextMenuStrip = this.ContextMenuUserPicture; + this.UserPicture.Cursor = System.Windows.Forms.Cursors.Hand; this.UserPicture.Name = "UserPicture"; this.TableLayoutPanel1.SetRowSpan(this.UserPicture, 2); this.UserPicture.TabStop = false; - this.UserPicture.DoubleClick += new System.EventHandler(this.UserPicture_DoubleClick); - this.UserPicture.MouseEnter += new System.EventHandler(this.UserPicture_MouseEnter); - this.UserPicture.MouseLeave += new System.EventHandler(this.UserPicture_MouseLeave); + this.UserPicture.Click += new System.EventHandler(this.UserPicture_Click); // // ContextMenuUserPicture // @@ -199,12 +201,34 @@ private void InitializeComponent() resources.ApplyResources(this.SaveIconPictureToolStripMenuItem, "SaveIconPictureToolStripMenuItem"); this.SaveIconPictureToolStripMenuItem.Click += new System.EventHandler(this.SaveIconPictureToolStripMenuItem_Click); // - // NameLabel + // flowLayoutPanel1 // - this.NameLabel.AutoEllipsis = true; - resources.ApplyResources(this.NameLabel, "NameLabel"); - this.NameLabel.Name = "NameLabel"; - this.NameLabel.UseMnemonic = false; + resources.ApplyResources(this.flowLayoutPanel1, "flowLayoutPanel1"); + this.flowLayoutPanel1.Controls.Add(this.AuthorNameLinkLabel); + this.flowLayoutPanel1.Controls.Add(this.RetweetedByLinkLabel); + this.flowLayoutPanel1.Name = "flowLayoutPanel1"; + // + // AuthorNameLinkLabel + // + this.AuthorNameLinkLabel.ActiveLinkColor = System.Drawing.SystemColors.ControlText; + resources.ApplyResources(this.AuthorNameLinkLabel, "AuthorNameLinkLabel"); + this.AuthorNameLinkLabel.AutoEllipsis = true; + this.AuthorNameLinkLabel.LinkBehavior = System.Windows.Forms.LinkBehavior.NeverUnderline; + this.AuthorNameLinkLabel.LinkColor = System.Drawing.SystemColors.ControlText; + this.AuthorNameLinkLabel.Name = "AuthorNameLinkLabel"; + this.AuthorNameLinkLabel.TabStop = true; + this.AuthorNameLinkLabel.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.AuthorNameLinkLabel_LinkClicked); + // + // RetweetedByLinkLabel + // + this.RetweetedByLinkLabel.ActiveLinkColor = System.Drawing.SystemColors.ControlText; + resources.ApplyResources(this.RetweetedByLinkLabel, "RetweetedByLinkLabel"); + this.RetweetedByLinkLabel.AutoEllipsis = true; + this.RetweetedByLinkLabel.LinkBehavior = System.Windows.Forms.LinkBehavior.NeverUnderline; + this.RetweetedByLinkLabel.LinkColor = System.Drawing.SystemColors.ControlText; + this.RetweetedByLinkLabel.Name = "RetweetedByLinkLabel"; + this.RetweetedByLinkLabel.TabStop = true; + this.RetweetedByLinkLabel.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.RetweetedByLinkLabel_LinkClicked); // // PostBrowser // @@ -445,6 +469,8 @@ private void InitializeComponent() this.TableLayoutPanel1.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.UserPicture)).EndInit(); this.ContextMenuUserPicture.ResumeLayout(false); + this.flowLayoutPanel1.ResumeLayout(false); + this.flowLayoutPanel1.PerformLayout(); this.ContextMenuPostBrowser.ResumeLayout(false); this.ContextMenuSource.ResumeLayout(false); this.ResumeLayout(false); @@ -455,7 +481,9 @@ private void InitializeComponent() internal System.Windows.Forms.TableLayoutPanel TableLayoutPanel1; internal OTPictureBox UserPicture; - internal System.Windows.Forms.Label NameLabel; + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; + private System.Windows.Forms.LinkLabel AuthorNameLinkLabel; + private System.Windows.Forms.LinkLabel RetweetedByLinkLabel; internal System.Windows.Forms.WebBrowser PostBrowser; internal System.Windows.Forms.Label DateTimeLabel; internal System.Windows.Forms.LinkLabel SourceLinkLabel; diff --git a/OpenTween/TweetDetailsView.cs b/OpenTween/TweetDetailsView.cs index 6f8533670..4f8a5350f 100644 --- a/OpenTween/TweetDetailsView.cs +++ b/OpenTween/TweetDetailsView.cs @@ -47,17 +47,29 @@ namespace OpenTween { public partial class TweetDetailsView : UserControl { - public TweenMain Owner { get; set; } = null!; + private TweenMain Owner + => this.owner ?? throw this.NotInitializedException(); /// プロフィール画像のキャッシュ - public ImageCache IconCache { get; set; } = null!; + private ImageCache IconCache + => this.iconCache ?? throw this.NotInitializedException(); /// のダンプを表示するか + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool DumpPostClass { get; set; } /// 現在表示中の発言 public PostClass? CurrentPost { get; private set; } + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ThemeManager Theme + { + get => this.themeManager ?? throw this.NotInitializedException(); + set => this.themeManager = value; + } + [DefaultValue(false)] public new bool TabStop { @@ -69,7 +81,11 @@ public partial class TweetDetailsView : UserControl public event EventHandler? StatusChanged; /// 展開時の .StatusText を保持するフィールド - private string _postBrowserStatusText = ""; + private string postBrowserStatusText = ""; + + private TweenMain? owner; + private ImageCache? iconCache; + private ThemeManager? themeManager; public TweetDetailsView() { @@ -77,15 +93,29 @@ public TweetDetailsView() this.TabStop = false; - //発言詳細部の初期化 - NameLabel.Text = ""; - DateTimeLabel.Text = ""; - SourceLinkLabel.Text = ""; + // 発言詳細部の初期化 + this.AuthorNameLinkLabel.Text = ""; + this.RetweetedByLinkLabel.Text = ""; + this.DateTimeLabel.Text = ""; + this.SourceLinkLabel.Text = ""; - new InternetSecurityManager(PostBrowser); + new InternetSecurityManager(this.PostBrowser); this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する } + public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager) + { + this.owner = owner; + this.iconCache = iconCache; + this.themeManager = themeManager; + } + + private Exception NotInitializedException() + => new InvalidOperationException("Cannot call before initialization"); + + public void ClearPostBrowser() + => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(""); + public async Task ShowPostDetails(PostClass post) { this.CurrentPost = post; @@ -94,8 +124,8 @@ public async Task ShowPostDetails(PostClass post) using (ControlTransaction.Update(this.TableLayoutPanel1)) { - SourceLinkLabel.Text = post.Source; - SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる + this.SourceLinkLabel.Text = post.Source; + this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる string nameText; if (post.IsDm) @@ -110,23 +140,35 @@ public async Task ShowPostDetails(PostClass post) nameText = ""; } nameText += post.ScreenName + "/" + post.Nickname; - if (post.RetweetedId != null) - nameText += " (RT:" + post.RetweetedBy + ")"; + this.AuthorNameLinkLabel.Text = nameText; - NameLabel.Text = nameText; + if (post.RetweetedId != null) + { + this.RetweetedByLinkLabel.Visible = true; + this.RetweetedByLinkLabel.Text = $"(RT:{post.RetweetedBy})"; + } + else + { + this.RetweetedByLinkLabel.Visible = false; + this.RetweetedByLinkLabel.Text = ""; + } var nameForeColor = SystemColors.ControlText; - if (post.IsOwl && (SettingManager.Common.OneWayLove || post.IsDm)) - nameForeColor = SettingManager.Local.ColorOWL; + if (post.IsOwl && (SettingManager.Instance.Common.OneWayLove || post.IsDm)) + nameForeColor = this.Theme.ColorOWL; if (post.RetweetedId != null) - nameForeColor = SettingManager.Local.ColorRetweet; + nameForeColor = this.Theme.ColorRetweet; if (post.IsFav) - nameForeColor = SettingManager.Local.ColorFav; - NameLabel.ForeColor = nameForeColor; + nameForeColor = this.Theme.ColorFav; + + this.AuthorNameLinkLabel.LinkColor = nameForeColor; + this.AuthorNameLinkLabel.ActiveLinkColor = nameForeColor; + this.RetweetedByLinkLabel.LinkColor = nameForeColor; + this.RetweetedByLinkLabel.ActiveLinkColor = nameForeColor; loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl)); - DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString(); + this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString(); } if (this.DumpPostClass) @@ -177,14 +219,14 @@ public async Task ShowPostDetails(PostClass post) } sb.Append("-----End PostClass Dump
"); - PostBrowser.DocumentText = this.Owner.createDetailHtml(sb.ToString()); + this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString()); return; } using (ControlTransaction.Update(this.PostBrowser)) { this.PostBrowser.DocumentText = - this.Owner.createDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text); + this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text); this.PostBrowser.Document.Window.ScrollTo(0, 0); } @@ -196,31 +238,31 @@ public async Task ShowPostDetails(PostClass post) public void ScrollDownPostBrowser(bool forward) { - var doc = PostBrowser.Document; + var doc = this.PostBrowser.Document; if (doc == null) return; var tags = doc.GetElementsByTagName("html"); if (tags.Count > 0) { if (forward) - tags[0].ScrollTop += SettingManager.Local.FontDetail.Height; + tags[0].ScrollTop += this.Theme.FontDetail.Height; else - tags[0].ScrollTop -= SettingManager.Local.FontDetail.Height; + tags[0].ScrollTop -= this.Theme.FontDetail.Height; } } public void PageDownPostBrowser(bool forward) { - var doc = PostBrowser.Document; + var doc = this.PostBrowser.Document; if (doc == null) return; var tags = doc.GetElementsByTagName("html"); if (tags.Count > 0) { if (forward) - tags[0].ScrollTop += PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height; + tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height; else - tags[0].ScrollTop -= PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height; + tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height; } } @@ -231,9 +273,9 @@ public HtmlElement[] GetLinkElements() .ToArray(); } - private async Task SetUserPictureAsync(string imageUrl, bool force = false) + private async Task SetUserPictureAsync(string normalImageUrl, bool force = false) { - if (MyCommon.IsNullOrEmpty(imageUrl)) + if (MyCommon.IsNullOrEmpty(normalImageUrl)) return; if (this.IconCache == null) @@ -241,14 +283,31 @@ private async Task SetUserPictureAsync(string imageUrl, bool force = false) this.ClearUserPicture(); - await this.UserPicture.SetImageFromTask(async () => + var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width); + var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize); + if (cachedImage != null) { - var image = await this.IconCache.DownloadImageAsync(imageUrl, force) - .ConfigureAwait(false); + // 既にキャッシュされていればそれを表示して終了 + this.UserPicture.Image = cachedImage.Clone(); + return; + } + + // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する + var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini"); + if (fallbackImage != null) + this.UserPicture.Image = fallbackImage.Clone(); + + await this.UserPicture.SetImageFromTask( + async () => + { + var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize); + var image = await this.IconCache.DownloadImageAsync(imageUrl, force) + .ConfigureAwait(false); - return await image.CloneAsync() - .ConfigureAwait(false); - }); + return image.Clone(); + }, + useStatusImage: false + ); } /// @@ -283,7 +342,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml; using (ControlTransaction.Update(this.PostBrowser)) - this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body); + this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body); // 引用ツイートを読み込み var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList(); @@ -300,7 +359,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) body = post.Text + string.Concat(quoteHtmls); using (ControlTransaction.Update(this.PostBrowser)) - this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body); + this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body); } private async Task CreateQuoteTweetHtml(long statusId, bool isReply) @@ -372,9 +431,9 @@ private async Task DoTranslation(string str) { var translatedText = await bing.TranslateAsync(str, langFrom: null, - langTo: SettingManager.Common.TranslateLanguage); + langTo: SettingManager.Instance.Common.TranslateLanguage); - this.PostBrowser.DocumentText = this.Owner.createDetailHtml(translatedText); + this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText); } catch (WebApiException e) { @@ -388,26 +447,26 @@ private async Task DoTranslation(string str) private async Task DoSearchToolStrip(string url) { - //発言詳細で「選択文字列で検索」(選択文字列取得) - var _selText = this.PostBrowser.GetSelectedText(); + // 発言詳細で「選択文字列で検索」(選択文字列取得) + var selText = this.PostBrowser.GetSelectedText(); - if (_selText != null) + if (selText != null) { if (url == Properties.Resources.SearchItem4Url) { - //公式検索 - this.Owner.AddNewTabForSearch(_selText); + // 公式検索 + this.Owner.AddNewTabForSearch(selText); return; } - var tmp = string.Format(url, Uri.EscapeDataString(_selText)); + var tmp = string.Format(url, Uri.EscapeDataString(selText)); await MyCommon.OpenInBrowserAsync(this, tmp); } } private string? GetUserId() { - var m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$"); + var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$"); if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}"))) return m.Result("${ScreenName}"); else @@ -420,26 +479,20 @@ protected void RaiseStatusChanged(string statusText) private void TweetDetailsView_FontChanged(object sender, EventArgs e) { // OTBaseForm.GlobalFont による UI フォントの変更に対応 - var origFont = this.NameLabel.Font; - this.NameLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style); + var origFont = this.AuthorNameLinkLabel.Font; + this.AuthorNameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style); + this.RetweetedByLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style); } #region TableLayoutPanel1 - private async void UserPicture_DoubleClick(object sender, EventArgs e) + private async void UserPicture_Click(object sender, EventArgs e) { - if (this.CurrentPost == null) - return; - - await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + this.CurrentPost.ScreenName); + var screenName = this.CurrentPost?.ScreenName; + if (screenName != null) + await this.Owner.ShowUserStatus(screenName, showInputDialog: false); } - private void UserPicture_MouseEnter(object sender, EventArgs e) - => this.UserPicture.Cursor = Cursors.Hand; - - private void UserPicture_MouseLeave(object sender, EventArgs e) - => this.UserPicture.Cursor = Cursors.Default; - private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e) { if (e.Url.AbsoluteUri != "about:blank") @@ -465,8 +518,8 @@ private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEve private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { - var KeyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask); - if (KeyRes) + var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask); + if (keyRes) { e.IsInputKey = true; } @@ -498,13 +551,13 @@ private void PostBrowser_StatusTextChanged(object sender, EventArgs e) { try { - if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal) - || PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal) - || PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal)) + if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal) + || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal) + || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal)) { this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&")); } - if (MyCommon.IsNullOrEmpty(PostBrowser.StatusText)) + if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText)) { this.RaiseStatusChanged(statusText: ""); } @@ -541,7 +594,7 @@ private void SourceLinkLabel_MouseLeave(object sender, EventArgs e) private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e) { - //発言詳細のアイコン右クリック時のメニュー制御 + // 発言詳細のアイコン右クリック時のメニュー制御 if (this.CurrentPost != null) { var name = this.CurrentPost.ImageUrl; @@ -599,34 +652,34 @@ private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e) { if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId) { - FollowToolStripMenuItem.Enabled = false; - UnFollowToolStripMenuItem.Enabled = false; - ShowFriendShipToolStripMenuItem.Enabled = false; - ShowUserStatusToolStripMenuItem.Enabled = true; - SearchPostsDetailNameToolStripMenuItem.Enabled = true; - SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; - ListManageUserContextToolStripMenuItem3.Enabled = true; + this.FollowToolStripMenuItem.Enabled = false; + this.UnFollowToolStripMenuItem.Enabled = false; + this.ShowFriendShipToolStripMenuItem.Enabled = false; + this.ShowUserStatusToolStripMenuItem.Enabled = true; + this.SearchPostsDetailNameToolStripMenuItem.Enabled = true; + this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; + this.ListManageUserContextToolStripMenuItem3.Enabled = true; } else { - FollowToolStripMenuItem.Enabled = true; - UnFollowToolStripMenuItem.Enabled = true; - ShowFriendShipToolStripMenuItem.Enabled = true; - ShowUserStatusToolStripMenuItem.Enabled = true; - SearchPostsDetailNameToolStripMenuItem.Enabled = true; - SearchAtPostsDetailNameToolStripMenuItem.Enabled = true; - ListManageUserContextToolStripMenuItem3.Enabled = true; + this.FollowToolStripMenuItem.Enabled = true; + this.UnFollowToolStripMenuItem.Enabled = true; + this.ShowFriendShipToolStripMenuItem.Enabled = true; + this.ShowUserStatusToolStripMenuItem.Enabled = true; + this.SearchPostsDetailNameToolStripMenuItem.Enabled = true; + this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true; + this.ListManageUserContextToolStripMenuItem3.Enabled = true; } } else { - FollowToolStripMenuItem.Enabled = false; - UnFollowToolStripMenuItem.Enabled = false; - ShowFriendShipToolStripMenuItem.Enabled = false; - ShowUserStatusToolStripMenuItem.Enabled = false; - SearchPostsDetailNameToolStripMenuItem.Enabled = false; - SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; - ListManageUserContextToolStripMenuItem3.Enabled = false; + this.FollowToolStripMenuItem.Enabled = false; + this.UnFollowToolStripMenuItem.Enabled = false; + this.ShowFriendShipToolStripMenuItem.Enabled = false; + this.ShowUserStatusToolStripMenuItem.Enabled = false; + this.SearchPostsDetailNameToolStripMenuItem.Enabled = false; + this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; + this.ListManageUserContextToolStripMenuItem3.Enabled = false; } } @@ -691,11 +744,12 @@ private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, Event private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e) { - var imageUrl = this.CurrentPost?.ImageUrl; - if (MyCommon.IsNullOrEmpty(imageUrl)) + var imageNormalUrl = this.CurrentPost?.ImageUrl; + if (MyCommon.IsNullOrEmpty(imageNormalUrl)) return; - await MyCommon.OpenInBrowserAsync(this, imageUrl.Remove(imageUrl.LastIndexOf("_normal", StringComparison.Ordinal), 7)); // "_normal".Length + var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original"); + await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl); } private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e) @@ -735,7 +789,7 @@ private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e) } catch (Exception) { - //処理中にキャッシュアウトする可能性あり + // 処理中にキャッシュアウトする可能性あり } } } @@ -747,68 +801,68 @@ private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e) private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e) { // URLコピーの項目の表示/非表示 - if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)) + if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)) { - this._postBrowserStatusText = PostBrowser.StatusText; - var name = GetUserId(); - UrlCopyContextMenuItem.Enabled = true; + this.postBrowserStatusText = this.PostBrowser.StatusText; + var name = this.GetUserId(); + this.UrlCopyContextMenuItem.Enabled = true; if (name != null) { - FollowContextMenuItem.Enabled = true; - RemoveContextMenuItem.Enabled = true; - FriendshipContextMenuItem.Enabled = true; - ShowUserStatusContextMenuItem.Enabled = true; - SearchPostsDetailToolStripMenuItem.Enabled = true; - IdFilterAddMenuItem.Enabled = true; - ListManageUserContextToolStripMenuItem.Enabled = true; - SearchAtPostsDetailToolStripMenuItem.Enabled = true; + this.FollowContextMenuItem.Enabled = true; + this.RemoveContextMenuItem.Enabled = true; + this.FriendshipContextMenuItem.Enabled = true; + this.ShowUserStatusContextMenuItem.Enabled = true; + this.SearchPostsDetailToolStripMenuItem.Enabled = true; + this.IdFilterAddMenuItem.Enabled = true; + this.ListManageUserContextToolStripMenuItem.Enabled = true; + this.SearchAtPostsDetailToolStripMenuItem.Enabled = true; } else { - FollowContextMenuItem.Enabled = false; - RemoveContextMenuItem.Enabled = false; - FriendshipContextMenuItem.Enabled = false; - ShowUserStatusContextMenuItem.Enabled = false; - SearchPostsDetailToolStripMenuItem.Enabled = false; - IdFilterAddMenuItem.Enabled = false; - ListManageUserContextToolStripMenuItem.Enabled = false; - SearchAtPostsDetailToolStripMenuItem.Enabled = false; + this.FollowContextMenuItem.Enabled = false; + this.RemoveContextMenuItem.Enabled = false; + this.FriendshipContextMenuItem.Enabled = false; + this.ShowUserStatusContextMenuItem.Enabled = false; + this.SearchPostsDetailToolStripMenuItem.Enabled = false; + this.IdFilterAddMenuItem.Enabled = false; + this.ListManageUserContextToolStripMenuItem.Enabled = false; + this.SearchAtPostsDetailToolStripMenuItem.Enabled = false; } - if (Regex.IsMatch(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23")) - UseHashtagMenuItem.Enabled = true; + if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23")) + this.UseHashtagMenuItem.Enabled = true; else - UseHashtagMenuItem.Enabled = false; + this.UseHashtagMenuItem.Enabled = false; } else { - this._postBrowserStatusText = ""; - UrlCopyContextMenuItem.Enabled = false; - FollowContextMenuItem.Enabled = false; - RemoveContextMenuItem.Enabled = false; - FriendshipContextMenuItem.Enabled = false; - ShowUserStatusContextMenuItem.Enabled = false; - SearchPostsDetailToolStripMenuItem.Enabled = false; - SearchAtPostsDetailToolStripMenuItem.Enabled = false; - UseHashtagMenuItem.Enabled = false; - IdFilterAddMenuItem.Enabled = false; - ListManageUserContextToolStripMenuItem.Enabled = false; + this.postBrowserStatusText = ""; + this.UrlCopyContextMenuItem.Enabled = false; + this.FollowContextMenuItem.Enabled = false; + this.RemoveContextMenuItem.Enabled = false; + this.FriendshipContextMenuItem.Enabled = false; + this.ShowUserStatusContextMenuItem.Enabled = false; + this.SearchPostsDetailToolStripMenuItem.Enabled = false; + this.SearchAtPostsDetailToolStripMenuItem.Enabled = false; + this.UseHashtagMenuItem.Enabled = false; + this.IdFilterAddMenuItem.Enabled = false; + this.ListManageUserContextToolStripMenuItem.Enabled = false; } // 文字列選択されていないときは選択文字列関係の項目を非表示に - var _selText = this.PostBrowser.GetSelectedText(); - if (_selText == null) + var selText = this.PostBrowser.GetSelectedText(); + if (selText == null) { - SelectionSearchContextMenuItem.Enabled = false; - SelectionCopyContextMenuItem.Enabled = false; - SelectionTranslationToolStripMenuItem.Enabled = false; + this.SelectionSearchContextMenuItem.Enabled = false; + this.SelectionCopyContextMenuItem.Enabled = false; + this.SelectionTranslationToolStripMenuItem.Enabled = false; } else { - SelectionSearchContextMenuItem.Enabled = true; - SelectionCopyContextMenuItem.Enabled = true; - SelectionTranslationToolStripMenuItem.Enabled = true; + this.SelectionSearchContextMenuItem.Enabled = true; + this.SelectionCopyContextMenuItem.Enabled = true; + this.SelectionTranslationToolStripMenuItem.Enabled = true; } - //発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に + // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?"""); var fAllFlag = false; foreach (Match mu in ma) @@ -822,9 +876,9 @@ private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e) this.FriendshipAllMenuItem.Enabled = fAllFlag; if (this.CurrentPost == null) - TranslationToolStripMenuItem.Enabled = false; + this.TranslationToolStripMenuItem.Enabled = false; else - TranslationToolStripMenuItem.Enabled = true; + this.TranslationToolStripMenuItem.Enabled = true; e.Cancel = false; } @@ -840,17 +894,18 @@ private async void SearchPublicSearchContextMenuItem_Click(object sender, EventA private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e) { - //発言詳細の選択文字列で現在のタブを検索 - var _selText = this.PostBrowser.GetSelectedText(); + // 発言詳細の選択文字列で現在のタブを検索 + var selText = this.PostBrowser.GetSelectedText(); - if (_selText != null) + if (selText != null) { var searchOptions = new SearchWordDialog.SearchOptions( SearchWordDialog.SearchType.Timeline, - _selText, - newTab: false, - caseSensitive: false, - useRegex: false); + selText, + NewTab: false, + CaseSensitive: false, + UseRegex: false + ); this.Owner.SearchDialog.ResultOptions = searchOptions; @@ -864,11 +919,11 @@ private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e) private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e) { - //発言詳細で「選択文字列をコピー」 - var _selText = this.PostBrowser.GetSelectedText(); + // 発言詳細で「選択文字列をコピー」 + var selText = this.PostBrowser.GetSelectedText(); try { - Clipboard.SetDataObject(_selText, false, 5, 100); + Clipboard.SetDataObject(selText, false, 5, 100); } catch (Exception ex) { @@ -882,7 +937,7 @@ private void UrlCopyContextMenuItem_Click(object sender, EventArgs e) { foreach (var link in this.PostBrowser.Document.Links.Cast()) { - if (link.GetAttribute("href") == this._postBrowserStatusText) + if (link.GetAttribute("href") == this.postBrowserStatusText) { var linkStr = link.GetAttribute("title"); if (MyCommon.IsNullOrEmpty(linkStr)) @@ -893,7 +948,7 @@ private void UrlCopyContextMenuItem_Click(object sender, EventArgs e) } } - Clipboard.SetDataObject(this._postBrowserStatusText, false, 5, 100); + Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100); } catch (Exception ex) { @@ -906,21 +961,21 @@ private void SelectionAllContextMenuItem_Click(object sender, EventArgs e) private async void FollowContextMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) await this.Owner.FollowCommand(name); } private async void RemoveContextMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) await this.Owner.RemoveCommand(name, false); } private async void FriendshipContextMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) await this.Owner.ShowFriendship(name); } @@ -942,27 +997,27 @@ private async void FriendshipAllMenuItem_Click(object sender, EventArgs e) private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) await this.Owner.ShowUserStatus(name); } private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) await this.Owner.AddNewTabForUserTimeline(name); } private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) this.Owner.AddNewTabForSearch("@" + name); } private void IdFilterAddMenuItem_Click(object sender, EventArgs e) { - var name = GetUserId(); + var name = this.GetUserId(); if (name != null) this.Owner.AddFilterRuleByScreenName(name); } @@ -974,7 +1029,7 @@ private void ListManageUserContextToolStripMenuItem_Click(object sender, EventAr string? user; if (menuItem.Owner == this.ContextMenuPostBrowser) { - user = GetUserId(); + user = this.GetUserId(); if (user == null) return; } else if (this.CurrentPost != null) @@ -991,7 +1046,7 @@ private void ListManageUserContextToolStripMenuItem_Click(object sender, EventAr private void UseHashtagMenuItem_Click(object sender, EventArgs e) { - var m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?.+)$"); + var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?.+)$"); if (m.Success) this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value)); } @@ -1013,13 +1068,13 @@ private void ContextMenuSource_Opening(object sender, CancelEventArgs e) { if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm) { - SourceCopyMenuItem.Enabled = false; - SourceUrlCopyMenuItem.Enabled = false; + this.SourceCopyMenuItem.Enabled = false; + this.SourceUrlCopyMenuItem.Enabled = false; } else { - SourceCopyMenuItem.Enabled = true; - SourceUrlCopyMenuItem.Enabled = true; + this.SourceCopyMenuItem.Enabled = true; + this.SourceUrlCopyMenuItem.Enabled = true; } } @@ -1055,6 +1110,20 @@ private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e) } #endregion + + private async void AuthorNameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + var screenName = this.CurrentPost?.ScreenName; + if (screenName != null) + await this.Owner.ShowUserStatus(screenName, showInputDialog: false); + } + + private async void RetweetedByLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + var screenName = this.CurrentPost?.RetweetedBy; + if (screenName != null) + await this.Owner.ShowUserStatus(screenName, showInputDialog: false); + } } public class TweetDetailsViewStatusChengedEventArgs : EventArgs diff --git a/OpenTween/TweetDetailsView.resx b/OpenTween/TweetDetailsView.resx index 89f0daae2..f7c950d4e 100644 --- a/OpenTween/TweetDetailsView.resx +++ b/OpenTween/TweetDetailsView.resx @@ -12,6 +12,10 @@ 511, 85 TweetDetailsView System.Windows.Forms.UserControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + AuthorNameLinkLabel + flowLayoutPanel1 + System.Windows.Forms.LinkLabel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 0 ContextMenuPostBrowser System.Windows.Forms.ContextMenuStrip, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ContextMenuSource @@ -24,6 +28,10 @@ TableLayoutPanel1 System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 3 + flowLayoutPanel1 + TableLayoutPanel1 + System.Windows.Forms.FlowLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 4 FollowContextMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 FollowToolStripMenuItem @@ -40,10 +48,6 @@ System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ListManageUserContextToolStripMenuItem3 System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - NameLabel - TableLayoutPanel1 - System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 1 PostBrowser TableLayoutPanel1 System.Windows.Forms.WebBrowser, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -52,6 +56,10 @@ System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 RemoveContextMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + RetweetedByLinkLabel + flowLayoutPanel1 + System.Windows.Forms.LinkLabel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + 1 SaveIconPictureToolStripMenuItem System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 SearchAtPostsDetailNameToolStripMenuItem @@ -120,6 +128,15 @@ TableLayoutPanel1 OpenTween.OTPictureBox, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null 0 + Bottom + True + MS UI Gothic, 9pt, style=Bold + 3, 3 + 3, 3, 0, 0 + 135, 12 + 0 + AuthorNameLinkLabel + MiddleLeft 232, 386 17, 17 168, 48 @@ -133,10 +150,18 @@ Off 405, 3 3, 3, 3, 0 - 38, 14 + 38, 12 1 Label1 MiddleRight + True + GrowAndShrink + Fill + 56, 0 + 0, 0, 0, 0 + 346, 15 + 0 + False 231, 22 フォローする(&F) 231, 22 @@ -153,15 +178,6 @@ リスト管理(&L) 231, 22 リスト管理(&L) - Fill - MS UI Gothic, 9pt, style=Bold - Off - 59, 3 - 3, 3, 3, 0 - 340, 14 - 1 - LblName - MiddleLeft 発言本文 Fill 59, 20 @@ -171,6 +187,16 @@ 再読み込み(&R) 231, 22 フォロー解除(&N) + Bottom + True + MS UI Gothic, 9pt, style=Bold + NoControl + 141, 3 + 3, 3, 0, 0 + 141, 12 + 1 + RetweetedByLinkLabel + MiddleLeft 231, 22 保存(&I)... 231, 22 @@ -218,7 +244,7 @@ 4 Fill - <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="UserPicture" Row="0" RowSpan="2" Column="0" ColumnSpan="1" /><Control Name="NameLabel" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="PostBrowser" Row="1" RowSpan="1" Column="1" ColumnSpan="3" /><Control Name="DateTimeLabel" Row="0" RowSpan="1" Column="2" ColumnSpan="1" /><Control Name="SourceLinkLabel" Row="0" RowSpan="1" Column="3" ColumnSpan="1" /></Controls><Columns Styles="Absolute,56,Percent,100,AutoSize,0,AutoSize,0" /><Rows Styles="AutoSize,0,Percent,100" /></TableLayoutSettings> + <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="UserPicture" Row="0" RowSpan="2" Column="0" ColumnSpan="1" /><Control Name="flowLayoutPanel1" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="PostBrowser" Row="1" RowSpan="1" Column="1" ColumnSpan="3" /><Control Name="DateTimeLabel" Row="0" RowSpan="1" Column="2" ColumnSpan="1" /><Control Name="SourceLinkLabel" Row="0" RowSpan="1" Column="3" ColumnSpan="1" /></Controls><Columns Styles="Absolute,56,Percent,100,AutoSize,0,AutoSize,0" /><Rows Styles="AutoSize,0,Percent,100" /></TableLayoutSettings> 0, 0 2 511, 85 diff --git a/OpenTween/TweetExtractor.cs b/OpenTween/TweetExtractor.cs index 97e0d7b3a..da822153d 100644 --- a/OpenTween/TweetExtractor.cs +++ b/OpenTween/TweetExtractor.cs @@ -34,7 +34,7 @@ namespace OpenTween { public static class TweetExtractor { - public static readonly Regex EmojiPattern = new Regex(@"(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f"); + public static readonly Regex EmojiPattern = new(@"(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef0-\udef6]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedd-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7c\ude80-\ude86\ude90-\udeac\udeb0-\udeba\udec0-\udec2\uded0-\uded9\udee0-\udee7]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f"); /// /// テキストから URL を抽出して返します @@ -47,7 +47,7 @@ public static IEnumerable ExtractUrls(string text) /// public static IEnumerable ExtractUrlEntities(string text) { - var urlMatches = Regex.Matches(text, Twitter.rgUrl, RegexOptions.IgnoreCase).Cast(); + var urlMatches = Regex.Matches(text, Twitter.RgUrl, RegexOptions.IgnoreCase).Cast(); foreach (var m in urlMatches) { var before = m.Groups["before"].Value; @@ -59,17 +59,15 @@ public static IEnumerable ExtractUrlEntities(string text) var validUrl = false; if (protocol.Length == 0) { - if (Regex.IsMatch(before, Twitter.url_invalid_without_protocol_preceding_chars)) + if (Regex.IsMatch(before, Twitter.UrlInvalidWithoutProtocolPrecedingChars)) continue; - string? lasturl = null; - var last_url_invalid_match = false; - var domainMatches = Regex.Matches(domain, Twitter.url_valid_ascii_domain, RegexOptions.IgnoreCase).Cast(); + var domainMatches = Regex.Matches(domain, Twitter.UrlValidAsciiDomain, RegexOptions.IgnoreCase).Cast(); foreach (var mm in domainMatches) { - lasturl = mm.Value; - last_url_invalid_match = Regex.IsMatch(lasturl, Twitter.url_invalid_short_domain, RegexOptions.IgnoreCase); + var lasturl = mm.Value; + last_url_invalid_match = Regex.IsMatch(lasturl, Twitter.UrlInvalidShortDomain, RegexOptions.IgnoreCase); if (!last_url_invalid_match) { validUrl = true; @@ -142,7 +140,7 @@ public static IEnumerable ExtractMentionEntities(string te /// public static IEnumerable ExtractHashtagEntities(string text) { - var matches = Regex.Matches(text, Twitter.HASHTAG); + var matches = Regex.Matches(text, Twitter.Hashtag); foreach (var match in matches.Cast()) { var groupHashtagSharp = match.Groups[2]; diff --git a/OpenTween/TweetFormatter.cs b/OpenTween/TweetFormatter.cs index a881dee44..6cb8cf389 100644 --- a/OpenTween/TweetFormatter.cs +++ b/OpenTween/TweetFormatter.cs @@ -68,26 +68,24 @@ private static IEnumerable AutoLinkHtmlInternal(string text, IEnumerable continue; // 区間が文字列長を越えている不正なエンティティを無視する if (curIndex != startIndex) - yield return t(e(text.Substring(curIndex, startIndex - curIndex))); + yield return T(E(text.Substring(curIndex, startIndex - curIndex))); var targetText = text.Substring(startIndex, endIndex - startIndex); - if (entity is TwitterEntityUrl urlEntity) - yield return FormatUrlEntity(targetText, urlEntity, keepTco); - else if (entity is TwitterEntityHashtag hashtagEntity) - yield return FormatHashtagEntity(targetText, hashtagEntity); - else if (entity is TwitterEntityMention mentionEntity) - yield return FormatMentionEntity(targetText, mentionEntity); - else if (entity is TwitterEntityEmoji emojiEntity) - yield return FormatEmojiEntity(targetText, emojiEntity); - else - yield return t(e(targetText)); + yield return entity switch + { + TwitterEntityUrl urlEntity => FormatUrlEntity(targetText, urlEntity, keepTco), + TwitterEntityHashtag hashtagEntity => FormatHashtagEntity(targetText, hashtagEntity), + TwitterEntityMention mentionEntity => FormatMentionEntity(targetText, mentionEntity), + TwitterEntityEmoji emojiEntity => FormatEmojiEntity(targetText, emojiEntity), + _ => T(E(targetText)), + }; curIndex = endIndex; } if (curIndex != text.Length) - yield return t(e(text.Substring(curIndex))); + yield return T(E(text.Substring(curIndex))); } /// @@ -104,15 +102,19 @@ private static IEnumerable FixEntityIndices(string text, IEnumera var endIndex = entity.Indices[1]; for (var i = curIndex; i < (startIndex + indexOffset); i++) + { if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1])) indexOffset++; + } startIndex += indexOffset; curIndex = startIndex; for (var i = curIndex; i < (endIndex + indexOffset); i++) + { if (i + 1 < text.Length && char.IsSurrogatePair(text[i], text[i + 1])) indexOffset++; + } endIndex += indexOffset; curIndex = endIndex; @@ -133,7 +135,7 @@ private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity if (entity.DisplayUrl == null) { expandedUrl = MyCommon.ConvertToReadableUrl(targetText); - return "" + t(e(targetText)) + ""; + return "" + T(E(targetText)) + ""; } var linkUrl = entity.Url; @@ -154,30 +156,30 @@ private static string FormatUrlEntity(string targetText, TwitterEntityUrl entity } } - return "" + t(e(entity.DisplayUrl)) + ""; + return "" + T(E(entity.DisplayUrl)) + ""; } private static string FormatHashtagEntity(string targetText, TwitterEntityHashtag entity) - => "" + t(e(targetText)) + ""; + => "" + T(E(targetText)) + ""; private static string FormatMentionEntity(string targetText, TwitterEntityMention entity) - => "" + t(e(targetText)) + ""; + => "" + T(E(targetText)) + ""; private static string FormatEmojiEntity(string targetText, TwitterEntityEmoji entity) { - if (!SettingManager.Local.UseTwemoji) - return t(e(targetText)); + if (!SettingManager.Instance.Local.UseTwemoji) + return T(E(targetText)); if (MyCommon.IsNullOrEmpty(entity.Url)) return ""; - return "\"""; + return "\"""; } // 長いのでエイリアスとして e(...), eu(...), t(...) でエスケープできるようにする - private static readonly Func e = EscapeHtml; - private static readonly Func eu = Uri.EscapeDataString; - private static readonly Func t = FilterText; + private static readonly Func E = EscapeHtml; + private static readonly Func EU = Uri.EscapeDataString; + private static readonly Func T = FilterText; private static string EscapeHtml(string text) { @@ -190,27 +192,15 @@ private static string EscapeHtml(string text) { // 「<」「>」「&」「"」「'」についてエスケープ処理を施す // 参照: http://d.hatena.ne.jp/ockeghem/20070510/1178813849 - switch (c) + result.Append(c switch { - case '<': - result.Append("<"); - break; - case '>': - result.Append(">"); - break; - case '&': - result.Append("&"); - break; - case '"': - result.Append("""); - break; - case '\'': - result.Append("'"); - break; - default: - result.Append(c); - break; - } + '<' => "<", + '>' => ">", + '&' => "&", + '"' => """, + '\'' => "'", + _ => c, + }); } return result.ToString(); diff --git a/OpenTween/TweetThumbnail.Designer.cs b/OpenTween/TweetThumbnail.Designer.cs index 6e49c47ca..702d66159 100644 --- a/OpenTween/TweetThumbnail.Designer.cs +++ b/OpenTween/TweetThumbnail.Designer.cs @@ -49,7 +49,7 @@ private void InitializeComponent() this.scrollBar.Maximum = 0; this.scrollBar.Name = "scrollBar"; this.toolTip.SetToolTip(this.scrollBar, resources.GetString("scrollBar.ToolTip")); - this.scrollBar.ValueChanged += new System.EventHandler(this.scrollBar_ValueChanged); + this.scrollBar.ValueChanged += new System.EventHandler(this.ScrollBar_ValueChanged); // // panelPictureBox // @@ -68,19 +68,19 @@ private void InitializeComponent() this.searchImageSauceNaoMenuItem}); this.contextMenuStrip.Name = "contextMenuStrip"; this.toolTip.SetToolTip(this.contextMenuStrip, resources.GetString("contextMenuStrip.ToolTip")); - this.contextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.contextMenuStrip_Opening); + this.contextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.ContextMenuStrip_Opening); // // openMenuItem // resources.ApplyResources(this.openMenuItem, "openMenuItem"); this.openMenuItem.Name = "openMenuItem"; - this.openMenuItem.Click += new System.EventHandler(this.openMenuItem_Click); + this.openMenuItem.Click += new System.EventHandler(this.OpenMenuItem_Click); // // copyUrlMenuItem // resources.ApplyResources(this.copyUrlMenuItem, "copyUrlMenuItem"); this.copyUrlMenuItem.Name = "copyUrlMenuItem"; - this.copyUrlMenuItem.Click += new System.EventHandler(this.copyUrlMenuItem_Click); + this.copyUrlMenuItem.Click += new System.EventHandler(this.CopyUrlMenuItem_Click); // // toolStripSeparator1 // @@ -91,13 +91,13 @@ private void InitializeComponent() // resources.ApplyResources(this.searchImageGoogleMenuItem, "searchImageGoogleMenuItem"); this.searchImageGoogleMenuItem.Name = "searchImageGoogleMenuItem"; - this.searchImageGoogleMenuItem.Click += new System.EventHandler(this.searchSimilarImageMenuItem_Click); + this.searchImageGoogleMenuItem.Click += new System.EventHandler(this.SearchSimilarImageMenuItem_Click); // // searchImageSauceNaoMenuItem // resources.ApplyResources(this.searchImageSauceNaoMenuItem, "searchImageSauceNaoMenuItem"); this.searchImageSauceNaoMenuItem.Name = "searchImageSauceNaoMenuItem"; - this.searchImageSauceNaoMenuItem.Click += new System.EventHandler(this.searchImageSauceNaoMenuItem_Click); + this.searchImageSauceNaoMenuItem.Click += new System.EventHandler(this.SearchImageSauceNaoMenuItem_Click); // // TweetThumbnail // diff --git a/OpenTween/TweetThumbnail.cs b/OpenTween/TweetThumbnail.cs index 0a255367f..dddb955cb 100644 --- a/OpenTween/TweetThumbnail.cs +++ b/OpenTween/TweetThumbnail.cs @@ -24,38 +24,50 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Data; using System.Diagnostics.CodeAnalysis; using System.Drawing; -using System.Data; using System.Linq; using System.Net; using System.Net.Http; +using System.Runtime.InteropServices; using System.Text; -using System.Windows.Forms; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using OpenTween.Thumbnail; using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; using OpenTween.Models; -using System.Runtime.InteropServices; +using OpenTween.Thumbnail; namespace OpenTween { public partial class TweetThumbnail : UserControl { - protected internal List pictureBox = new List(); - protected MouseWheelMessageFilter filter = new MouseWheelMessageFilter(); + protected internal List PictureBox = new(); + protected MouseWheelMessageFilter filter = new(); + private ThumbnailGenerator? thumbGenerator; public event EventHandler? ThumbnailLoading; + public event EventHandler? ThumbnailDoubleClick; + public event EventHandler? ThumbnailImageSearchClick; public ThumbnailInfo Thumbnail - => (ThumbnailInfo)this.pictureBox[this.scrollBar.Value].Tag; + => (ThumbnailInfo)this.PictureBox[this.scrollBar.Value].Tag; + + private ThumbnailGenerator ThumbGenerator + => this.thumbGenerator ?? throw this.NotInitializedException(); public TweetThumbnail() => this.InitializeComponent(); + public void Initialize(ThumbnailGenerator thumbnailGenerator) + => this.thumbGenerator = thumbnailGenerator; + + private Exception NotInitializedException() + => new InvalidOperationException("Cannot call before initialization"); + public Task ShowThumbnailAsync(PostClass post) => this.ShowThumbnailAsync(post, CancellationToken.None); @@ -84,7 +96,7 @@ public async Task ShowThumbnailAsync(PostClass post, CancellationToken cancelTok for (var i = 0; i < thumbnails.Length; i++) { var thumb = thumbnails[i]; - var picbox = this.pictureBox[i]; + var picbox = this.PictureBox[i]; picbox.Tag = thumb; picbox.ContextMenuStrip = this.contextMenuStrip; @@ -120,7 +132,7 @@ private string GetImageSearchUriSauceNao(string imageUri) => @"https://saucenao.com/search.php?url=" + Uri.EscapeDataString(imageUri); protected virtual Task> GetThumbailInfoAsync(PostClass post, CancellationToken token) - => ThumbnailGenerator.GetThumbnailsAsync(post, token); + => this.ThumbGenerator.GetThumbnailsAsync(post, token); /// /// 表示するサムネイルの数を設定する @@ -128,20 +140,20 @@ protected virtual Task> GetThumbailInfoAsync(PostClas /// 表示するサムネイルの数 protected void SetThumbnailCount(int count) { - if (count == 0 && this.pictureBox.Count == 0) + if (count == 0 && this.PictureBox.Count == 0) return; using (ControlTransaction.Layout(this.panelPictureBox, false)) { this.panelPictureBox.Controls.Clear(); - foreach (var picbox in this.pictureBox) + foreach (var picbox in this.PictureBox) { var memoryImage = picbox.Image; - filter.Unregister(picbox); + this.filter.Unregister(picbox); - picbox.MouseWheel -= this.pictureBox_MouseWheel; - picbox.DoubleClick -= this.pictureBox_DoubleClick; + picbox.MouseWheel -= this.PictureBox_MouseWheel; + picbox.DoubleClick -= this.PictureBox_DoubleClick; picbox.Dispose(); memoryImage?.Dispose(); @@ -149,22 +161,22 @@ protected void SetThumbnailCount(int count) // メモリリーク対策 (http://stackoverflow.com/questions/2792427#2793714) picbox.ContextMenuStrip = null; } - this.pictureBox.Clear(); + this.PictureBox.Clear(); this.scrollBar.Maximum = (count > 0) ? count - 1 : 0; this.scrollBar.Value = 0; for (var i = 0; i < count; i++) { - var picbox = CreatePictureBox("pictureBox" + i); - picbox.Visible = (i == 0); - picbox.MouseWheel += this.pictureBox_MouseWheel; - picbox.DoubleClick += this.pictureBox_DoubleClick; + var picbox = this.CreatePictureBox("pictureBox" + i); + picbox.Visible = i == 0; + picbox.MouseWheel += this.PictureBox_MouseWheel; + picbox.DoubleClick += this.PictureBox_DoubleClick; - filter.Register(picbox); + this.filter.Register(picbox); this.panelPictureBox.Controls.Add(picbox); - this.pictureBox.Add(picbox); + this.PictureBox.Add(picbox); } } } @@ -211,19 +223,19 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) OTBaseForm.ScaleChildControl(this.scrollBar, factor); } - private void scrollBar_ValueChanged(object sender, EventArgs e) + private void ScrollBar_ValueChanged(object sender, EventArgs e) { using (ControlTransaction.Layout(this, false)) { var value = this.scrollBar.Value; - for (var i = 0; i < this.pictureBox.Count; i++) + for (var i = 0; i < this.PictureBox.Count; i++) { - this.pictureBox[i].Visible = (i == value); + this.PictureBox[i].Visible = i == value; } } } - private void pictureBox_MouseWheel(object sender, MouseEventArgs e) + private void PictureBox_MouseWheel(object sender, MouseEventArgs e) { if (e.Delta > 0) this.ScrollUp(); @@ -231,13 +243,13 @@ private void pictureBox_MouseWheel(object sender, MouseEventArgs e) this.ScrollDown(); } - private void pictureBox_DoubleClick(object sender, EventArgs e) + private void PictureBox_DoubleClick(object sender, EventArgs e) { if (((PictureBox)sender).Tag is ThumbnailInfo thumb) this.OpenImage(thumb); } - private void contextMenuStrip_Opening(object sender, CancelEventArgs e) + private void ContextMenuStrip_Opening(object sender, CancelEventArgs e) { var picbox = (OTPictureBox)this.contextMenuStrip.SourceControl; var thumb = (ThumbnailInfo)picbox.Tag; @@ -257,7 +269,7 @@ private void contextMenuStrip_Opening(object sender, CancelEventArgs e) } } - private void searchSimilarImageMenuItem_Click(object sender, EventArgs e) + private void SearchSimilarImageMenuItem_Click(object sender, EventArgs e) { var searchTargetUri = (string)this.searchImageGoogleMenuItem.Tag; var searchUri = this.GetImageSearchUriGoogle(searchTargetUri); @@ -265,7 +277,7 @@ private void searchSimilarImageMenuItem_Click(object sender, EventArgs e) this.ThumbnailImageSearchClick?.Invoke(this, new ThumbnailImageSearchEventArgs(searchUri)); } - private void searchImageSauceNaoMenuItem_Click(object sender, EventArgs e) + private void SearchImageSauceNaoMenuItem_Click(object sender, EventArgs e) { var searchTargetUri = (string)this.searchImageSauceNaoMenuItem.Tag; var searchUri = this.GetImageSearchUriSauceNao(searchTargetUri); @@ -273,10 +285,10 @@ private void searchImageSauceNaoMenuItem_Click(object sender, EventArgs e) this.ThumbnailImageSearchClick?.Invoke(this, new ThumbnailImageSearchEventArgs(searchUri)); } - private void openMenuItem_Click(object sender, EventArgs e) + private void OpenMenuItem_Click(object sender, EventArgs e) => this.OpenImage(this.Thumbnail); - private void copyUrlMenuItem_Click(object sender, EventArgs e) + private void CopyUrlMenuItem_Click(object sender, EventArgs e) { try { diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index 92eb41e75..1eab17e64 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -27,26 +27,26 @@ #nullable enable +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System; -using System.Reflection; -using System.Collections.Generic; using System.Windows.Forms; using OpenTween.Api; using OpenTween.Api.DataModel; +using OpenTween.Api.TwitterV2; using OpenTween.Connection; using OpenTween.Models; using OpenTween.Setting; -using System.Globalization; namespace OpenTween { @@ -71,47 +71,48 @@ public class Twitter : IDisposable // implied. See the License for the specific language governing // permissions and limitations under the License. - //Hashtag用正規表現 - private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff"; - private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF"; - private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}"; - private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!"; - private const string HASHTAG_ALPHA = "[A-Za-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]"; - private const string HASHTAG_ALPHANUMERIC = "[A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]"; - private const string HASHTAG_TERMINATOR = "[^A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]"; - public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")"; - //URL正規表現 - private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)"; - public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$"; - private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e"; - private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]"; - private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)"; - private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)"; - private const string url_valid_GTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))"; - private const string url_valid_CCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))"; - private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)"; - private const string url_valid_domain = @"(?" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")"; - public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")"; - public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$"; - private const string url_valid_port_number = @"[0-9]+"; - - private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]"; - private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))"; - private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")"; - private const string pth = "(?:" + + // Hashtag用正規表現 + private const string LatinAccents = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff"; + private const string NonLatinHashtagChars = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF"; + private const string CJHashtagCharacters = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}"; + private const string HashtagBoundary = @"^|$|\s|「|」|。|\.|!"; + private const string HashtagAlpha = $"[A-Za-z_{LatinAccents}{NonLatinHashtagChars}{CJHashtagCharacters}]"; + private const string HashtagAlphanumeric = $"[A-Za-z0-9_{LatinAccents}{NonLatinHashtagChars}{CJHashtagCharacters}]"; + private const string HashtagTerminator = $"[^A-Za-z0-9_{LatinAccents}{NonLatinHashtagChars}{CJHashtagCharacters}]"; + public const string Hashtag = $"({HashtagBoundary})(#|#)({HashtagAlphanumeric}*{HashtagAlpha}{HashtagAlphanumeric}*)(?={HashtagTerminator}|{HashtagBoundary})"; + // URL正規表現 + private const string UrlValidPrecedingChars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)"; + public const string UrlInvalidWithoutProtocolPrecedingChars = @"[-_./]$"; + private const string UrlInvalidDomainChars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e"; + private const string UrlValidDomainChars = $@"[^{UrlInvalidDomainChars}]"; + private const string UrlValidSubdomain = $@"(?:(?:{UrlValidDomainChars}(?:[_-]|{UrlValidDomainChars})*)?{UrlValidDomainChars}\.)"; + private const string UrlValidDomainName = $@"(?:(?:{UrlValidDomainChars}(?:-|{UrlValidDomainChars})*)?{UrlValidDomainChars}\.)"; + private const string UrlValidGTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))"; + private const string UrlValidCCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))"; + private const string UrlValidPunycode = @"(?:xn--[0-9a-z]+)"; + private const string UrlValidDomain = $@"(?{UrlValidSubdomain}*{UrlValidDomainName}(?:{UrlValidGTLD}|{UrlValidCCTLD})|{UrlValidPunycode})"; + public const string UrlValidAsciiDomain = $@"(?:(?:[a-z0-9{LatinAccents}]+)\.)+(?:{UrlValidGTLD}|{UrlValidCCTLD}|{UrlValidPunycode})"; + public const string UrlInvalidShortDomain = $"^{UrlValidDomainName}{UrlValidCCTLD}$"; + private const string UrlValidPortNumber = @"[0-9]+"; + + private const string UrlValidGeneralPathChars = $@"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&{LatinAccents}]"; + private const string UrlBalanceParens = $@"(?:\({UrlValidGeneralPathChars}+\))"; + private const string UrlValidPathEndingChars = $@"(?:[+\-a-z0-9=_#/{LatinAccents}]|{UrlBalanceParens})"; + private const string Pth = "(?:" + "(?:" + - url_valid_general_path_chars + "*" + - "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" + - url_valid_path_ending_chars + - ")|(?:@" + url_valid_general_path_chars + "+/)" + + $"{UrlValidGeneralPathChars}*" + + $"(?:{UrlBalanceParens}{UrlValidGeneralPathChars}*)*" + + UrlValidPathEndingChars + + $")|(?:@{UrlValidGeneralPathChars}+/)" + ")"; - private const string qry = @"(?\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?"; - public const string rgUrl = @"(?" + url_valid_preceding_chars + ")" + + + private const string Qry = @"(?\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?"; + public const string RgUrl = $@"(?{UrlValidPrecedingChars})" + "(?(?https?://)?" + - "(?" + url_valid_domain + ")" + - "(?::" + url_valid_port_number + ")?" + - "(?/" + pth + "*)?" + - qry + + $"(?{UrlValidDomain})" + + $"(?::{UrlValidPortNumber})?" + + $"(?/{Pth}*)?" + + Qry + ")"; #endregion @@ -119,51 +120,58 @@ public class Twitter : IDisposable /// /// Twitter API のステータスページのURL /// - public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617"; + public const string ServiceAvailabilityStatusUrl = "https://api.twitterstat.us/"; /// /// ツイートへのパーマリンクURLを判定する正規表現 /// - public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?[a-zA-Z0-9_]+)/status(es)?/(?[0-9]+)(/photo)?", RegexOptions.IgnoreCase); + public static readonly Regex StatusUrlRegex = new(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?[a-zA-Z0-9_]+)/status(es)?/(?[0-9]+)(/photo)?", RegexOptions.IgnoreCase); /// /// attachment_url に指定可能な URL を判定する正規表現 /// - public static readonly Regex AttachmentUrlRegex = new Regex(@"https?://( + public static readonly Regex AttachmentUrlRegex = new( + @"https?://( twitter\.com/[0-9A-Za-z_]+/status/[0-9]+ | mobile\.twitter\.com/[0-9A-Za-z_]+/status/[0-9]+ | twitter\.com/messages/compose\?recipient_id=[0-9]+(&.+)? -)$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); +)$", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); /// /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現 /// - public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?: + public static readonly Regex ThirdPartyStatusUrlRegex = new( + @"https?://(?:[^.]+\.)?(?: favstar\.fm/users/[a-zA-Z0-9_]+/status/ # Favstar | favstar\.fm/t/ # Favstar (short) | aclog\.koba789\.com/i/ # aclog | frtrt\.net/solo_status\.php\?status= # RtRT -)(?[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); +)(?[0-9]+)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); /// /// DM送信かどうかを判定する正規表現 /// - public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?[a-zA-Z0-9_]+) +(?.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline); + public static readonly Regex DMSendTextRegex = new(@"^DM? +(?[a-zA-Z0-9_]+) +(?.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline); public TwitterApi Api { get; } + public TwitterConfiguration Configuration { get; private set; } + public TwitterTextConfiguration TextConfiguration { get; private set; } public bool GetFollowersSuccess { get; private set; } = false; + public bool GetNoRetweetSuccess { get; private set; } = false; - delegate void GetIconImageDelegate(PostClass post); - private readonly object LockObj = new object(); + private delegate void GetIconImageDelegate(PostClass post); + + private readonly object lockObj = new(); private ISet followerId = new HashSet(); private long[] noRTId = Array.Empty(); - //プロパティからアクセスされる共通情報 - private readonly List _hashList = new List(); + private readonly TwitterPostFactory postFactory; private string? nextCursorDirectMessage = null; @@ -171,6 +179,8 @@ public class Twitter : IDisposable public Twitter(TwitterApi api) { + this.postFactory = new(TabInformations.GetInstance()); + this.Api = api; this.Configuration = TwitterConfiguration.DefaultConfiguration(); this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration(); @@ -188,7 +198,6 @@ public void ClearAuthInfo() this.ResetApiStatus(); } - [Obsolete] public void VerifyCredentials() { try @@ -211,7 +220,7 @@ public async Task VerifyCredentialsAsync() public void Initialize(string token, string tokenSecret, string username, long userId) { - //OAuth認証 + // OAuth認証 if (MyCommon.IsNullOrEmpty(token) || MyCommon.IsNullOrEmpty(tokenSecret) || MyCommon.IsNullOrEmpty(username)) { Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid; @@ -220,44 +229,6 @@ public void Initialize(string token, string tokenSecret, string username, long u this.Api.Initialize(token, tokenSecret, userId, username); } - internal static string PreProcessUrl(string orgData) - { - int posl1; - var posl2 = 0; - var href = " -1) - { - // IDN展開 - posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal); - posl1 += href.Length; - posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal); - var urlStr = orgData.Substring(posl1, posl2 - posl1); - - if (!urlStr.StartsWith("http://", StringComparison.Ordinal) - && !urlStr.StartsWith("https://", StringComparison.Ordinal) - && !urlStr.StartsWith("ftp://", StringComparison.Ordinal)) - { - continue; - } - - var replacedUrl = MyCommon.IDNEncode(urlStr); - if (replacedUrl == null) continue; - if (replacedUrl == urlStr) continue; - - orgData = orgData.Replace(" PostStatus(PostStatusParams param) { this.CheckAccountState(); @@ -271,8 +242,14 @@ await this.SendDirectMessage(param.Text, mediaId) return null; } - var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds, - param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl) + var response = await this.Api.StatusesUpdate( + param.Text, + param.InReplyToStatusId, + param.MediaIds, + param.AutoPopulateReplyMetadata, + param.ExcludeReplyUserIds, + param.AttachmentUrl + ) .ConfigureAwait(false); var status = await response.LoadJsonAsync() @@ -285,8 +262,8 @@ await this.SendDirectMessage(param.Text, mediaId) this.previousStatusId = status.Id; - //投稿したものを返す - var post = CreatePostsFromStatusData(status); + // 投稿したものを返す + var post = this.CreatePostsFromStatusData(status); if (this.ReadOwnPost) post.IsRead = true; return post; } @@ -375,12 +352,12 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) { this.CheckAccountState(); - //データ部分の生成 + // データ部分の生成 var post = TabInformations.GetInstance()[id]; if (post == null) throw new WebApiException("Err:Target isn't found."); - var target = post.RetweetedId ?? id; //再RTの場合は元発言をRT + var target = post.RetweetedId ?? id; // 再RTの場合は元発言をRT var response = await this.Api.StatusesRetweet(target) .ConfigureAwait(false); @@ -388,21 +365,21 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) var status = await response.LoadJsonAsync() .ConfigureAwait(false); - //二重取得回避 - lock (LockObj) + // 二重取得回避 + lock (this.lockObj) { if (TabInformations.GetInstance().ContainsKey(status.Id)) return null; } - //Retweet判定 + // Retweet判定 if (status.RetweetedStatus == null) throw new WebApiException("Invalid Json!"); - //Retweetしたものを返す - post = CreatePostsFromStatusData(status); + // Retweetしたものを返す + post = this.CreatePostsFromStatusData(status); - //ユーザー情報 + // ユーザー情報 post.IsMe = true; post.IsRead = read; @@ -420,13 +397,19 @@ public long UserId => this.Api.CurrentUserId; public static MyCommon.ACCOUNT_STATE AccountState { get; set; } = MyCommon.ACCOUNT_STATE.Valid; + public bool RestrictFavCheck { get; set; } + public bool ReadOwnPost { get; set; } public int FollowersCount { get; private set; } + public int FriendsCount { get; private set; } + public int StatusesCount { get; private set; } + public string Location { get; private set; } = ""; + public string Bio { get; private set; } = ""; /// ユーザーのフォロワー数などの情報を更新します @@ -466,7 +449,7 @@ public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type) // https://dev.twitter.com/rest/public return type switch { - MyCommon.WORKERTYPE.Timeline => 200, + MyCommon.WORKERTYPE.Timeline => 100, MyCommon.WORKERTYPE.Reply => 200, MyCommon.WORKERTYPE.UserTimeline => 200, MyCommon.WORKERTYPE.Favorites => 200, @@ -481,42 +464,42 @@ public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type) /// public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup) { - if (SettingManager.Common.UseAdditionalCount) + if (SettingManager.Instance.Common.UseAdditionalCount) { switch (type) { case MyCommon.WORKERTYPE.Favorites: - if (SettingManager.Common.FavoritesCountApi != 0) - return SettingManager.Common.FavoritesCountApi; + if (SettingManager.Instance.Common.FavoritesCountApi != 0) + return SettingManager.Instance.Common.FavoritesCountApi; break; case MyCommon.WORKERTYPE.List: - if (SettingManager.Common.ListCountApi != 0) - return SettingManager.Common.ListCountApi; + if (SettingManager.Instance.Common.ListCountApi != 0) + return SettingManager.Instance.Common.ListCountApi; break; case MyCommon.WORKERTYPE.PublicSearch: - if (SettingManager.Common.SearchCountApi != 0) - return SettingManager.Common.SearchCountApi; + if (SettingManager.Instance.Common.SearchCountApi != 0) + return SettingManager.Instance.Common.SearchCountApi; break; case MyCommon.WORKERTYPE.UserTimeline: - if (SettingManager.Common.UserTimelineCountApi != 0) - return SettingManager.Common.UserTimelineCountApi; + if (SettingManager.Instance.Common.UserTimelineCountApi != 0) + return SettingManager.Instance.Common.UserTimelineCountApi; break; } - if (more && SettingManager.Common.MoreCountApi != 0) + if (more && SettingManager.Instance.Common.MoreCountApi != 0) { - return Math.Min(SettingManager.Common.MoreCountApi, GetMaxApiResultCount(type)); + return Math.Min(SettingManager.Instance.Common.MoreCountApi, GetMaxApiResultCount(type)); } - if (startup && SettingManager.Common.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply) + if (startup && SettingManager.Instance.Common.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply) { - return Math.Min(SettingManager.Common.FirstCountApi, GetMaxApiResultCount(type)); + return Math.Min(SettingManager.Instance.Common.FirstCountApi, GetMaxApiResultCount(type)); } } // 上記に当てはまらない場合の共通処理 - var count = SettingManager.Common.CountApi; + var count = SettingManager.Instance.Common.CountApi; if (type == MyCommon.WORKERTYPE.Reply) - count = SettingManager.Common.CountApiReply; + count = SettingManager.Instance.Common.CountApiReply; return Math.Min(count, GetMaxApiResultCount(type)); } @@ -527,17 +510,19 @@ public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, boo var count = GetApiResultCount(MyCommon.WORKERTYPE.Timeline, more, startup); - TwitterStatus[] statuses; - if (more) + var request = new GetTimelineRequest(this.UserId) { - statuses = await this.Api.StatusesHomeTimeline(count, maxId: tab.OldestId) - .ConfigureAwait(false); - } - else - { - statuses = await this.Api.StatusesHomeTimeline(count) - .ConfigureAwait(false); - } + MaxResults = count, + UntilId = more ? tab.OldestId.ToString() : null, + }; + + var response = await request.Send(this.Api.Connection) + .ConfigureAwait(false); + + var tweetIds = response.Data.Select(x => x.Id).ToList(); + + var statuses = await this.Api.StatusesLookup(tweetIds) + .ConfigureAwait(false); var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read); if (minimumId != null) @@ -596,7 +581,7 @@ public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTab } } - var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read); + var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read); if (minimumId != null) tab.OldestId = minimumId.Value; @@ -609,7 +594,7 @@ public async Task GetStatusApi(bool read, long id) var status = await this.Api.StatusesShow(id) .ConfigureAwait(false); - var item = CreatePostsFromStatusData(status); + var item = this.CreatePostsFromStatusData(status); item.IsRead = read; if (item.IsMe && !read && this.ReadOwnPost) item.IsRead = true; @@ -622,7 +607,7 @@ public async Task GetStatusApi(bool read, long id, TabModel tab) var post = await this.GetStatusApi(read, id) .ConfigureAwait(false); - //非同期アイコン取得&StatusDictionaryに追加 + // 非同期アイコン取得&StatusDictionaryに追加 if (tab != null && tab.IsInnerStorageTabType) tab.AddPostQueue(post); else @@ -630,206 +615,10 @@ public async Task GetStatusApi(bool read, long id, TabModel tab) } private PostClass CreatePostsFromStatusData(TwitterStatus status) - => this.CreatePostsFromStatusData(status, false); + => this.CreatePostsFromStatusData(status, favTweet: false); private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) - { - var post = new PostClass(); - TwitterEntities entities; - string sourceHtml; - - post.StatusId = status.Id; - if (status.RetweetedStatus != null) - { - var retweeted = status.RetweetedStatus; - - post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt); - - //Id - post.RetweetedId = retweeted.Id; - //本文 - post.TextFromApi = retweeted.FullText; - entities = retweeted.MergedEntities; - sourceHtml = retweeted.Source; - //Reply先 - post.InReplyToStatusId = retweeted.InReplyToStatusId; - post.InReplyToUser = retweeted.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; - - if (favTweet) - { - post.IsFav = true; - } - else - { - //幻覚fav対策 - var tc = TabInformations.GetInstance().FavoriteTab; - post.IsFav = tc.Contains(retweeted.Id); - } - - if (retweeted.Coordinates != null) - post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]); - - //以下、ユーザー情報 - var user = retweeted.User; - if (user != null) - { - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - } - else - { - post.UserId = 0L; - post.ScreenName = "?????"; - post.Nickname = "Unknown User"; - } - - //Retweetした人 - if (status.User != null) - { - post.RetweetedBy = status.User.ScreenName; - post.RetweetedByUserId = status.User.Id; - post.IsMe = post.RetweetedByUserId == this.UserId; - } - else - { - post.RetweetedBy = "?????"; - post.RetweetedByUserId = 0L; - } - } - else - { - post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt); - //本文 - post.TextFromApi = status.FullText; - entities = status.MergedEntities; - sourceHtml = status.Source; - post.InReplyToStatusId = status.InReplyToStatusId; - post.InReplyToUser = status.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; - - if (favTweet) - { - post.IsFav = true; - } - else - { - //幻覚fav対策 - var tc = TabInformations.GetInstance().FavoriteTab; - post.IsFav = tc.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav; - } - - if (status.Coordinates != null) - post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]); - - //以下、ユーザー情報 - var user = status.User; - if (user != null) - { - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - post.IsMe = post.UserId == this.UserId; - } - else - { - post.UserId = 0L; - post.ScreenName = "?????"; - post.Nickname = "Unknown User"; - } - } - //HTMLに整形 - var textFromApi = post.TextFromApi; - - var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink; - - if (quotedStatusLink != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded)) - quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある - - post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink); - post.TextFromApi = textFromApi; - post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink); - post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); - post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); - post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink); - post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); - post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); - - this.ExtractEntities(entities, post.ReplyToList, post.Media); - - post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) - .Where(x => x != post.StatusId && x != post.RetweetedId) - .Distinct().ToArray(); - - post.ExpandedUrls = entities.OfType() - .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) - .ToArray(); - - // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) - if (post.Text == post.TextFromApi) - post.Text = post.TextFromApi; - if (post.AccessibleText == post.TextFromApi) - post.AccessibleText = post.TextFromApi; - - // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す - post.ScreenName = string.Intern(post.ScreenName); - post.Nickname = string.Intern(post.Nickname); - post.ImageUrl = string.Intern(post.ImageUrl); - post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null; - - //Source整形 - var (sourceText, sourceUri) = ParseSource(sourceHtml); - post.Source = string.Intern(sourceText); - post.SourceUri = sourceUri; - - post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == this.UserId); - post.IsExcludeReply = false; - - if (post.IsMe) - { - post.IsOwl = false; - } - else - { - if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId); - } - - post.IsDm = false; - return post; - } - - /// - /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出 - /// - public static IEnumerable GetQuoteTweetStatusIds(IEnumerable? entities, TwitterQuotedStatusPermalink? quotedStatusLink) - { - entities ??= Enumerable.Empty(); - - var urls = entities.OfType().Select(x => x.ExpandedUrl); - - if (quotedStatusLink != null) - urls = urls.Append(quotedStatusLink.Expanded); - - return GetQuoteTweetStatusIds(urls); - } - - public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) - { - foreach (var url in urls) - { - var match = Twitter.StatusUrlRegex.Match(url); - if (match.Success) - { - if (long.TryParse(match.Groups["StatusId"].Value, out var statusId)) - yield return statusId; - } - } - } + => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet); private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read) { @@ -840,8 +629,8 @@ public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) if (minimumId == null || minimumId.Value > status.Id) minimumId = status.Id; - //二重取得回避 - lock (LockObj) + // 二重取得回避 + lock (this.lockObj) { if (tab == null) { @@ -853,11 +642,11 @@ public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) } } - //RT禁止ユーザーによるもの + // RT禁止ユーザーによるもの if (gType != MyCommon.WORKERTYPE.UserTimeline && status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue; - var post = CreatePostsFromStatusData(status); + var post = this.CreatePostsFromStatusData(status); post.IsRead = read; if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true; @@ -881,13 +670,13 @@ public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) minimumId = status.Id; if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id; - //二重取得回避 - lock (LockObj) + // 二重取得回避 + lock (this.lockObj) { if (tab.Contains(status.Id)) continue; } - var post = CreatePostsFromStatusData(status); + var post = this.CreatePostsFromStatusData(status); post.IsRead = read; if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true; @@ -908,13 +697,13 @@ public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) if (minimumId == null || minimumId.Value > status.Id) minimumId = status.Id; - //二重取得回避 - lock (LockObj) + // 二重取得回避 + lock (this.lockObj) { if (favTab.Contains(status.Id)) continue; } - var post = CreatePostsFromStatusData(status, true); + var post = this.CreatePostsFromStatusData(status, true); post.IsRead = read; @@ -931,16 +720,16 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, TwitterStatus[] statuses; if (more) { - statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Common.IsListsIncludeRts) + statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts) .ConfigureAwait(false); } else { - statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingManager.Common.IsListsIncludeRts) + statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts) .ConfigureAwait(false); } - var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read); + var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read); if (minimumId != null) tab.OldestId = minimumId.Value; @@ -969,10 +758,20 @@ internal static PostClass FindTopOfReplyChain(IDictionary posts public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) { var targetPost = tab.TargetPost; + + if (targetPost.RetweetedId != null) + { + var originalPost = targetPost.Clone(); + originalPost.StatusId = targetPost.RetweetedId.Value; + originalPost.RetweetedId = null; + originalPost.RetweetedBy = null; + targetPost = originalPost; + } + var relPosts = new Dictionary(); if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null) { - //検索結果対応 + // 検索結果対応 var p = TabInformations.GetInstance()[targetPost.StatusId]; if (p != null && p.InReplyToStatusId != null) { @@ -1016,23 +815,23 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId); } - //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む + // MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む var text = targetPost.Text; var ma = Twitter.StatusUrlRegex.Matches(text).Cast() .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast()); - foreach (var _match in ma) + foreach (var match in ma) { - if (long.TryParse(_match.Groups["StatusId"].Value, out var _statusId)) + if (long.TryParse(match.Groups["StatusId"].Value, out var statusId)) { - if (relPosts.ContainsKey(_statusId)) + if (relPosts.ContainsKey(statusId)) continue; - var p = TabInformations.GetInstance()[_statusId]; + var p = TabInformations.GetInstance()[statusId]; if (p == null) { try { - p = await this.GetStatusApi(read, _statusId) + p = await this.GetStatusApi(read, statusId) .ConfigureAwait(false); } catch (WebApiException ex) @@ -1047,20 +846,60 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) } } + try + { + var firstPost = nextPost; + var posts = await this.GetConversationPosts(firstPost, targetPost) + .ConfigureAwait(false); + + foreach (var post in posts.OrderBy(x => x.StatusId)) + { + if (relPosts.ContainsKey(post.StatusId)) + continue; + + // リプライチェーンが繋がらないツイートは除外 + if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId.Value)) + continue; + + relPosts.Add(post.StatusId, post); + } + } + catch (WebException ex) + { + lastException = ex; + } + relPosts.Values.ToList().ForEach(p => { - if (p.IsMe && !read && this.ReadOwnPost) - p.IsRead = true; + var post = p.Clone(); + if (post.IsMe && !read && this.ReadOwnPost) + post.IsRead = true; else - p.IsRead = read; + post.IsRead = read; - tab.AddPostQueue(p); + tab.AddPostQueue(post); }); if (lastException != null) throw new WebApiException(lastException.Message, lastException); } + private async Task GetConversationPosts(PostClass firstPost, PostClass targetPost) + { + var conversationId = firstPost.StatusId; + var query = $"conversation_id:{conversationId}"; + + if (targetPost.InReplyToUser != null && targetPost.InReplyToUser != targetPost.ScreenName) + query += $" (from:{targetPost.ScreenName} to:{targetPost.InReplyToUser}) OR (from:{targetPost.InReplyToUser} to:{targetPost.ScreenName})"; + else + query += $" from:{targetPost.ScreenName} to:{targetPost.ScreenName}"; + + var statuses = await this.Api.SearchTweets(query, count: 100) + .ConfigureAwait(false); + + return statuses.Statuses.Select(x => this.CreatePostsFromStatusData(x)).ToArray(); + } + public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more) { var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false); @@ -1147,99 +986,22 @@ private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventList eve this.CreateDirectMessagesEventFromJson(events, users, apps, read); } - private void CreateDirectMessagesEventFromJson(IEnumerable events, IReadOnlyDictionary users, - IReadOnlyDictionary apps, bool read) + private void CreateDirectMessagesEventFromJson( + IEnumerable events, + IReadOnlyDictionary users, + IReadOnlyDictionary apps, + bool read) { + var dmTab = TabInformations.GetInstance().DirectMessageTab; + foreach (var eventItem in events) { - var post = new PostClass(); - post.StatusId = long.Parse(eventItem.Id); - - var timestamp = long.Parse(eventItem.CreatedTimestamp); - post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond); - //本文 - var textFromApi = eventItem.MessageCreate.MessageData.Text; - - var entities = eventItem.MessageCreate.MessageData.Entities; - var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media; - - if (mediaEntity != null) - entities.Media = new[] { mediaEntity }; - - //HTMLに整形 - post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null); - post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null); - post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); - post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); - post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null); - post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); - post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); - post.IsFav = false; - - this.ExtractEntities(entities, post.ReplyToList, post.Media); - - post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null) - .Distinct().ToArray(); - - post.ExpandedUrls = entities.OfType() - .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) - .ToArray(); - - //以下、ユーザー情報 - string userId; - if (eventItem.MessageCreate.SenderId != this.Api.CurrentUserId.ToString(CultureInfo.InvariantCulture)) - { - userId = eventItem.MessageCreate.SenderId; - post.IsMe = false; - post.IsOwl = true; - } - else - { - userId = eventItem.MessageCreate.Target.RecipientId; - post.IsMe = true; - post.IsOwl = false; - } - - if (!users.TryGetValue(userId, out var user)) - continue; - - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - - // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) - if (post.Text == post.TextFromApi) - post.Text = post.TextFromApi; - if (post.AccessibleText == post.TextFromApi) - post.AccessibleText = post.TextFromApi; - - // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す - post.ScreenName = string.Intern(post.ScreenName); - post.Nickname = string.Intern(post.Nickname); - post.ImageUrl = string.Intern(post.ImageUrl); - - var appId = eventItem.MessageCreate.SourceAppId; - if (appId != null && apps.TryGetValue(appId, out var app)) - { - post.Source = string.Intern(app.Name); - - try - { - post.SourceUri = new Uri(SourceUriBase, app.Url); - } - catch (UriFormatException) { } - } + var post = this.postFactory.CreateFromDirectMessageEvent(eventItem, users, apps, this.UserId); post.IsRead = read; if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true; - post.IsReply = false; - post.IsExcludeReply = false; - post.IsDm = true; - var dmTab = TabInformations.GetInstance().DirectMessageTab; dmTab.AddPostQueue(post); } } @@ -1268,88 +1030,13 @@ public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backwar tab.OldestId = minimumId.Value; } - private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink) - { - if (entities != null) - { - if (entities.Urls != null) - { - foreach (var m in entities.Urls) - { - if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl); - } - } - if (entities.Media != null) - { - foreach (var m in entities.Media) - { - if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl); - } - } - } - - if (quotedStatusLink != null) - text += " " + quotedStatusLink.Display; - - return text; - } - - internal static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink) - { - if (entities == null) - return text; - - if (entities.Urls != null) - { - foreach (var entity in entities.Urls) - { - if (quotedStatus != null) - { - var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl); - if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr) - { - var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); - text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText)); - continue; - } - } - - if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl)) - text = text.Replace(entity.Url, entity.DisplayUrl); - } - } - - if (entities.Media != null) - { - foreach (var entity in entities.Media) - { - if (!MyCommon.IsNullOrEmpty(entity.AltText)) - { - text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText)); - } - else if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl)) - { - text = text.Replace(entity.Url, entity.DisplayUrl); - } - } - } - - if (quotedStatus != null && quotedStatusLink != null) - { - var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); - text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText); - } - - return text; - } - /// /// フォロワーIDを更新します /// /// public async Task RefreshFollowerIds() { - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; var cursor = -1L; var newFollowerIds = Enumerable.Empty(); @@ -1363,7 +1050,8 @@ public async Task RefreshFollowerIds() newFollowerIds = newFollowerIds.Concat(ret.Ids); cursor = ret.NextCursor; - } while (cursor != 0); + } + while (cursor != 0); this.followerId = newFollowerIds.ToHashSet(); TabInformations.GetInstance().RefreshOwl(this.followerId); @@ -1377,7 +1065,7 @@ public async Task RefreshFollowerIds() /// public async Task RefreshNoRetweetIds() { - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; this.noRTId = await this.Api.NoRetweetIds() .ConfigureAwait(false); @@ -1482,109 +1170,11 @@ await this.Api.ListsMembersShow(listId, user) } } - private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> AtList, List media) - { - if (entities != null) - { - if (entities.Hashtags != null) - { - lock (this.LockObj) - { - this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text)); - } - } - if (entities.UserMentions != null) - { - foreach (var ent in entities.UserMentions) - { - AtList.Add((ent.Id, ent.ScreenName)); - } - } - if (entities.Media != null) - { - if (media != null) - { - foreach (var ent in entities.Media) - { - if (!media.Any(x => x.Url == ent.MediaUrlHttps)) - { - if (ent.VideoInfo != null && - ent.Type == "animated_gif" || ent.Type == "video") - { - media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl)); - } - else - media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl: null)); - } - } - } - } - } - } - - internal static string CreateHtmlAnchor(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink) - { - var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text)); - - // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない - text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true); - - text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1$2$3"); - text = PreProcessUrl(text); //IDN置換 - - if (quotedStatusLink != null) - { - text += string.Format(" {1}", - WebUtility.HtmlEncode(quotedStatusLink.Url), - WebUtility.HtmlEncode(quotedStatusLink.Display)); - } - - return text; - } - - private static readonly Uri SourceUriBase = new Uri("https://twitter.com/"); - - /// - /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します - /// - internal static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml) - { - if (MyCommon.IsNullOrEmpty(sourceHtml)) - return ("", null); - - string sourceText; - Uri? sourceUri; - - // sourceHtmlの例: Twitter Web Client - - var match = Regex.Match(sourceHtml, "^.+?)\".*?>(?.+)$", RegexOptions.IgnoreCase); - if (match.Success) - { - sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value); - try - { - var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value); - sourceUri = new Uri(SourceUriBase, uriStr); - } - catch (UriFormatException) - { - sourceUri = null; - } - } - else - { - sourceText = WebUtility.HtmlDecode(sourceHtml); - sourceUri = null; - } - - return (sourceText, sourceUri); - } - public async Task GetInfoApi() { if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null; - if (MyCommon._endingFlag) return null; + if (MyCommon.EndingFlag) return null; var limits = await this.Api.ApplicationRateLimitStatus() .ConfigureAwait(false); @@ -1600,7 +1190,7 @@ internal static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHt /// public async Task RefreshBlockIds() { - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; var cursor = -1L; var newBlockIds = Enumerable.Empty(); @@ -1611,7 +1201,8 @@ public async Task RefreshBlockIds() newBlockIds = newBlockIds.Concat(ret.Ids); cursor = ret.NextCursor; - } while (cursor != 0); + } + while (cursor != 0); var blockIdsSet = newBlockIds.ToHashSet(); blockIdsSet.Remove(this.UserId); // 元のソースにあったので一応残しておく @@ -1625,7 +1216,7 @@ public async Task RefreshBlockIds() /// public async Task RefreshMuteUserIdsAsync() { - if (MyCommon._endingFlag) return; + if (MyCommon.EndingFlag) return; var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x)) .ConfigureAwait(false); @@ -1634,15 +1225,7 @@ public async Task RefreshMuteUserIdsAsync() } public string[] GetHashList() - { - string[] hashArray; - lock (LockObj) - { - hashArray = _hashList.ToArray(); - _hashList.Clear(); - } - return hashArray; - } + => this.postFactory.GetReceivedHashtags(); public string AccessToken => ((TwitterApiConnection)this.Api.Connection).AccessToken; @@ -1751,6 +1334,34 @@ int GetWeightFromCodepoint(int codepoint) return remainWeight / config.Scale; } + /// + /// プロフィール画像のサイズを指定したURLを生成 + /// + /// + /// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/user-profile-images-and-banners を参照 + /// + public static string CreateProfileImageUrl(string normalUrl, string size) + { + return size switch + { + "original" => normalUrl.Replace("_normal.", "."), + "normal" => normalUrl, + "bigger" or "mini" => normalUrl.Replace("_normal.", $"_{size}."), + _ => throw new ArgumentException($"Invalid size: ${size}", nameof(size)), + }; + } + + public static string DecideProfileImageSize(int sizePx) + { + return sizePx switch + { + <= 24 => "mini", + <= 48 => "normal", + <= 73 => "bigger", + _ => "original", + }; + } + public bool IsDisposed { get; private set; } = false; protected virtual void Dispose(bool disposing) @@ -1768,7 +1379,7 @@ protected virtual void Dispose(bool disposing) public void Dispose() { - Dispose(true); + this.Dispose(true); GC.SuppressFinalize(this); } } diff --git a/OpenTween/UpdateDialog.cs b/OpenTween/UpdateDialog.cs index 53c49bd48..f3cd7c80d 100644 --- a/OpenTween/UpdateDialog.cs +++ b/OpenTween/UpdateDialog.cs @@ -7,19 +7,19 @@ // (c) 2012 tigree4th // (c) 2012 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -56,7 +56,7 @@ public string DetailsText public UpdateDialog() { - InitializeComponent(); + this.InitializeComponent(); this.PictureBox1.Image = SystemIcons.Question.ToBitmap(); this.Text = MyCommon.ReplaceAppName(this.Text); diff --git a/OpenTween/UserInfo.cs b/OpenTween/UserInfo.cs index f11781dda..69f57d19f 100644 --- a/OpenTween/UserInfo.cs +++ b/OpenTween/UserInfo.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 Egtra (@egtra) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -67,6 +67,7 @@ public UserInfo(TwitterUser user) this.PostSource = user.Status.Source; } } + public long Id = 0; public string Name = ""; public string ScreenName = ""; @@ -84,8 +85,6 @@ public UserInfo(TwitterUser user) public string RecentPost = ""; public DateTimeUtc PostCreatedAt; public string PostSource = ""; // html形式 "Tween" - public bool isFollowing = false; - public bool isFollowed = false; public override string ToString() => this.ScreenName + " / " + this.Name; diff --git a/OpenTween/UserInfoDialog.cs b/OpenTween/UserInfoDialog.cs index 8a35cbb9c..ac778d17f 100644 --- a/OpenTween/UserInfoDialog.cs +++ b/OpenTween/UserInfoDialog.cs @@ -6,19 +6,19 @@ // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. -// +// // This file is part of OpenTween. -// +// // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. -// +// // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -// for more details. -// +// for more details. +// // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, @@ -30,26 +30,26 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.IO; using System.Linq; +using System.Net; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; -using System.Text.RegularExpressions; using System.Web; -using System.IO; -using System.Net; +using System.Windows.Forms; using OpenTween.Api; using OpenTween.Api.DataModel; using OpenTween.Connection; -using System.Diagnostics.CodeAnalysis; namespace OpenTween { public partial class UserInfoDialog : OTBaseForm { - private TwitterUser _displayUser = null!; + private TwitterUser displayUser = null!; private CancellationTokenSource? cancellationTokenSource = null; private readonly TweenMain mainForm; @@ -60,7 +60,7 @@ public UserInfoDialog(TweenMain mainForm, TwitterApi twitterApi) this.mainForm = mainForm; this.twitterApi = twitterApi; - InitializeComponent(); + this.InitializeComponent(); // LabelScreenName のフォントを OTBaseForm.GlobalFont に変更 this.LabelScreenName.Font = this.ReplaceToGlobalFont(this.LabelScreenName.Font); @@ -90,14 +90,14 @@ public async Task ShowUserAsync(TwitterUser user) if (this.IsDisposed) return; - if (user == null || user == this._displayUser) + if (user == null || user == this.displayUser) return; this.CancelLoading(); var cancellationToken = this.cancellationTokenSource!.Token; - this._displayUser = user; + this.displayUser = user; this.LabelId.Text = user.IdStr; this.LabelScreenName.Text = user.ScreenName; @@ -178,7 +178,7 @@ private async Task SetDescriptionAsync(string? descriptionText, TwitterEntities? .Concat(TweetExtractor.ExtractEmojiEntities(descriptionText)); var html = TweetFormatter.AutoLinkHtml(descriptionText, mergedEntities); - html = this.mainForm.createDetailHtml(html); + html = this.mainForm.CreateDetailHtml(html); if (cancellationToken.IsCancellationRequested) return; @@ -187,7 +187,7 @@ private async Task SetDescriptionAsync(string? descriptionText, TwitterEntities? } else { - this.DescriptionBrowser.DocumentText = ""; + this.DescriptionBrowser.DocumentText = this.mainForm.CreateDetailHtml(""); } } @@ -204,7 +204,8 @@ private async Task SetUserImageAsync(string imageUri, CancellationToken cancella await this.UserPicture.SetImageFromTask(async () => { - var uri = imageUri.Replace("_normal", "_bigger"); + var sizeName = Twitter.DecideProfileImageSize(this.UserPicture.Width); + var uri = Twitter.CreateProfileImageUrl(imageUri, sizeName); using var imageStream = await Networking.Http.GetStreamAsync(uri) .ConfigureAwait(false); @@ -257,7 +258,7 @@ private async Task SetRecentStatusAsync(TwitterStatus? status, CancellationToken var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(status.FullText)); var html = TweetFormatter.AutoLinkHtml(status.FullText, mergedEntities); - html = this.mainForm.createDetailHtml(html + + html = this.mainForm.CreateDetailHtml(html + " Posted at " + MyCommon.DateTimeParse(status.CreatedAt).ToLocalTimeString() + " via " + status.Source); @@ -268,7 +269,7 @@ private async Task SetRecentStatusAsync(TwitterStatus? status, CancellationToken } else { - this.RecentPostBrowser.DocumentText = Properties.Resources.ShowUserInfo2; + this.RecentPostBrowser.DocumentText = this.mainForm.CreateDetailHtml(Properties.Resources.ShowUserInfo2); } } @@ -289,8 +290,8 @@ private async Task LoadFriendshipAsync(string screenName, CancellationToken canc } catch (WebApiException) { - LabelIsFollowed.Text = Properties.Resources.GetFriendshipInfo6; - LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo6; + this.LabelIsFollowed.Text = Properties.Resources.GetFriendshipInfo6; + this.LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo6; return; } @@ -354,7 +355,7 @@ private async void ButtonFollow_Click(object sender, EventArgs e) { try { - await this.twitterApi.FriendshipsCreate(this._displayUser.ScreenName) + await this.twitterApi.FriendshipsCreate(this.displayUser.ScreenName) .IgnoreResponse(); } catch (WebApiException ex) @@ -365,22 +366,25 @@ await this.twitterApi.FriendshipsCreate(this._displayUser.ScreenName) } MessageBox.Show(Properties.Resources.FRMessage3); - LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo1; - ButtonFollow.Enabled = false; - ButtonUnFollow.Enabled = true; + this.LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo1; + this.ButtonFollow.Enabled = false; + this.ButtonUnFollow.Enabled = true; } private async void ButtonUnFollow_Click(object sender, EventArgs e) { - if (MessageBox.Show(this._displayUser.ScreenName + Properties.Resources.ButtonUnFollow_ClickText1, - Properties.Resources.ButtonUnFollow_ClickText2, - MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes) + if (MessageBox.Show( + this.displayUser.ScreenName + Properties.Resources.ButtonUnFollow_ClickText1, + Properties.Resources.ButtonUnFollow_ClickText2, + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2) == DialogResult.Yes) { using (ControlTransaction.Disabled(this.ButtonUnFollow)) { try { - await this.twitterApi.FriendshipsDestroy(this._displayUser.ScreenName) + await this.twitterApi.FriendshipsDestroy(this.displayUser.ScreenName) .IgnoreResponse(); } catch (WebApiException ex) @@ -391,17 +395,17 @@ await this.twitterApi.FriendshipsDestroy(this._displayUser.ScreenName) } MessageBox.Show(Properties.Resources.FRMessage3); - LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo2; - ButtonFollow.Enabled = true; - ButtonUnFollow.Enabled = false; + this.LabelIsFollowing.Text = Properties.Resources.GetFriendshipInfo2; + this.ButtonFollow.Enabled = true; + this.ButtonUnFollow.Enabled = false; } } private void ShowUserInfo_Activated(object sender, EventArgs e) { - //画面が他画面の裏に隠れると、アイコン画像が再描画されない問題の対応 - if (UserPicture.Image != null) - UserPicture.Invalidate(false); + // 画面が他画面の裏に隠れると、アイコン画像が再描画されない問題の対応 + if (this.UserPicture.Image != null) + this.UserPicture.Invalidate(false); } private void ShowUserInfo_Shown(object sender, EventArgs e) @@ -456,65 +460,65 @@ private async void LinkLabel2_LinkClicked(object sender, LinkLabelLinkClickedEve => await MyCommon.OpenInBrowserAsync(this, "https://support.twitter.com/groups/31-twitter-basics/topics/107-my-profile-account-settings/articles/243055-x516c-x958b-x3001-x975e-x516c-x958b-x30a2-x30ab-x30a6-x30f3-x30c8-x306b-x3064-x3044-x3066"); private async void ButtonSearchPosts_Click(object sender, EventArgs e) - => await this.mainForm.AddNewTabForUserTimeline(this._displayUser.ScreenName); + => await this.mainForm.AddNewTabForUserTimeline(this.displayUser.ScreenName); private async void UserPicture_Click(object sender, EventArgs e) { - var imageUrl = this._displayUser.ProfileImageUrlHttps; - imageUrl = imageUrl.Remove(imageUrl.LastIndexOf("_normal", StringComparison.Ordinal), 7); + var imageUrl = this.displayUser.ProfileImageUrlHttps; + imageUrl = Twitter.CreateProfileImageUrl(imageUrl, "original"); await MyCommon.OpenInBrowserAsync(this, imageUrl); } - private bool IsEditing = false; - private string ButtonEditText = ""; + private bool isEditing = false; + private string buttonEditText = ""; private async void ButtonEdit_Click(object sender, EventArgs e) { // 自分以外のプロフィールは変更できない - if (this.twitterApi.CurrentUserId != this._displayUser.Id) + if (this.twitterApi.CurrentUserId != this.displayUser.Id) return; using (ControlTransaction.Disabled(this.ButtonEdit)) { - if (!IsEditing) + if (!this.isEditing) { - ButtonEditText = ButtonEdit.Text; - ButtonEdit.Text = Properties.Resources.UserInfoButtonEdit_ClickText1; + this.buttonEditText = this.ButtonEdit.Text; + this.ButtonEdit.Text = Properties.Resources.UserInfoButtonEdit_ClickText1; - TextBoxName.Text = LabelName.Text; - TextBoxName.Enabled = true; - TextBoxName.Visible = true; - LabelName.Visible = false; + this.TextBoxName.Text = this.LabelName.Text; + this.TextBoxName.Enabled = true; + this.TextBoxName.Visible = true; + this.LabelName.Visible = false; - TextBoxLocation.Text = LabelLocation.Text; - TextBoxLocation.Enabled = true; - TextBoxLocation.Visible = true; - LabelLocation.Visible = false; + this.TextBoxLocation.Text = this.LabelLocation.Text; + this.TextBoxLocation.Enabled = true; + this.TextBoxLocation.Visible = true; + this.LabelLocation.Visible = false; - TextBoxWeb.Text = this._displayUser.Url; - TextBoxWeb.Enabled = true; - TextBoxWeb.Visible = true; - LinkLabelWeb.Visible = false; + this.TextBoxWeb.Text = this.displayUser.Url; + this.TextBoxWeb.Enabled = true; + this.TextBoxWeb.Visible = true; + this.LinkLabelWeb.Visible = false; - TextBoxDescription.Text = this._displayUser.Description; - TextBoxDescription.Enabled = true; - TextBoxDescription.Visible = true; - DescriptionBrowser.Visible = false; + this.TextBoxDescription.Text = this.displayUser.Description; + this.TextBoxDescription.Enabled = true; + this.TextBoxDescription.Visible = true; + this.DescriptionBrowser.Visible = false; - TextBoxName.Focus(); - TextBoxName.Select(TextBoxName.Text.Length, 0); + this.TextBoxName.Focus(); + this.TextBoxName.Select(this.TextBoxName.Text.Length, 0); - IsEditing = true; + this.isEditing = true; } else { Task? showUserTask = null; - if (TextBoxName.Modified || - TextBoxLocation.Modified || - TextBoxWeb.Modified || - TextBoxDescription.Modified) + if (this.TextBoxName.Modified || + this.TextBoxLocation.Modified || + this.TextBoxWeb.Modified || + this.TextBoxDescription.Modified) { try { @@ -534,25 +538,25 @@ private async void ButtonEdit_Click(object sender, EventArgs e) } } - TextBoxName.Enabled = false; - TextBoxName.Visible = false; - LabelName.Visible = true; + this.TextBoxName.Enabled = false; + this.TextBoxName.Visible = false; + this.LabelName.Visible = true; - TextBoxLocation.Enabled = false; - TextBoxLocation.Visible = false; - LabelLocation.Visible = true; + this.TextBoxLocation.Enabled = false; + this.TextBoxLocation.Visible = false; + this.LabelLocation.Visible = true; - TextBoxWeb.Enabled = false; - TextBoxWeb.Visible = false; - LinkLabelWeb.Visible = true; + this.TextBoxWeb.Enabled = false; + this.TextBoxWeb.Visible = false; + this.LinkLabelWeb.Visible = true; - TextBoxDescription.Enabled = false; - TextBoxDescription.Visible = false; - DescriptionBrowser.Visible = true; + this.TextBoxDescription.Enabled = false; + this.TextBoxDescription.Visible = false; + this.DescriptionBrowser.Visible = true; - ButtonEdit.Text = ButtonEditText; + this.ButtonEdit.Text = this.buttonEditText; - IsEditing = false; + this.isEditing = false; if (showUserTask != null) await showUserTask; @@ -579,7 +583,7 @@ await this.twitterApi.AccountUpdateProfileImage(mediaItem) try { - var user = await this.twitterApi.UsersShow(this._displayUser.ScreenName); + var user = await this.twitterApi.UsersShow(this.displayUser.ScreenName); if (user != null) await this.ShowUserAsync(user); @@ -592,18 +596,18 @@ await this.twitterApi.AccountUpdateProfileImage(mediaItem) private async void ChangeIconToolStripMenuItem_Click(object sender, EventArgs e) { - OpenFileDialogIcon.Filter = Properties.Resources.ChangeIconToolStripMenuItem_ClickText1; - OpenFileDialogIcon.Title = Properties.Resources.ChangeIconToolStripMenuItem_ClickText2; - OpenFileDialogIcon.FileName = ""; + this.OpenFileDialogIcon.Filter = Properties.Resources.ChangeIconToolStripMenuItem_ClickText1; + this.OpenFileDialogIcon.Title = Properties.Resources.ChangeIconToolStripMenuItem_ClickText2; + this.OpenFileDialogIcon.FileName = ""; - var rslt = OpenFileDialogIcon.ShowDialog(); + var rslt = this.OpenFileDialogIcon.ShowDialog(); if (rslt != DialogResult.OK) { return; } - var fn = OpenFileDialogIcon.FileName; + var fn = this.OpenFileDialogIcon.FileName; if (this.IsValidIconFile(new FileInfo(fn))) { await this.DoChangeIcon(fn); @@ -616,15 +620,18 @@ private async void ChangeIconToolStripMenuItem_Click(object sender, EventArgs e) private async void ButtonBlock_Click(object sender, EventArgs e) { - if (MessageBox.Show(this._displayUser.ScreenName + Properties.Resources.ButtonBlock_ClickText1, - Properties.Resources.ButtonBlock_ClickText2, - MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes) + if (MessageBox.Show( + this.displayUser.ScreenName + Properties.Resources.ButtonBlock_ClickText1, + Properties.Resources.ButtonBlock_ClickText2, + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2) == DialogResult.Yes) { using (ControlTransaction.Disabled(this.ButtonBlock)) { try { - await this.twitterApi.BlocksCreate(this._displayUser.ScreenName) + await this.twitterApi.BlocksCreate(this.displayUser.ScreenName) .IgnoreResponse(); } catch (WebApiException ex) @@ -640,15 +647,18 @@ await this.twitterApi.BlocksCreate(this._displayUser.ScreenName) private async void ButtonReportSpam_Click(object sender, EventArgs e) { - if (MessageBox.Show(this._displayUser.ScreenName + Properties.Resources.ButtonReportSpam_ClickText1, - Properties.Resources.ButtonReportSpam_ClickText2, - MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes) + if (MessageBox.Show( + this.displayUser.ScreenName + Properties.Resources.ButtonReportSpam_ClickText1, + Properties.Resources.ButtonReportSpam_ClickText2, + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2) == DialogResult.Yes) { using (ControlTransaction.Disabled(this.ButtonReportSpam)) { try { - await this.twitterApi.UsersReportSpam(this._displayUser.ScreenName) + await this.twitterApi.UsersReportSpam(this.displayUser.ScreenName) .IgnoreResponse(); } catch (WebApiException ex) @@ -664,15 +674,18 @@ await this.twitterApi.UsersReportSpam(this._displayUser.ScreenName) private async void ButtonBlockDestroy_Click(object sender, EventArgs e) { - if (MessageBox.Show(this._displayUser.ScreenName + Properties.Resources.ButtonBlockDestroy_ClickText1, - Properties.Resources.ButtonBlockDestroy_ClickText2, - MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes) + if (MessageBox.Show( + this.displayUser.ScreenName + Properties.Resources.ButtonBlockDestroy_ClickText1, + Properties.Resources.ButtonBlockDestroy_ClickText2, + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2) == DialogResult.Yes) { using (ControlTransaction.Disabled(this.ButtonBlockDestroy)) { try { - await this.twitterApi.BlocksDestroy(this._displayUser.ScreenName) + await this.twitterApi.BlocksDestroy(this.displayUser.ScreenName) .IgnoreResponse(); } catch (WebApiException ex) @@ -703,7 +716,7 @@ private bool IsValidIconFile(FileInfo info) private void ShowUserInfo_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop) && - !e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像D&Dは弾く + !e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像D&Dは弾く { var files = (string[])e.Data.GetData(DataFormats.FileDrop, false); if (files.Length != 1) @@ -720,10 +733,14 @@ private void ShowUserInfo_DragEnter(object sender, DragEventArgs e) private async void ShowUserInfo_DragDrop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop) && - !e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像D&Dは弾く - { - var ret = MessageBox.Show(this, Properties.Resources.ChangeIconToolStripMenuItem_Confirm, - ApplicationSettings.ApplicationName, MessageBoxButtons.OKCancel, MessageBoxIcon.Question); + !e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像D&Dは弾く + { + var ret = MessageBox.Show( + this, + Properties.Resources.ChangeIconToolStripMenuItem_Confirm, + ApplicationSettings.ApplicationName, + MessageBoxButtons.OKCancel, + MessageBoxIcon.Question); if (ret != DialogResult.OK) return; diff --git a/OpenTween/WebApiException.cs b/OpenTween/WebApiException.cs index cbafa5d67..75eac8385 100644 --- a/OpenTween/WebApiException.cs +++ b/OpenTween/WebApiException.cs @@ -37,9 +37,19 @@ public class WebApiException : Exception { public string? ResponseText { get; } = null; - public WebApiException() { } - public WebApiException(string message) : base(message) { } - public WebApiException(string message, Exception innerException) : base(message, innerException) { } + public WebApiException() + { + } + + public WebApiException(string message) + : base(message) + { + } + + public WebApiException(string message, Exception innerException) + : base(message, innerException) + { + } public WebApiException(string message, string responseText) : this(message) diff --git a/appveyor.yml b/appveyor.yml index 62f169cf0..43838a454 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ -version: 2.4.3.{build} +version: 2.5.0.{build} -os: Visual Studio 2019 +os: Visual Studio 2022 environment: matrix: @@ -18,6 +18,11 @@ matrix: - RELEASE_TAG: 'true' configuration: Debug +branches: + only: + - develop + - release + for: - # for dev build matrix: @@ -45,24 +50,31 @@ build: verbosity: minimal cache: - - ./packages/ + - '%UserProfile%\.nuget\packages -> OpenTween\OpenTween.csproj, OpenTween.Tests\OpenTween.Tests.csproj' init: - git config --global core.autocrlf true before_build: - nuget restore - - choco install opencover.portable test_script: - - OpenCover.Console.exe -register -target:"%xunit20%\xunit.console.exe" -targetargs:".\OpenTween.Tests\bin\%CONFIGURATION%\net472\OpenTween.Tests.dll -noshadow -appveyor" -filter:"+[OpenTween*]* -[OpenTween.Tests]*" -excludebyfile:"*.Designer.cs" -skipautoprops -hideskipped:All -returntargetcode -output:coverage.xml + - cmd: | + set altCoverVersion=8.2.837 + set xunitVersion=2.4.1 + set targetFramework=net472 + set nugetPackages=%UserProfile%\.nuget\packages -after_test: - - npm install codecov --save - - ./node_modules/.bin/codecov -f coverage.xml + %nugetPackages%\altcover\%altCoverVersion%\tools\%targetFramework%\AltCover.exe --inputDirectory .\OpenTween.Tests\bin\Debug\%targetFramework%\ --outputDirectory .\__Instrumented\ --assemblyFilter "?^OpenTween(?!\.Tests)" --typeFilter "?^OpenTween\." --fileFilter "\.Designer\.cs" --visibleBranches + + %nugetPackages%\altcover\%altCoverVersion%\tools\%targetFramework%\AltCover.exe runner --recorderDirectory .\__Instrumented\ --executable %nugetPackages%\xunit.runner.console\%xunitVersion%\tools\%targetFramework%\xunit.console.exe -- .\__Instrumented\OpenTween.Tests.dll +after_test: + - ps: | + Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe + .\codecov.exe -f coverage.xml - ps: | - $env:PATH = $env:PATH + ';C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\bin\Roslyn\;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\' + $env:PATH = $env:PATH + ';C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\Roslyn\;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\' $binDir = '.\OpenTween\bin\' + $env:CONFIGURATION + '\' $objDir = '.\OpenTween\obj\' + $env:CONFIGURATION + '\' $assemblyInfo = '.\OpenTween\Properties\AssemblyInfo.cs' diff --git a/msbuild.rsp b/msbuild.rsp new file mode 100644 index 000000000..c4dbaecb1 --- /dev/null +++ b/msbuild.rsp @@ -0,0 +1,3 @@ +# MSBuild response file for AppVeyor build + +/warnaserror diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 000000000..8fa1c968b --- /dev/null +++ b/stylecop.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace", + }, + "documentationRules": { + "documentInterfaces": false, + "documentExposedElements": false, + "documentInternalElements": false + } + } +}