1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2011 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
23 // You should have received a copy of the GNU General public License along
24 // with this program. if not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
31 using System.Collections;
32 using System.Collections.Generic;
33 using System.ComponentModel;
34 using System.Diagnostics;
35 using System.Diagnostics.CodeAnalysis;
37 using System.Drawing.Imaging;
38 using System.Globalization;
42 using System.Net.Http;
43 using System.Net.NetworkInformation;
44 using System.Reflection;
45 using System.Runtime.InteropServices;
46 using System.Runtime.Serialization.Json;
47 using System.Security.Cryptography;
48 using System.Security.Principal;
50 using System.Text.RegularExpressions;
51 using System.Threading.Tasks;
53 using System.Windows.Forms;
55 using OpenTween.Models;
56 using OpenTween.Setting;
60 public static class MyCommon
62 private static readonly object LockObj = new();
64 public static bool EndingFlag { get; set; } // 終了フラグ
75 public enum NameBalloonEnum
82 public enum DispTitleEnum
94 public enum LogUnitEnum
101 public enum UploadFileType
108 public enum UrlConverter
122 public enum ListItemDoubleClickActionType
124 // 設定ファイルの互換性を保つため新規の項目は途中に追加しないこと
136 public enum HITRESULT
145 public enum HttpTimeOut
152 // Backgroundworkerへ処理種別を通知するための引数用enum
153 public enum WORKERTYPE
155 Timeline, // タイムライン取得
157 DirectMessegeRcv, // 受信DM取得
158 DirectMessegeSnt, // 送信DM取得
159 PostMessage, // 発言POST
162 Follower, // Followerリスト取得
164 Retweet, // Retweetする
165 PublicSearch, // 公式検索
168 UserTimeline, // UserTimeline
169 BlockIds, // Blocking/ids
170 Configuration, // Twitter Configuration読み込み
171 NoRetweetIds, // RT非表示ユーザー取得
173 ErrorState, // エラー表示のみで後処理終了(認証エラー時など)
176 public static class DEFAULTTAB
178 public const string RECENT = "Recent";
179 public const string REPLY = "Reply";
180 public const string DM = "Direct";
181 public const string FAV = "Favorites";
182 public static readonly string MUTE = Properties.Resources.MuteTabName;
185 public static readonly object? Block = null;
186 public static bool TraceFlag = false;
189 public static bool DebugBuild = true;
191 public static bool DebugBuild = false;
194 public enum ACCOUNT_STATE
200 public enum REPLY_ICONSTATE
207 public static _Assembly EntryAssembly { get; internal set; }
209 public static string FileVersion { get; internal set; }
213 var assembly = Assembly.GetExecutingAssembly();
214 MyCommon.EntryAssembly = assembly;
216 var assemblyVersion = assembly.GetName().Version;
217 MyCommon.FileVersion = assemblyVersion.ToString();
220 public static string GetErrorLogPath()
221 => Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
223 public static void TraceOut(WebApiException ex)
225 var message = ExceptionOutMessage(ex);
227 if (ex.ResponseText != null)
228 message += Environment.NewLine + "------- Response Data -------" + Environment.NewLine + ex.ResponseText;
230 TraceOut(TraceFlag, message);
233 public static void TraceOut(Exception ex, string message)
235 var buf = ExceptionOutMessage(ex);
236 TraceOut(TraceFlag, message + Environment.NewLine + buf);
239 public static void TraceOut(string message)
240 => TraceOut(TraceFlag, message);
242 public static void TraceOut(bool outputFlag, string message)
246 if (!outputFlag) return;
248 var logPath = MyCommon.GetErrorLogPath();
249 if (!Directory.Exists(logPath))
250 Directory.CreateDirectory(logPath);
252 var now = DateTimeUtc.Now;
253 var fileName = $"{ApplicationSettings.AssemblyName}Trace-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
254 fileName = Path.Combine(logPath, fileName);
256 using var writer = new StreamWriter(fileName);
258 writer.WriteLine("**** TraceOut: {0} ****", now.ToLocalTimeString());
259 writer.WriteLine(Properties.Resources.TraceOutText1, ApplicationSettings.FeedbackEmailAddress);
260 writer.WriteLine(Properties.Resources.TraceOutText2, ApplicationSettings.FeedbackTwitterName);
262 writer.WriteLine(Properties.Resources.TraceOutText3);
263 writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString);
264 writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version);
265 writer.WriteLine(Properties.Resources.TraceOutText6, ApplicationSettings.AssemblyName, FileVersion);
266 writer.WriteLine(message);
272 // 注意:最終的にファイル出力されるエラーログに記録されるため次の情報は書き出さない
274 // Dataプロパティにある終了許可フラグのパースもここで行う
276 public static string ExceptionOutMessage(Exception ex)
278 var isTerminatePermission = true;
279 return ExceptionOutMessage(ex, ref isTerminatePermission);
282 public static string ExceptionOutMessage(Exception ex, ref bool isTerminatePermission)
284 if (ex == null) return "";
286 var buf = new StringBuilder();
288 buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, ex.GetType().FullName, ex.Message);
292 var needHeader = true;
293 foreach (DictionaryEntry dt in ex.Data)
298 buf.AppendLine("-------Extra Information-------");
301 buf.AppendFormat("{0} : {1}", dt.Key, dt.Value);
303 if (dt.Key.Equals("IsTerminatePermission"))
305 isTerminatePermission = (bool)dt.Value;
310 buf.AppendLine("-----End Extra Information-----");
313 buf.AppendLine(ex.StackTrace);
316 // InnerExceptionが存在する場合書き出す
317 var innerException = ex.InnerException;
319 while (innerException != null)
321 buf.AppendFormat("-----InnerException[{0}]-----\r\n", nesting);
323 buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, innerException.GetType().FullName, innerException.Message);
325 if (innerException.Data != null)
327 var needHeader = true;
329 foreach (DictionaryEntry dt in innerException.Data)
334 buf.AppendLine("-------Extra Information-------");
337 buf.AppendFormat("{0} : {1}", dt.Key, dt.Value);
338 if (dt.Key.Equals("IsTerminatePermission"))
340 isTerminatePermission = (bool)dt.Value;
345 buf.AppendLine("-----End Extra Information-----");
348 buf.AppendLine(innerException.StackTrace);
351 innerException = innerException.InnerException;
353 return buf.ToString();
356 public static bool ExceptionOut(Exception ex)
360 var isTerminatePermission = true;
362 var ident = WindowsIdentity.GetCurrent();
363 var princ = new WindowsPrincipal(ident);
364 var now = DateTimeUtc.Now;
366 var errorReport = string.Join(Environment.NewLine,
367 string.Format(Properties.Resources.UnhandledExceptionText1, now.ToLocalTimeString()),
370 string.Format(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator)),
371 string.Format(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User)),
374 // OSVersion,AppVersion書き出し
375 string.Format(Properties.Resources.UnhandledExceptionText4),
376 string.Format(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString),
377 string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version),
378 string.Format(Properties.Resources.UnhandledExceptionText7, ApplicationSettings.AssemblyName, FileVersion),
380 ExceptionOutMessage(ex, ref isTerminatePermission));
382 var logPath = MyCommon.GetErrorLogPath();
383 if (!Directory.Exists(logPath))
384 Directory.CreateDirectory(logPath);
386 var fileName = $"{ApplicationSettings.AssemblyName}-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
387 using (var writer = new StreamWriter(Path.Combine(logPath, fileName)))
389 writer.Write(errorReport);
392 var settings = SettingManager.Instance;
393 var mainForm = Application.OpenForms.OfType<TweenMain>().FirstOrDefault();
396 if (mainForm != null && !mainForm.IsDisposed)
397 report = new ErrorReport(mainForm.TwitterInstance, errorReport);
399 report = new ErrorReport(errorReport);
401 report.AnonymousReport = settings.Common.ErrorReportAnonymous;
403 OpenErrorReportDialog(mainForm, report);
405 // ダイアログ内で設定が変更されていれば保存する
406 if (settings.Common.ErrorReportAnonymous != report.AnonymousReport)
408 settings.Common.ErrorReportAnonymous = report.AnonymousReport;
409 settings.SaveCommon();
416 private static void OpenErrorReportDialog(Form? owner, ErrorReport report)
418 if (owner != null && owner.InvokeRequired)
420 owner.Invoke((Action)(() => OpenErrorReportDialog(owner, report)));
424 using var dialog = new SendErrorReportForm();
425 dialog.ErrorReport = report;
426 dialog.ShowDialog(owner);
430 /// URLのドメイン名をPunycode展開します。
432 /// ドメイン名がIDNでない場合はそのまま返します。
433 /// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
436 /// <param name="inputUrl">展開対象のURL</param>
437 /// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
438 public static string? IDNEncode(string inputUrl)
442 var uriBuilder = new UriBuilder(inputUrl);
444 var idnConverter = new IdnMapping();
445 uriBuilder.Host = idnConverter.GetAscii(uriBuilder.Host);
447 return uriBuilder.Uri.AbsoluteUri;
455 public static string IDNDecode(string inputUrl)
459 var uriBuilder = new UriBuilder(inputUrl);
461 if (uriBuilder.Host != null)
463 var idnConverter = new IdnMapping();
464 uriBuilder.Host = idnConverter.GetUnicode(uriBuilder.Host);
467 return uriBuilder.Uri.AbsoluteUri;
476 /// URL を画面上で人間に読みやすい文字列に変換する(エスケープ解除など)
478 public static string ConvertToReadableUrl(string inputUrl)
482 var outputUrl = inputUrl;
485 outputUrl = MyCommon.IDNDecode(outputUrl);
487 // URL内で特殊な意味を持つ記号は元の文字に変換されることを避けるために二重エスケープする
488 // 参考: Firefoxの losslessDecodeURI() 関数
489 // http://hg.mozilla.org/mozilla-central/annotate/FIREFOX_AURORA_27_BASE/browser/base/content/browser.js#l2128
490 outputUrl = Regex.Replace(outputUrl, @"%(2[3456BCF]|3[ABDF]|40)", @"%25$1", RegexOptions.IgnoreCase);
493 outputUrl = Uri.UnescapeDataString(outputUrl);
497 catch (UriFormatException)
503 public static void MoveArrayItem(int[] values, int idx_fr, int idx_to)
505 var moved_value = values[idx_fr];
506 var num_moved = Math.Abs(idx_fr - idx_to);
510 Array.Copy(values, idx_to, values, idx_to + 1, num_moved);
514 Array.Copy(values, idx_fr + 1, values, idx_fr, num_moved);
517 values[idx_to] = moved_value;
520 public static string EncryptString(string str)
522 if (MyCommon.IsNullOrEmpty(str)) return "";
525 var bytesIn = Encoding.UTF8.GetBytes(str);
527 // DESCryptoServiceProviderオブジェクトの作成
528 using var des = new DESCryptoServiceProvider();
532 var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
534 des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
535 des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
537 MemoryStream? msOut = null;
538 ICryptoTransform? desdecrypt = null;
542 // 暗号化されたデータを書き出すためのMemoryStream
543 msOut = new MemoryStream();
546 desdecrypt = des.CreateEncryptor();
548 // 書き込むためのCryptoStreamの作成
549 using var cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write);
551 // Disposeが重複して呼ばれないようにする
557 cryptStream.Write(bytesIn, 0, bytesIn.Length);
558 cryptStream.FlushFinalBlock();
560 var bytesOut = msTmp.ToArray();
562 // Base64で文字列に変更して結果を返す
563 return Convert.ToBase64String(bytesOut);
568 desdecrypt?.Dispose();
572 public static string DecryptString(string str)
574 if (MyCommon.IsNullOrEmpty(str)) return "";
576 // DESCryptoServiceProviderオブジェクトの作成
577 using var des = new DESCryptoServiceProvider();
581 var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
583 des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
584 des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
586 // Base64で文字列をバイト配列に戻す
587 var bytesIn = Convert.FromBase64String(str);
589 MemoryStream? msIn = null;
590 ICryptoTransform? desdecrypt = null;
591 CryptoStream? cryptStreem = null;
595 // 暗号化されたデータを読み込むためのMemoryStream
596 msIn = new MemoryStream(bytesIn);
598 desdecrypt = des.CreateDecryptor();
599 // 読み込むためのCryptoStreamの作成
600 cryptStreem = new CryptoStream(msIn, desdecrypt, CryptoStreamMode.Read);
602 // Disposeが重複して呼ばれないようにする
606 // 復号化されたデータを取得するためのStreamReader
607 using var srOut = new StreamReader(cryptStreem, Encoding.UTF8);
609 // Disposeが重複して呼ばれないようにする
613 var result = srOut.ReadToEnd();
620 desdecrypt?.Dispose();
621 cryptStreem?.Dispose();
625 public static byte[] ResizeBytesArray(byte[] bytes,
628 var newBytes = new byte[newSize];
629 if (bytes.Length <= newSize)
631 foreach (var i in Enumerable.Range(0, bytes.Length))
633 newBytes[i] = bytes[i];
639 foreach (var i in Enumerable.Range(0, bytes.Length))
641 newBytes[pos] = unchecked((byte)(newBytes[pos] ^ bytes[i]));
643 if (pos >= newBytes.Length)
653 public enum TabUsageType
657 Mentions = 2, // Unique
658 DirectMessage = 4, // Unique
659 Favorites = 8, // Unique
661 LocalQuery = 32, // Pin(no save/no save query/distribute/no update(normal update))
662 Profile = 64, // Pin(save/no distribute/manual update)
663 PublicSearch = 128, // Pin(save/no distribute/auto update)
668 SearchResults = 4096,
671 public static TwitterApiStatus TwitterApiInfo = new();
673 public static bool IsAnimatedGif(string filename)
678 img = Image.FromFile(filename);
679 if (img == null) return false;
680 if (img.RawFormat.Guid == ImageFormat.Gif.Guid)
682 var fd = new FrameDimension(img.FrameDimensionsList[0]);
683 var fd_count = img.GetFrameCount(fd);
705 public static DateTimeUtc DateTimeParse(string input)
709 "ddd MMM dd HH:mm:ss zzzz yyyy",
710 "ddd, d MMM yyyy HH:mm:ss zzzz",
713 if (DateTimeUtc.TryParseExact(input, formats, DateTimeFormatInfo.InvariantInfo, out var result))
716 TraceOut("Parse Error(DateTimeFormat) : " + input);
718 return DateTimeUtc.Now;
721 public static T CreateDataFromJson<T>(string content)
722 => MyCommon.CreateDataFromJson<T>(Encoding.UTF8.GetBytes(content));
724 public static T CreateDataFromJson<T>(byte[] bytes)
726 using var stream = new MemoryStream(bytes);
727 var settings = new DataContractJsonSerializerSettings
729 UseSimpleDictionaryFormat = true,
731 return (T)new DataContractJsonSerializer(typeof(T), settings).ReadObject(stream);
734 public static bool IsNetworkAvailable()
738 return NetworkInterface.GetIsNetworkAvailable();
746 public static bool IsValidEmail(string strIn)
748 var pattern = @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
749 @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$";
751 // Return true if strIn is in valid e-mail format.
752 return Regex.IsMatch(strIn, pattern);
756 /// 指定された修飾キーが押されている状態かを取得します。
758 /// <param name="keys">状態を調べるキー</param>
759 /// <returns><paramref name="keys"/> で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。</returns>
760 public static bool IsKeyDown(params Keys[] keys)
761 => MyCommon.IsKeyDownInternal(Control.ModifierKeys, keys);
763 internal static bool IsKeyDownInternal(Keys modifierKeys, Keys[] targetKeys)
765 foreach (var key in targetKeys)
767 if ((modifierKeys & key) != key)
776 /// アプリケーションのアセンブリ名を取得します。
779 /// VB.NETの<code>My.Application.Info.AssemblyName</code>と(ほぼ)同じ動作をします。
781 /// <returns>アプリケーションのアセンブリ名</returns>
782 public static string GetAssemblyName()
783 => MyCommon.EntryAssembly.GetName().Name;
786 /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
788 /// <param name="orig">対象となる文字列</param>
789 /// <returns>置換後の文字列</returns>
790 public static string ReplaceAppName(string orig)
791 => MyCommon.ReplaceAppName(orig, ApplicationSettings.ApplicationName);
794 /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
796 /// <param name="orig">対象となる文字列</param>
797 /// <param name="appname">アプリケーション名</param>
798 /// <returns>置換後の文字列</returns>
799 public static string ReplaceAppName(string orig, string appname)
800 => orig.Replace("%AppName%", appname);
803 /// 表示用のバージョン番号の文字列を生成する
806 /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-beta1」が出力される
811 public static string GetReadableVersion(string? versionStr = null)
813 var version = Version.Parse(versionStr ?? MyCommon.FileVersion);
815 return GetReadableVersion(version);
819 /// 表示用のバージョン番号の文字列を生成する
822 /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-dev」のように出力される
827 public static string GetReadableVersion(Version version)
829 var versionNum = new[] { version.Major, version.Minor, version.Build, version.Revision };
831 if (versionNum[3] == 0)
833 return string.Format("{0}.{1}.{2}", versionNum[0], versionNum[1], versionNum[2]);
837 versionNum[2] = versionNum[2] + 1;
839 if (versionNum[3] == 1)
840 return string.Format("{0}.{1}.{2}-dev", versionNum[0], versionNum[1], versionNum[2]);
842 return string.Format("{0}.{1}.{2}-dev+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
846 public const string TwitterUrl = "https://twitter.com/";
848 public static string GetStatusUrl(PostClass post)
850 var statusId = post.RetweetedId ?? post.StatusId;
851 return GetStatusUrl(post.ScreenName, statusId.ToTwitterStatusId());
854 public static string GetStatusUrl(string screenName, TwitterStatusId statusId)
855 => TwitterUrl + screenName + "/status/" + statusId.Id;
858 /// 指定された IDictionary を元にクエリ文字列を生成します
860 /// <param name="param">生成するクエリの key-value コレクション</param>
861 public static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> param)
867 .Where(x => x.Value != null)
868 .Select(x => EscapeQueryString(x.Key) + '=' + EscapeQueryString(x.Value));
870 return string.Join("&", query);
873 // .NET 4.5+: Reserved characters のうち、Uriクラスによってエスケープ強制解除されてしまうものも最初から Unreserved として扱う
874 private static readonly HashSet<char> UnreservedChars =
875 new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:");
878 /// 2バイト文字も考慮したクエリ用エンコード
880 /// <param name="stringToEncode">エンコードする文字列</param>
881 /// <returns>エンコード結果文字列</returns>
882 public static string EscapeQueryString(string stringToEncode)
884 var sb = new StringBuilder(stringToEncode.Length * 2);
886 foreach (var b in Encoding.UTF8.GetBytes(stringToEncode))
888 if (UnreservedChars.Contains((char)b))
891 sb.AppendFormat("%{0:X2}", b);
894 return sb.ToString();
898 /// 指定された範囲の整数を昇順に列挙します
901 /// start, start + 1, start + 2, ..., end の範囲の数列を生成します
903 /// <param name="from">数列の先頭の値 (最小値)</param>
904 /// <param name="to">数列の末尾の値 (最大値)</param>
905 /// <returns>整数を列挙する IEnumerable インスタンス</returns>
906 public static IEnumerable<int> CountUp(int from, int to)
909 return Enumerable.Empty<int>();
911 return Enumerable.Range(from, to - from + 1);
915 /// 指定された範囲の整数を降順に列挙します
918 /// start, start - 1, start - 2, ..., end の範囲の数列を生成します
920 /// <param name="from">数列の先頭の値 (最大値)</param>
921 /// <param name="to">数列の末尾の値 (最小値)</param>
922 /// <returns>整数を列挙する IEnumerable インスタンス</returns>
923 public static IEnumerable<int> CountDown(int from, int to)
925 for (var i = from; i >= to; i--)
929 public static IEnumerable<int> CircularCountUp(int length, int startIndex)
932 throw new ArgumentOutOfRangeException(nameof(length));
933 if (startIndex < 0 || startIndex >= length)
934 throw new ArgumentOutOfRangeException(nameof(startIndex));
937 var indices = MyCommon.CountUp(startIndex, length - 1);
939 // 先頭 ... (startIndex - 1)
941 indices = indices.Concat(MyCommon.CountUp(0, startIndex - 1));
946 public static IEnumerable<int> CircularCountDown(int length, int startIndex)
949 throw new ArgumentOutOfRangeException(nameof(length));
950 if (startIndex < 0 || startIndex >= length)
951 throw new ArgumentOutOfRangeException(nameof(startIndex));
954 var indices = MyCommon.CountDown(startIndex, 0);
956 // 末尾 ... (startIndex + 1)
957 if (startIndex != length - 1)
958 indices = indices.Concat(MyCommon.CountDown(length - 1, startIndex + 1));
964 /// 2バイト文字も考慮したUrlエンコード
966 /// <param name="stringToEncode">エンコードする文字列</param>
967 /// <returns>エンコード結果文字列</returns>
968 public static string UrlEncode(string stringToEncode)
970 const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
971 var sb = new StringBuilder();
972 var bytes = Encoding.UTF8.GetBytes(stringToEncode);
974 foreach (var b in bytes)
976 if (UnreservedChars.IndexOf((char)b) != -1)
979 sb.AppendFormat("%{0:X2}", b);
981 return sb.ToString();
984 public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
985 => string.IsNullOrEmpty(value);
987 public static Task OpenInBrowserAsync(IWin32Window? owner, string urlStr)
988 => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, urlStr);
990 public static Task OpenInBrowserAsync(IWin32Window? owner, Uri uri)
991 => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, uri);
993 public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, string urlStr)
995 if (!Uri.TryCreate(urlStr, UriKind.Absolute, out var uri))
997 var message = string.Format(Properties.Resources.CannotOpenUriText, urlStr);
998 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1000 await MyCommon.OpenInBrowserAsync(owner, browserPath, uri);
1003 public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, Uri uri)
1005 if (uri.Scheme != "http" && uri.Scheme != "https")
1007 var message = string.Format(Properties.Resources.CannotOpenUriText, uri.OriginalString);
1008 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1013 if (MyCommon.IsNullOrEmpty(browserPath))
1015 var options = new Windows.System.LauncherOptions
1017 IgnoreAppUriHandlers = true,
1019 await Windows.System.Launcher.LaunchUriAsync(uri, options);
1023 await Task.Run(() =>
1025 var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPath, uri.AbsoluteUri);
1026 Process.Start(startInfo);
1030 catch (Win32Exception ex)
1032 var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message);
1033 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1035 catch (MissingMethodException ex)
1037 // WinRT API で存在しないメソッドを呼び出した(非対応の OS 上で実行した)場合に発生する
1038 var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message);
1039 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1043 public static ProcessStartInfo CreateBrowserProcessStartInfo(string browserPathWithArgs, string url)
1046 if (browserPathWithArgs.StartsWith("\"", StringComparison.Ordinal))
1047 quoteEnd = browserPathWithArgs.IndexOf("\"", 1, StringComparison.Ordinal);
1049 string browserPath, browserArgs;
1050 var isQuoted = quoteEnd != -1;
1053 browserPath = browserPathWithArgs.Substring(1, quoteEnd - 1);
1054 browserArgs = browserPathWithArgs.Substring(quoteEnd + 1).Trim();
1058 browserPath = browserPathWithArgs;
1062 var quotedUrl = "\"" + url.Replace("\"", "\\\"") + "\"";
1063 var args = MyCommon.IsNullOrEmpty(browserArgs) ? quotedUrl : browserArgs + " " + quotedUrl;
1065 return new ProcessStartInfo
1067 FileName = browserPath,
1069 UseShellExecute = false,
1073 public static IEnumerable<(int Start, int End)> ToRangeChunk(IEnumerable<int> values)
1078 foreach (var value in values.OrderBy(x => x))
1087 if (value == end + 1)
1093 yield return (start, end);
1101 yield return (start, end);