OSDN Git Service

DeleteRetweetを使用したリツイートの取消に対応
[opentween/open-tween.git] / OpenTween / Tween.cs
index 1bcf361..e0033aa 100644 (file)
@@ -52,6 +52,7 @@ using System.Threading.Tasks;
 using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Api.DataModel;
+using OpenTween.Api.TwitterV2;
 using OpenTween.Connection;
 using OpenTween.MediaUploadServices;
 using OpenTween.Models;
@@ -80,15 +81,6 @@ namespace OpenTween
         /// <summary>プレビュー区切り位置</summary>
         private int mySpDis3;
 
-        /// <summary>アイコンサイズ</summary>
-        /// <remarks>
-        /// 現在は16、24、48の3種類。将来直接数字指定可能とする
-        /// 注:24x24の場合に26と指定しているのはMSゴシック系フォントのための仕様
-        /// </remarks>
-        private int iconSz;
-
-        private bool iconCol; // 1列表示の時true(48サイズのとき)
-
         // 雑多なフラグ類
         private bool initial; // true:起動時処理中
         private bool initialLayout = true;
@@ -106,10 +98,10 @@ namespace OpenTween
         private readonly object syncObject = new(); // ロック用
 
         private const string DetailHtmlFormatHead =
-            "<head><meta http-equiv=\"X-UA-Compatible\" content=\"IE=8\">"
-            + "<style type=\"text/css\"><!-- "
+            """<head><meta http-equiv="X-UA-Compatible" content="IE=8">"""
+            + """<style type="text/css"><!-- """
             + "body, p, pre {margin: 0;} "
-            + "body {font-family: \"%FONT_FAMILY%\", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} "
+            + """body {font-family: "%FONT_FAMILY%", "Segoe UI Emoji", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} """
             + "pre {font-family: inherit;} "
             + "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
             + "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
@@ -132,9 +124,11 @@ namespace OpenTween
         private bool soundfileListup = false;
         private FormWindowState formWindowState = FormWindowState.Normal; // フォームの状態保存用 通知領域からアイコンをクリックして復帰した際に使用する
 
+        // 設定ファイル
+        private readonly SettingManager settings;
+
         // twitter解析部
-        private readonly TwitterApi twitterApi = new(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret);
-        private Twitter tw = null!;
+        private readonly Twitter tw;
 
         // Growl呼び出し部
         private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName);
@@ -155,101 +149,14 @@ namespace OpenTween
         public HashtagManage HashMgr = null!;
 
         // 表示フォント、色、アイコン
-
-        /// <summary>未読用フォント</summary>
-        private Font fntUnread = null!;
-
-        /// <summary>未読用文字色</summary>
-        private Color clUnread;
-
-        /// <summary>既読用フォント</summary>
-        private Font fntReaded = null!;
-
-        /// <summary>既読用文字色</summary>
-        private Color clReaded;
-
-        /// <summary>Fav用文字色</summary>
-        private Color clFav;
-
-        /// <summary>片思い用文字色</summary>
-        private Color clOWL;
-
-        /// <summary>Retweet用文字色</summary>
-        private Color clRetweet;
-
-        /// <summary>選択中の行用文字色</summary>
-        private readonly Color clHighLight = Color.FromKnownColor(KnownColor.HighlightText);
-
-        /// <summary>発言詳細部用フォント</summary>
-        private Font fntDetail = null!;
-
-        /// <summary>発言詳細部用色</summary>
-        private Color clDetail;
-
-        /// <summary>発言詳細部用リンク文字色</summary>
-        private Color clDetailLink;
-
-        /// <summary>発言詳細部用背景色</summary>
-        private Color clDetailBackcolor;
-
-        /// <summary>自分の発言用背景色</summary>
-        private Color clSelf;
-
-        /// <summary>自分宛返信用背景色</summary>
-        private Color clAtSelf;
-
-        /// <summary>選択発言者の他の発言用背景色</summary>
-        private Color clTarget;
-
-        /// <summary>選択発言中の返信先用背景色</summary>
-        private Color clAtTarget;
-
-        /// <summary>選択発言者への返信発言用背景色</summary>
-        private Color clAtFromTarget;
-
-        /// <summary>選択発言の唯一@先</summary>
-        private Color clAtTo;
-
-        /// <summary>リスト部通常発言背景色</summary>
-        private Color clListBackcolor;
-
-        /// <summary>入力欄背景色</summary>
-        private Color clInputBackcolor;
-
-        /// <summary>入力欄文字色</summary>
-        private Color clInputFont;
-
-        /// <summary>入力欄フォント</summary>
-        private Font fntInputFont = null!;
+        private ThemeManager themeManager;
 
         /// <summary>アイコン画像リスト</summary>
-        private ImageCache iconCache = null!;
-
-        /// <summary>タスクトレイアイコン:通常時 (At.ico)</summary>
-        private Icon nIconAt = null!;
-
-        /// <summary>タスクトレイアイコン:通信エラー時 (AtRed.ico)</summary>
-        private Icon nIconAtRed = null!;
-
-        /// <summary>タスクトレイアイコン:オフライン時 (AtSmoke.ico)</summary>
-        private Icon nIconAtSmoke = null!;
-
-        /// <summary>タスクトレイアイコン:更新中 (Refresh.ico)</summary>
-        private Icon[] nIconRefresh = new Icon[4];
+        private readonly ImageCache iconCache;
 
-        /// <summary>未読のあるタブ用アイコン (Tab.ico)</summary>
-        private Icon tabIcon = null!;
+        private readonly IconAssetsManager iconAssets;
 
-        /// <summary>画面左上のアイコン (Main.ico)</summary>
-        private Icon mainIcon = null!;
-
-        private Icon replyIcon = null!;
-        private Icon replyIconBlink = null!;
-
-        private readonly ImageList listViewImageList = new(); // ListViewItemの高さ変更用
-
-        private PostClass? anchorPost;
-        private bool anchorFlag; // true:関連発言移動中(関連移動以外のオペレーションをするとfalseへ。trueだとリスト背景色をアンカー発言選択中として描画)
+        private readonly ThumbnailGenerator thumbGenerator;
 
         /// <summary>発言履歴</summary>
         private readonly List<StatusTextHistory> history = new();
@@ -260,82 +167,23 @@ namespace OpenTween
         // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない)
 
         /// <summary>リプライ先のステータスID・スクリーン名</summary>
-        private (long StatusId, string ScreenName)? inReplyTo = null;
+        private (PostId StatusId, string ScreenName)? inReplyTo = null;
 
         // 時速表示用
         private readonly List<DateTimeUtc> postTimestamps = new();
         private readonly List<DateTimeUtc> favTimestamps = new();
 
         // 以下DrawItem関連
-        private readonly SolidBrush brsHighLight = new(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!;
-
-        /// <summary>Listにフォーカスないときの選択行の背景色</summary>
-        private readonly SolidBrush brsDeactiveSelection = new(Color.FromKnownColor(KnownColor.ButtonFace));
-
         private readonly StringFormat sfTab = new();
 
         //////////////////////////////////////////////////////////////////////////////////////////////////////////
-        private TabInformations statuses = null!;
-
-        /// <summary>
-        /// 現在表示している発言一覧の <see cref="ListView"/> に対するキャッシュ
-        /// </summary>
-        /// <remarks>
-        /// キャッシュクリアのために null が代入されることがあるため、
-        /// 使用する場合には <see cref="listItemCache"/> に対して直接メソッド等を呼び出さずに
-        /// 一旦ローカル変数に代入してから参照すること。
-        /// </remarks>
-        private ListViewItemCache? listItemCache = null;
-
-        /// <param name="TargetList">アイテムをキャッシュする対象の <see cref="ListView"/></param>
-        /// <param name="StartIndex">キャッシュする範囲の開始インデックス</param>
-        /// <param name="EndIndex">キャッシュする範囲の終了インデックス</param>
-        /// <param name="Cache">ャッシュされた範囲に対応する <see cref="ListViewItem"/> と <see cref="PostClass"/> の組</param>
-        internal record class ListViewItemCache(
-            ListView TargetList,
-            int StartIndex,
-            int EndIndex,
-            (ListViewItem, PostClass)[] Cache
-        )
-        {
-            /// <summary>キャッシュされたアイテムの件数</summary>
-            public int Count
-                => this.EndIndex - this.StartIndex + 1;
-
-            /// <summary>指定されたインデックスがキャッシュの範囲内であるか判定します</summary>
-            /// <returns><paramref name="index"/> がキャッシュの範囲内であれば true、それ以外は false</returns>
-            public bool Contains(int index)
-                => index >= this.StartIndex && index <= this.EndIndex;
 
-            /// <summary>指定されたインデックスの範囲が全てキャッシュの範囲内であるか判定します</summary>
-            /// <returns><paramref name="rangeStart"/> から <paramref name="rangeEnd"/> の範囲が全てキャッシュの範囲内であれば true、それ以外は false</returns>
-            public bool IsSupersetOf(int rangeStart, int rangeEnd)
-                => rangeStart >= this.StartIndex && rangeEnd <= this.EndIndex;
+        /// <summary>発言保持クラス</summary>
+        private readonly TabInformations statuses;
 
-            /// <summary>指定されたインデックスの <see cref="ListViewItem"/> と <see cref="PostClass"/> をキャッシュから取得することを試みます</summary>
-            /// <returns>取得に成功すれば true、それ以外は false</returns>
-            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 TimelineListViewCache? listCache;
+        private TimelineListViewDrawer? listDrawer;
+        private readonly Dictionary<string, TimelineListViewState> listViewState = new();
 
         private bool isColumnChanged = false;
 
@@ -355,10 +203,10 @@ namespace OpenTween
         //////////////////////////////////////////////////////////////////////////////////////////////////////////
 
         private readonly TimelineScheduler timelineScheduler = new();
-        private DebounceTimer selectionDebouncer = null!;
-        private DebounceTimer saveConfigDebouncer = null!;
+        private readonly DebounceTimer selectionDebouncer;
+        private readonly DebounceTimer saveConfigDebouncer;
 
-        private string recommendedStatusFooter = null!;
+        private readonly string recommendedStatusFooter;
         private bool urlMultibyteSplit = false;
         private bool preventSmsCommand = true;
 
@@ -371,8 +219,8 @@ namespace OpenTween
         private List<UrlUndo>? urlUndoBuffer = null;
 
         private readonly record struct ReplyChain(
-            long OriginalId,
-            long InReplyToId,
+            PostId OriginalId,
+            PostId InReplyToId,
             TabModel OriginalTab
         );
 
@@ -397,6 +245,9 @@ namespace OpenTween
         public PostClass? CurrentPost
             => this.CurrentTab.SelectedPost;
 
+        public bool Use2ColumnsMode
+            => this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2;
+
         /// <summary>検索処理タイプ</summary>
         internal enum SEARCHTYPE
         {
@@ -407,9 +258,11 @@ namespace OpenTween
 
         private readonly record struct StatusTextHistory(
             string Status,
-            (long StatusId, string ScreenName)? InReplyTo = null
+            (PostId StatusId, string ScreenName)? InReplyTo = null
         );
 
+        private readonly HookGlobalHotkey hookGlobalHotkey;
+
         private void TweenMain_Activated(object sender, EventArgs e)
         {
             // 画面がアクティブになったら、発言欄の背景色戻す
@@ -439,41 +292,13 @@ namespace OpenTween
                 // 後始末
                 this.SearchDialog.Dispose();
                 this.urlDialog.Dispose();
-                this.nIconAt?.Dispose();
-                this.nIconAtRed?.Dispose();
-                this.nIconAtSmoke?.Dispose();
-                foreach (var iconRefresh in this.nIconRefresh)
-                {
-                    iconRefresh?.Dispose();
-                }
-                this.tabIcon?.Dispose();
-                this.mainIcon?.Dispose();
-                this.replyIcon?.Dispose();
-                this.replyIconBlink?.Dispose();
-                this.listViewImageList.Dispose();
-                this.brsHighLight.Dispose();
-                this.brsBackColorMine?.Dispose();
-                this.brsBackColorAt?.Dispose();
-                this.brsBackColorYou?.Dispose();
-                this.brsBackColorAtYou?.Dispose();
-                this.brsBackColorAtFromTarget?.Dispose();
-                this.brsBackColorAtTo?.Dispose();
-                this.brsBackColorNone?.Dispose();
-                this.brsDeactiveSelection?.Dispose();
+                this.themeManager.Dispose();
                 this.sfTab.Dispose();
 
                 this.timelineScheduler.Dispose();
                 this.workerCts.Cancel();
-
-                if (this.iconCache != null)
-                {
-                    this.iconCache.CancelAsync();
-                    this.iconCache.Dispose();
-                }
-
                 this.thumbnailTokenSource?.Dispose();
 
-                this.twitterApi.Dispose();
                 this.hookGlobalHotkey.Dispose();
             }
 
@@ -485,96 +310,6 @@ namespace OpenTween
             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();
@@ -582,7 +317,7 @@ namespace OpenTween
             ColumnHeader[]? columns = null;
             try
             {
-                if (this.iconCol)
+                if (this.Use2ColumnsMode)
                 {
                     columns = new[]
                     {
@@ -595,10 +330,10 @@ namespace OpenTween
 
                     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;
                     }
@@ -632,28 +367,12 @@ namespace OpenTween
 
                     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
@@ -712,7 +431,7 @@ namespace OpenTween
                 _ => 0,
             };
 
-            if (this.iconCol)
+            if (this.Use2ColumnsMode)
             {
                 if (this.statuses.SortOrder == SortOrder.Descending)
                 {
@@ -740,149 +459,79 @@ namespace OpenTween
             }
         }
 
-        private void InitializeTraceFrag()
+        public TweenMain(
+            SettingManager settingManager,
+            TabInformations tabInfo,
+            Twitter twitter,
+            ImageCache imageCache,
+            IconAssetsManager iconAssets,
+            ThumbnailGenerator thumbGenerator
+        )
         {
-#if DEBUG
-            this.TraceOutToolStripMenuItem.Checked = true;
-            MyCommon.TraceFlag = true;
-#endif
-            if (!MyCommon.FileVersion.EndsWith("0", StringComparison.Ordinal))
+            this.settings = settingManager;
+            this.statuses = tabInfo;
+            this.tw = twitter;
+            this.iconCache = imageCache;
+            this.iconAssets = iconAssets;
+            this.thumbGenerator = thumbGenerator;
+
+            this.InitializeComponent();
+
+            if (!this.DesignMode)
             {
-                this.TraceOutToolStripMenuItem.Checked = true;
-                MyCommon.TraceFlag = true;
+                // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策
+                this.StatusText.Dock = DockStyle.Fill;
             }
-        }
 
-        private void TweenMain_Load(object sender, EventArgs e)
-        {
+            this.hookGlobalHotkey = new HookGlobalHotkey(this);
+
+            this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed;
+            this.gh.NotifyClicked += this.GrowlHelper_Callback;
+
+            // メイリオフォント指定時にタブの最小幅が広くなる問題の対策
+            this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40);
+
+            this.ImageSelector.Visible = false;
+            this.ImageSelector.Enabled = false;
+            this.ImageSelector.FilePickDialog = this.OpenFileDialog1;
+
+            this.workerProgress = new Progress<string>(x => this.StatusLabel.Text = x);
+
+            this.ReplaceAppName();
+            this.InitializeShortcuts();
+
             this.ignoreConfigSave = true;
             this.Visible = false;
 
-            if (ApplicationEvents.StartupOptions.ContainsKey("d"))
-                MyCommon.TraceFlag = true;
-
-            this.InitializeTraceFrag();
+            this.TraceOutToolStripMenuItem.Checked = MyCommon.TraceFlag;
 
             Microsoft.Win32.SystemEvents.PowerModeChanged += this.SystemEvents_PowerModeChanged;
 
             Regex.CacheSize = 100;
 
-            // 発言保持クラス
-            this.statuses = TabInformations.GetInstance();
-
             // アイコン設定
-            this.LoadIcons();
-            this.Icon = this.mainIcon;              // メインフォーム(TweenMain)
-            this.NotifyIcon1.Icon = this.nIconAt;      // タスクトレイ
-            this.TabImage.Images.Add(this.tabIcon);    // タブ見出し
+            this.Icon = this.iconAssets.IconMain; // メインフォーム(TweenMain)
+            this.NotifyIcon1.Icon = this.iconAssets.IconTray; // タスクトレイ
+            this.TabImage.Images.Add(this.iconAssets.IconTab); // タブ見出し
 
             // <<<<<<<<<設定関連>>>>>>>>>
             // 設定読み出し
             this.LoadConfig();
 
             // 現在の DPI と設定保存時の DPI との比を取得する
-            var configScaleFactor = SettingManager.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
-
-            // UIフォント設定
-            var fontUIGlobal = SettingManager.Local.FontUIGlobal;
-            if (fontUIGlobal != null)
-            {
-                OTBaseForm.GlobalFont = fontUIGlobal;
-                this.Font = fontUIGlobal;
-            }
-
-            // 不正値チェック
-            if (!ApplicationEvents.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;
-
-                if (SettingManager.Common.PubSearchPeriod < 30 && SettingManager.Common.PubSearchPeriod > 0)
-                    SettingManager.Common.PubSearchPeriod = 30;
-
-                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;
-            }
-
-            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;
-
-            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;
-
-            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;
-
-            // 廃止サービスが選択されていた場合ux.nuへ読み替え
-            if (SettingManager.Common.AutoShortUrlFirst < 0)
-                SettingManager.Common.AutoShortUrlFirst = MyCommon.UrlConverter.Uxnu;
-
-            TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost;
-            this.tw = new Twitter(this.twitterApi);
+            var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
 
             // 認証関連
-            if (MyCommon.IsNullOrEmpty(SettingManager.Common.Token)) SettingManager.Common.UserName = "";
-            this.tw.Initialize(SettingManager.Common.Token, SettingManager.Common.TokenSecret, SettingManager.Common.UserName, SettingManager.Common.UserId);
+            var account = this.settings.Common.SelectedAccount;
+            if (account != null)
+                this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+            else
+                this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
 
             this.initial = true;
 
-            Networking.Initialize();
-
-            var saveRequired = false;
-            var firstRun = false;
-
-            // ユーザー名、パスワードが未設定なら設定画面を表示(初回起動時など)
-            if (MyCommon.IsNullOrEmpty(this.tw.Username))
-            {
-                saveRequired = true;
-                firstRun = true;
-
-                // 設定せずにキャンセルされたか、設定されたが依然ユーザー名が未設定ならプログラム終了
-                if (this.ShowSettingDialog(showTaskbarIcon: true) != DialogResult.OK ||
-                    MyCommon.IsNullOrEmpty(this.tw.Username))
-                {
-                    Application.Exit();  // 強制終了
-                    return;
-                }
-            }
-
-            // 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;
-
-            TwitterApiConnection.RestApiHost = SettingManager.Common.TwitterApiHost;
-            this.tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck;
-            this.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 への最初のアクセスになるようにすること
@@ -902,62 +551,33 @@ namespace OpenTween
 
             // サムネイル関連の初期化
             // プロキシ設定等の通信まわりの初期化が済んでから処理する
-            ThumbnailGenerator.InitializeGenerator();
-
-            var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
-            imgazyobizinet.Enabled = SettingManager.Common.EnableImgAzyobuziNet;
-            imgazyobizinet.DisabledInDM = SettingManager.Common.ImgAzyobuziNetDisabledInDM;
+            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.twitterApi.Connection;
+            Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection;
 
             // 画像投稿サービス
-            this.ImageSelector.Initialize(this.tw, this.tw.Configuration, SettingManager.Common.UseImageServiceName, SettingManager.Common.UseImageService);
+            this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
+            this.ImageSelector.Model.SelectMediaService(this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
+
+            this.tweetThumbnail1.Model.Initialize(this.thumbGenerator);
 
             // ハッシュタグ/@id関連
-            this.AtIdSupl = new AtIdSupplement(SettingManager.AtIdList.AtIdList, "@");
-            this.HashSupl = new AtIdSupplement(SettingManager.Common.HashTags, "#");
+            this.AtIdSupl = new AtIdSupplement(this.settings.AtIdList.AtIdList, "@");
+            this.HashSupl = new AtIdSupplement(this.settings.Common.HashTags, "#");
             this.HashMgr = new HashtagManage(this.HashSupl,
-                                    SettingManager.Common.HashTags.ToArray(),
-                                    SettingManager.Common.HashSelected,
-                                    SettingManager.Common.HashIsPermanent,
-                                    SettingManager.Common.HashIsHead,
-                                    SettingManager.Common.HashIsNotAddToAtReply);
+                                    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.iconCache = new ImageCache();
-            this.tweetDetailsView.IconCache = this.iconCache;
-
             // フォント&文字色&背景色保持
-            this.fntUnread = SettingManager.Local.FontUnread;
-            this.clUnread = SettingManager.Local.ColorUnread;
-            this.fntReaded = SettingManager.Local.FontRead;
-            this.clReaded = SettingManager.Local.ColorRead;
-            this.clFav = SettingManager.Local.ColorFav;
-            this.clOWL = SettingManager.Local.ColorOWL;
-            this.clRetweet = SettingManager.Local.ColorRetweet;
-            this.fntDetail = SettingManager.Local.FontDetail;
-            this.clDetail = SettingManager.Local.ColorDetail;
-            this.clDetailLink = SettingManager.Local.ColorDetailLink;
-            this.clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor;
-            this.clSelf = SettingManager.Local.ColorSelf;
-            this.clAtSelf = SettingManager.Local.ColorAtSelf;
-            this.clTarget = SettingManager.Local.ColorTarget;
-            this.clAtTarget = SettingManager.Local.ColorAtTarget;
-            this.clAtFromTarget = SettingManager.Local.ColorAtFromTarget;
-            this.clAtTo = SettingManager.Local.ColorAtTo;
-            this.clListBackcolor = SettingManager.Local.ColorListBackcolor;
-            this.clInputBackcolor = SettingManager.Local.ColorInputBackcolor;
-            this.clInputFont = SettingManager.Local.ColorInputFont;
-            this.fntInputFont = SettingManager.Local.FontInputFont;
-
-            this.brsBackColorMine = new SolidBrush(this.clSelf);
-            this.brsBackColorAt = new SolidBrush(this.clAtSelf);
-            this.brsBackColorYou = new SolidBrush(this.clTarget);
-            this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
-            this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
-            this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
-            this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
+            this.themeManager = new(this.settings.Local);
+            this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager);
 
             // StringFormatオブジェクトへの事前設定
             this.sfTab.Alignment = StringAlignment.Center;
@@ -977,20 +597,20 @@ namespace OpenTween
             this.urlDialog.Owner = this;
 
             // 新着バルーン通知のチェック状態設定
-            this.NewPostPopMenuItem.Checked = SettingManager.Common.NewAllPop;
+            this.NewPostPopMenuItem.Checked = this.settings.Common.NewAllPop;
             this.NotifyFileMenuItem.Checked = this.NewPostPopMenuItem.Checked;
 
             // 新着取得時のリストスクロールをするか。trueならスクロールしない
-            this.ListLockMenuItem.Checked = SettingManager.Common.ListLock;
-            this.LockListFileMenuItem.Checked = SettingManager.Common.ListLock;
+            this.ListLockMenuItem.Checked = this.settings.Common.ListLock;
+            this.LockListFileMenuItem.Checked = this.settings.Common.ListLock;
             // サウンド再生(タブ別設定より優先)
-            this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound;
-            this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound;
+            this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
+            this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
 
             // ウィンドウ設定
-            this.ClientSize = ScaleBy(configScaleFactor, SettingManager.Local.FormSize);
+            this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize);
             this.mySize = this.ClientSize; // サイズ保持(最小化・最大化されたまま終了した場合の対応用)
-            this.myLoc = SettingManager.Local.FormLocation;
+            this.myLoc = this.settings.Local.FormLocation;
             // タイトルバー領域
             if (this.WindowState != FormWindowState.Minimized)
             {
@@ -1012,34 +632,26 @@ namespace OpenTween
                 }
                 this.DesktopLocation = this.myLoc;
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
-            this.mySpDis = ScaleBy(configScaleFactor.Height, SettingManager.Local.SplitterDistance);
-            this.mySpDis2 = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight);
-            if (SettingManager.Local.PreviewDistance == -1)
-            {
-                this.mySpDis3 = this.mySize.Width - ScaleBy(this.CurrentScaleFactor.Width, 150);
-                if (this.mySpDis3 < 1) this.mySpDis3 = ScaleBy(this.CurrentScaleFactor.Width, 50);
-                SettingManager.Local.PreviewDistance = this.mySpDis3;
-            }
-            else
-            {
-                this.mySpDis3 = ScaleBy(configScaleFactor.Width, SettingManager.Local.PreviewDistance);
-            }
-            this.PlaySoundMenuItem.Checked = SettingManager.Common.PlaySound;
-            this.PlaySoundFileMenuItem.Checked = SettingManager.Common.PlaySound;
+            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.fntInputFont;
-            this.StatusText.ForeColor = this.clInputFont;
+            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;
 
             // 必要であれば、発言一覧と発言詳細部・入力欄の上下を入れ替える
-            this.SplitContainer1.IsPanelInverted = !SettingManager.Common.StatusAreaAtBottom;
+            this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom;
 
             // 全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定)
-            if (SettingManager.Common.UnreadManage == false)
+            if (this.settings.Common.UnreadManage == false)
             {
                 this.ReadedStripMenuItem.Enabled = false;
                 this.UnreadStripMenuItem.Enabled = false;
@@ -1065,8 +677,8 @@ namespace OpenTween
             this.SplitContainer2.Panel2.AccessibleName = "";
 
             ////////////////////////////////////////////////////////////////////////////////
-            var sortOrder = (SortOrder)SettingManager.Common.SortOrder;
-            var mode = SettingManager.Common.SortColumn switch
+            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ソートに読み替え
@@ -1080,7 +692,7 @@ namespace OpenTween
             this.statuses.SetSortMode(mode, sortOrder);
             ////////////////////////////////////////////////////////////////////////////////
 
-            this.ApplyListViewIconSize(SettingManager.Common.IconSize);
+            this.ApplyListViewIconSize(this.settings.Common.IconSize);
 
             // <<<<<<<<タブ関連>>>>>>>
             foreach (var tab in this.statuses.Tabs)
@@ -1089,7 +701,7 @@ namespace OpenTween
                     throw new TabException(Properties.Resources.TweenMain_LoadText1);
             }
 
-            this.statuses.SelectTab(this.ListTab.SelectedTab.Text);
+            this.ListTabSelect(this.ListTab.SelectedTab);
 
             // タブの位置を調整する
             this.SetTabAlignment();
@@ -1097,7 +709,7 @@ namespace OpenTween
             MyCommon.TwitterApiInfo.AccessLimitUpdated += this.TwitterApiStatus_AccessLimitUpdated;
             Microsoft.Win32.SystemEvents.TimeChanged += this.SystemEvents_TimeChanged;
 
-            if (SettingManager.Common.TabIconDisp)
+            if (this.settings.Common.TabIconDisp)
             {
                 this.ListTab.DrawMode = TabDrawMode.Normal;
             }
@@ -1108,23 +720,23 @@ namespace OpenTween
                 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;
 
-                this.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)
+            if (this.settings.Common.IsUseNotifyGrowl)
                 this.gh.RegisterGrowl();
 
             this.StatusLabel.Text = Properties.Resources.Form1_LoadText1;       // 画面右下の状態表示を変更
@@ -1132,7 +744,7 @@ namespace OpenTween
             this.SetMainWindowTitle();
             this.SetNotifyIconText();
 
-            if (!SettingManager.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized)
+            if (!this.settings.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized)
             {
                 this.Visible = true;
             }
@@ -1164,18 +776,8 @@ namespace OpenTween
 
             this.ignoreConfigSave = false;
             this.TweenMain_Resize(this, EventArgs.Empty);
-            if (saveRequired) this.SaveConfigsAll(false);
-
-            foreach (var ua in SettingManager.Common.UserAccounts)
-            {
-                if (ua.UserId == 0 && ua.Username.Equals(this.tw.Username, StringComparison.InvariantCultureIgnoreCase))
-                {
-                    ua.UserId = this.tw.UserId;
-                    break;
-                }
-            }
 
-            if (firstRun)
+            if (this.settings.IsFirstRun)
             {
                 // 初回起動時だけ右下のメニューを目立たせる
                 this.HashStripSplitButton.ShowDropDown();
@@ -1184,15 +786,18 @@ namespace OpenTween
 
         private void InitDetailHtmlFormat()
         {
-            var htmlTemplate = SettingManager.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
+            var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
+
+            static string ColorToRGBString(Color color)
+                => $"{color.R},{color.G},{color.B}";
 
             this.detailHtmlFormatPreparedTemplate = htmlTemplate
-                .Replace("%FONT_FAMILY%", this.fntDetail.Name)
-                .Replace("%FONT_SIZE%", this.fntDetail.Size.ToString())
-                .Replace("%FONT_COLOR%", $"{this.clDetail.R},{this.clDetail.G},{this.clDetail.B}")
-                .Replace("%LINK_COLOR%", $"{this.clDetailLink.R},{this.clDetailLink.G},{this.clDetailLink.B}")
-                .Replace("%BG_COLOR%", $"{this.clDetailBackcolor.R},{this.clDetailBackcolor.G},{this.clDetailBackcolor.B}")
-                .Replace("%BG_REPLY_COLOR%", $"{this.clAtTo.R}, {this.clAtTo.G}, {this.clAtTo.B}");
+                .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)
@@ -1229,71 +834,7 @@ namespace OpenTween
 
         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.LoadTabsFromSettings(this.settings.Tabs);
             this.statuses.AddDefaultTabs();
         }
 
@@ -1307,12 +848,12 @@ namespace OpenTween
             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);
 
@@ -1348,14 +889,11 @@ namespace OpenTween
 
         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;
@@ -1368,39 +906,21 @@ namespace OpenTween
             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();
-
-                        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.listCache.PurgeCache();
+                    this.listCache.UpdateListSize();
 
-                        // 選択位置などを復元
-                        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())
                     {
@@ -1416,7 +936,7 @@ namespace OpenTween
             }
 
             // スクロール位置を復元
-            this.RestoreListViewScroll(curListView, curTabModel, curListScroll);
+            currentListViewState.RestoreScroll();
 
             // 新着通知
             this.NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm);
@@ -1427,205 +947,6 @@ namespace OpenTween
             this.HashSupl.AddRangeItem(this.tw.GetHashList());
         }
 
-        internal readonly record struct ListViewScroll(
-            ScrollLockMode ScrollLockMode,
-            long? TopItemStatusId
-        );
-
-        internal enum ScrollLockMode
-        {
-            /// <summary>固定しない</summary>
-            None,
-
-            /// <summary>最上部に固定する</summary>
-            FixedToTop,
-
-            /// <summary>最下部に固定する</summary>
-            FixedToBottom,
-
-            /// <summary><see cref="ListViewScroll.TopItemStatusId"/> の位置に固定する</summary>
-            FixedToItem,
-        }
-
-        /// <summary>
-        /// <see cref="ListView"/> のスクロール位置に関する情報を <see cref="ListViewScroll"/> として返します
-        /// </summary>
-        private ListViewScroll SaveListViewScroll(DetailsListView listView, TabModel tab)
-        {
-            var lockMode = this.GetScrollLockMode(listView);
-            long? topItemStatusId = null;
-
-            if (lockMode == ScrollLockMode.FixedToItem)
-            {
-                var topItemIndex = listView.TopItem?.Index ?? -1;
-                if (topItemIndex != -1 && topItemIndex < tab.AllCount)
-                    topItemStatusId = tab.GetStatusIdAt(topItemIndex);
-            }
-
-            return new ListViewScroll
-            {
-                ScrollLockMode = lockMode,
-                TopItemStatusId = topItemStatusId,
-            };
-        }
-
-        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 readonly record struct ListViewSelection(
-            long[]? SelectedStatusIds,
-            long? SelectionMarkStatusId,
-            long? FocusedStatusId
-        );
-
-        /// <summary>
-        /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
-        /// </summary>
-        private IReadOnlyDictionary<string, ListViewSelection> SaveListViewSelection()
-        {
-            var listsDict = new Dictionary<string, ListViewSelection>();
-
-            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;
-        }
-
-        /// <summary>
-        /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
-        /// </summary>
-        private ListViewSelection SaveListViewSelection(DetailsListView listView, TabModel tab)
-        {
-            if (listView.VirtualListSize == 0)
-            {
-                return new ListViewSelection
-                {
-                    SelectedStatusIds = Array.Empty<long>(),
-                    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;
-        }
-
-        /// <summary>
-        /// <see cref="SaveListViewScroll"/> によって保存されたスクロール位置を復元します
-        /// </summary>
-        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;
-            }
-        }
-
-        /// <summary>
-        /// <see cref="SaveListViewSelection"/> によって保存された選択状態を復元します
-        /// </summary>
-        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();
-
-            var focusedIndex = -1;
-            if (listSelection.FocusedStatusId != null)
-                focusedIndex = tab.IndexOf(listSelection.FocusedStatusId.Value);
-
-            var selectionMarkIndex = -1;
-            if (listSelection.SelectionMarkStatusId != null)
-                selectionMarkIndex = tab.IndexOf(listSelection.SelectionMarkStatusId.Value);
-
-            this.SelectListItem(listView, selectedIndices, focusedIndex, selectionMarkIndex);
-        }
-
         private bool BalloonRequired()
         {
             if (this.initial)
@@ -1639,7 +960,7 @@ namespace OpenTween
                 return false;
 
             // 「画面最小化・アイコン時のみバルーンを表示する」が有効
-            if (SettingManager.Common.LimitBalloon)
+            if (this.settings.Common.LimitBalloon)
             {
                 if (this.WindowState != FormWindowState.Minimized && this.Visible && Form.ActiveForm != null)
                     return false;
@@ -1650,7 +971,7 @@ namespace OpenTween
 
         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 == this.tw.UserId))
                     return;
@@ -1662,7 +983,7 @@ namespace OpenTween
                 if (notifyPosts != null && notifyPosts.Length > 0)
                 {
                     // Growlは一個ずつばらして通知。ただし、3ポスト以上あるときはまとめる
-                    if (SettingManager.Common.IsUseNotifyGrowl)
+                    if (this.settings.Common.IsUseNotifyGrowl)
                     {
                         var sb = new StringBuilder();
                         var reply = false;
@@ -1679,7 +1000,7 @@ namespace OpenTween
                             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(" : ");
@@ -1696,7 +1017,7 @@ namespace OpenTween
 
                             var title = new StringBuilder();
                             GrowlHelper.NotifyType nt;
-                            if (SettingManager.Common.DispUsername)
+                            if (this.settings.Common.DispUsername)
                             {
                                 title.Append(this.tw.Username);
                                 title.Append(" - ");
@@ -1727,7 +1048,7 @@ namespace OpenTween
                             if (MyCommon.IsNullOrEmpty(bText)) return;
 
                             var image = this.iconCache.TryGetFromCache(post.ImageUrl);
-                            this.gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl);
+                            this.gh.Notify(nt, post.StatusId.Id, title.ToString(), bText, image?.Image, post.ImageUrl);
                         }
                     }
                     else
@@ -1740,7 +1061,7 @@ namespace OpenTween
                             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(" : ");
@@ -1754,7 +1075,7 @@ namespace OpenTween
 
                         var title = new StringBuilder();
                         ToolTipIcon ntIcon;
-                        if (SettingManager.Common.DispUsername)
+                        if (this.settings.Common.DispUsername)
                         {
                             title.Append(this.tw.Username);
                             title.Append(" - ");
@@ -1789,199 +1110,56 @@ namespace OpenTween
                         this.NotifyIcon1.BalloonTipIcon = ntIcon;
                         this.NotifyIcon1.ShowBalloonTip(500);
                     }
-                }
-            }
-
-            // サウンド再生
-            if (!this.initial && SettingManager.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile))
-            {
-                try
-                {
-                    var dir = Application.StartupPath;
-                    if (Directory.Exists(Path.Combine(dir, "Sounds")))
-                    {
-                        dir = Path.Combine(dir, "Sounds");
-                    }
-                    using var player = new SoundPlayer(Path.Combine(dir, soundFile));
-                    player.Play();
-                }
-                catch (Exception)
-                {
-                }
-            }
-
-            // mentions新着時に画面ブリンク
-            if (!this.initial && SettingManager.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null)
-            {
-                NativeMethods.FlashMyWindow(this.Handle, 3);
-            }
-        }
-
-        private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
-        {
-            var listView = this.CurrentListView;
-            if (listView != sender)
-                return;
-
-            var indices = listView.SelectedIndices.Cast<int>().ToArray();
-            this.CurrentTab.SelectPosts(indices);
-
-            if (indices.Length != 1)
-                return;
-
-            var index = indices[0];
-            if (index > listView.VirtualListSize - 1) return;
-
-            this.PushSelectPostChain();
-
-            var post = this.CurrentPost!;
-            this.statuses.SetReadAllTab(post.StatusId, read: true);
-
-            // キャッシュの書き換え
-            this.ChangeCacheStyleRead(true, index); // 既読へ(フォント、文字色)
-
-            this.ColorizeList();
-            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;
-
-            this.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 = this.fntReaded;
-                star = "";
-            }
-            else
-            {
-                fnt = this.fntUnread;
-                star = "★";
-            }
-            if (item.SubItems[5].Text != star)
-                item.SubItems[5].Text = star;
-
-            // 文字色
-            Color cl;
-            if (post.IsFav)
-                cl = this.clFav;
-            else if (post.RetweetedId != null)
-                cl = this.clRetweet;
-            else if (post.IsOwl && (post.IsDm || SettingManager.Common.OneWayLove))
-                cl = this.clOWL;
-            else if (read || !SettingManager.Common.UseUnreadStyle)
-                cl = this.clReaded;
-            else
-                cl = this.clUnread;
+                }
+            }
 
-            if (dList == null || item.Index == -1)
+            // サウンド再生
+            if (!this.initial && this.settings.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile))
             {
-                item.ForeColor = cl;
-                if (SettingManager.Common.UseUnreadStyle)
-                    item.Font = fnt;
+                try
+                {
+                    var dir = Application.StartupPath;
+                    if (Directory.Exists(Path.Combine(dir, "Sounds")))
+                    {
+                        dir = Path.Combine(dir, "Sounds");
+                    }
+                    using var player = new SoundPlayer(Path.Combine(dir, soundFile));
+                    player.Play();
+                }
+                catch (Exception)
+                {
+                }
             }
-            else
+
+            // mentions新着時に画面ブリンク
+            if (!this.initial && this.settings.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null)
             {
-                dList.Update();
-                if (SettingManager.Common.UseUnreadStyle)
-                    dList.ChangeItemFontAndColor(item, cl, fnt);
-                else
-                    dList.ChangeItemForeColor(item, cl);
+                NativeMethods.FlashMyWindow(this.Handle, 3);
             }
         }
 
-        private void ColorizeList()
+        private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
         {
-            // Index:更新対象のListviewItem.Index。Colorを返す。
-            // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
-            PostClass? post;
-            if (this.anchorFlag)
-                post = this.anchorPost;
-            else
-                post = this.CurrentPost;
-
-            if (post == null) return;
-
-            var listCache = this.listItemCache;
-            if (listCache == null)
+            var listView = this.CurrentListView;
+            if (listView != sender)
                 return;
 
-            var listView = (DetailsListView)listCache.TargetList;
-
-            // ValidateRectが呼ばれる前に選択色などの描画を済ませておく
-            listView.Update();
+            var indices = listView.SelectedIndices.Cast<int>().ToArray();
+            this.CurrentTab.SelectPosts(indices);
 
-            foreach (var (listViewItem, cachedPost) in listCache.Cache)
-            {
-                var backColor = this.JudgeColor(post, cachedPost);
-                listView.ChangeItemBackColor(listViewItem, backColor);
-            }
-        }
+            if (indices.Length != 1)
+                return;
 
-        private void ColorizeList(ListViewItem item, PostClass post)
-        {
-            // Index:更新対象のListviewItem.Index。Colorを返す。
-            // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
-            PostClass? basePost;
-            if (this.anchorFlag)
-                basePost = this.anchorPost;
-            else
-                basePost = this.CurrentPost;
+            var index = indices[0];
+            if (index > listView.VirtualListSize - 1) return;
 
-            if (basePost == null) return;
+            this.PushSelectPostChain();
 
-            if (item.Index == -1)
-                item.BackColor = this.JudgeColor(basePost, post);
-            else
-                this.CurrentListView.ChangeItemBackColor(item, this.JudgeColor(basePost, post));
-        }
-
-        private Color JudgeColor(PostClass basePost, PostClass targetPost)
-        {
-            Color cl;
-            if (targetPost.StatusId == basePost.InReplyToStatusId)
-                // @先
-                cl = this.clAtTo;
-            else if (targetPost.IsMe)
-                // 自分=発言者
-                cl = this.clSelf;
-            else if (targetPost.IsReply)
-                // 自分宛返信
-                cl = this.clAtSelf;
-            else if (basePost.ReplyToList.Any(x => x.UserId == targetPost.UserId))
-                // 返信先
-                cl = this.clAtFromTarget;
-            else if (targetPost.ReplyToList.Any(x => x.UserId == basePost.UserId))
-                // その人への返信
-                cl = this.clAtTarget;
-            else if (targetPost.UserId == basePost.UserId)
-                // 発言者
-                cl = this.clTarget;
-            else
-                // その他
-                cl = this.clListBackcolor;
+            var post = this.CurrentPost!;
+            this.statuses.SetReadAllTab(post.StatusId, read: true);
 
-            return cl;
+            this.listCache?.RefreshStyle();
+            await this.selectionDebouncer.Call();
         }
 
         private void StatusTextHistoryBack()
@@ -2054,7 +1232,7 @@ namespace OpenTween
 
             this.history[this.history.Count - 1] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo);
 
-            if (SettingManager.Common.Nicoms)
+            if (this.settings.Common.Nicoms)
             {
                 this.StatusText.SelectionStart = this.StatusText.Text.Length;
                 await this.UrlConvertAsync(MyCommon.UrlConverter.Nicoms);
@@ -2066,7 +1244,7 @@ namespace OpenTween
             var status = new PostStatusParams();
 
             var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
-            if (this.GetRestStatusCount(statusTextCompat) >= 0)
+            if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AppToken.AuthType == APIAuthType.OAuth1)
             {
                 // auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
                 // 収まる場合はこれらのオプションを使用せずに投稿する
@@ -2109,14 +1287,15 @@ namespace OpenTween
                 if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
                     return;
 
-                uploadService = this.ImageSelector.GetService(serviceName);
+                this.ImageSelector.EndSelection();
+                uploadService = this.ImageSelector.Model.GetService(serviceName);
             }
 
             this.inReplyTo = null;
             this.StatusText.Text = "";
             this.history.Add(new StatusTextHistory(""));
             this.hisIdx = this.history.Count - 1;
-            if (!SettingManager.Common.FocusLockToStatusText)
+            if (!this.settings.Common.FocusLockToStatusText)
                 this.CurrentListView.Focus();
             this.urlUndoBuffer = null;
             this.UrlUndoToolStripMenuItem.Enabled = false;  // Undoをできないように設定
@@ -2139,7 +1318,7 @@ namespace OpenTween
 
         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:フォームの×ボタン
                 e.Cancel = true;
@@ -2239,7 +1418,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavAddAsync(long statusId, TabModel tab)
+        private async Task FavAddAsync(PostId statusId, TabModel tab)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -2261,7 +1440,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, long statusId, TabModel tab)
+        private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, PostId statusId, TabModel tab)
         {
             if (ct.IsCancellationRequested)
                 return;
@@ -2281,9 +1460,10 @@ namespace OpenTween
 
                 try
                 {
+                    var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
                     try
                     {
-                        await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId)
+                        await this.tw.Api.FavoritesCreate(twitterStatusId)
                             .IgnoreResponse()
                             .ConfigureAwait(false);
                     }
@@ -2293,9 +1473,9 @@ namespace OpenTween
                         // エラーコード 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(twitterStatusId)
                             .ConfigureAwait(false);
 
                         if (status.Favorited != true)
@@ -2350,7 +1530,7 @@ namespace OpenTween
                 {
                     var idx = tab.IndexOf(statusId);
                     if (idx != -1)
-                        this.ChangeCacheStyleRead(post.IsRead, idx);
+                        this.listCache?.RefreshStyle(idx);
                 }
 
                 var currentPost = this.CurrentPost;
@@ -2359,7 +1539,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavRemoveAsync(IReadOnlyList<long> statusIds, TabModel tab)
+        private async Task FavRemoveAsync(IReadOnlyList<PostId> statusIds, TabModel tab)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -2381,7 +1561,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds, TabModel tab)
+        private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds, TabModel tab)
         {
             if (ct.IsCancellationRequested)
                 return;
@@ -2389,7 +1569,7 @@ namespace OpenTween
             if (!CheckAccountValid())
                 throw new WebApiException("Auth error. Check your account");
 
-            var successIds = new List<long>();
+            var successIds = new List<PostId>();
 
             await Task.Run(async () =>
             {
@@ -2407,9 +1587,11 @@ namespace OpenTween
                     if (!post.IsFav)
                         continue;
 
+                    var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
+
                     try
                     {
-                        await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId)
+                        await this.tw.Api.FavoritesDestroy(twitterStatusId)
                             .IgnoreResponse()
                             .ConfigureAwait(false);
                     }
@@ -2459,11 +1641,8 @@ namespace OpenTween
                         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);
                         }
                     }
 
@@ -2583,7 +1762,7 @@ namespace OpenTween
                     this.StatusText.Focus();
 
                     // 連投モードのときだけEnterイベントが起きないので強制的に背景色を戻す
-                    if (SettingManager.Common.FocusLockToStatusText)
+                    if (this.settings.Common.FocusLockToStatusText)
                         this.StatusText_Enter(this.StatusText, EventArgs.Empty);
                 }
                 return;
@@ -2609,22 +1788,18 @@ namespace OpenTween
             this.SetMainWindowTitle();
 
             // TLに反映
-            if (SettingManager.Common.PostAndGet)
-            {
-                await this.RefreshTabAsync<HomeTabModel>();
-            }
-            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<HomeTabModel>();
         }
 
-        private async Task RetweetAsync(IReadOnlyList<long> statusIds)
+        private async Task RetweetAsync(IReadOnlyList<PostId> statusIds)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -2646,7 +1821,7 @@ namespace OpenTween
             }
         }
 
-        private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds)
+        private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds)
         {
             if (ct.IsCancellationRequested)
                 return;
@@ -2655,10 +1830,10 @@ namespace OpenTween
                 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...");
 
@@ -2691,7 +1866,7 @@ namespace OpenTween
             // 投稿時取得の有無に関わらず追加しておく
             posts.ForEach(post => this.statuses.AddPost(post));
 
-            if (SettingManager.Common.PostAndGet)
+            if (this.settings.Common.PostAndGet)
             {
                 await this.RefreshTabAsync<HomeTabModel>();
             }
@@ -2716,7 +1891,7 @@ namespace OpenTween
                 this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText3;
 
                 this.RefreshTimeline();
-                this.PurgeListViewItemCache();
+                this.listCache?.PurgeCache();
                 this.CurrentListView.Refresh();
             }
             catch (WebApiException ex)
@@ -2784,13 +1959,13 @@ namespace OpenTween
 
                 if (this.tw.Configuration.PhotoSizeLimit != 0)
                 {
-                    foreach (var service in this.ImageSelector.GetServices())
+                    foreach (var (_, service) in this.ImageSelector.Model.MediaServices)
                     {
                         service.UpdateTwitterConfiguration(this.tw.Configuration);
                     }
                 }
 
-                this.PurgeListViewItemCache();
+                this.listCache?.PurgeCache();
                 this.CurrentListView.Refresh();
             }
             catch (WebApiException ex)
@@ -2839,7 +2014,7 @@ namespace OpenTween
 
         private async Task ListItemDoubleClickAction()
         {
-            switch (SettingManager.Common.ListDoubleClickAction)
+            switch (this.settings.Common.ListDoubleClickAction)
             {
                 case MyCommon.ListItemDoubleClickActionType.Reply:
                     this.MakeReplyText();
@@ -2946,18 +2121,6 @@ namespace OpenTween
             }
         }
 
-        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 AuthorOpenInBrowserMenuItem_Click(object sender, EventArgs e)
         {
             var post = this.CurrentPost;
@@ -2998,7 +2161,7 @@ namespace OpenTween
         /// <returns>ソートを行う ComparerMode。null であればソートを行わない</returns>
         private ComparerMode? GetComparerModeByColumnIndex(int columnIndex)
         {
-            if (this.iconCol)
+            if (this.Use2ColumnsMode)
                 return ComparerMode.Id;
 
             return columnIndex switch
@@ -3054,14 +2217,14 @@ namespace OpenTween
         /// </summary>
         private void SetSortColumn(ComparerMode sortColumn)
         {
-            if (SettingManager.Common.SortOrderLock)
+            if (this.settings.Common.SortOrderLock)
                 return;
 
             this.statuses.ToggleSortOrder(sortColumn);
             this.InitColumnText();
 
             var list = this.CurrentListView;
-            if (this.iconCol)
+            if (this.Use2ColumnsMode)
             {
                 list.Columns[0].Text = this.columnText[0];
                 list.Columns[1].Text = this.columnText[2];
@@ -3074,7 +2237,7 @@ namespace OpenTween
                 }
             }
 
-            this.PurgeListViewItemCache();
+            this.listCache?.PurgeCache();
 
             var tab = this.CurrentTab;
             var post = this.CurrentPost;
@@ -3153,7 +2316,7 @@ namespace OpenTween
                 this.StatusOpenMenuItem.Enabled = true;
                 this.ShowRelatedStatusesMenuItem.Enabled = true;  // PublicSearchの時問題出るかも
 
-                if (!post.CanRetweetBy(this.twitterApi.CurrentUserId))
+                if (!post.CanRetweetBy(this.tw.UserId))
                 {
                     this.ReTweetStripMenuItem.Enabled = false;
                     this.ReTweetUnofficialStripMenuItem.Enabled = false;
@@ -3229,9 +2392,9 @@ namespace OpenTween
 
                     try
                     {
-                        if (post.IsDm)
+                        if (post.StatusId is TwitterDirectMessageId dmId)
                         {
-                            await this.twitterApi.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture));
+                            await this.tw.Api.DirectMessagesEventsDestroy(dmId);
                         }
                         else
                         {
@@ -3239,8 +2402,7 @@ namespace OpenTween
                             {
                                 // 自分が RT したツイート (自分が RT した自分のツイートも含む)
                                 //   => RT を取り消し
-                                await this.twitterApi.StatusesDestroy(post.StatusId)
-                                    .IgnoreResponse();
+                                await this.tw.DeleteRetweet(post);
                             }
                             else
                             {
@@ -3250,15 +2412,13 @@ namespace OpenTween
                                     {
                                         // 他人に RT された自分のツイート
                                         //   => RT 元の自分のツイートを削除
-                                        await this.twitterApi.StatusesDestroy(post.RetweetedId.Value)
-                                            .IgnoreResponse();
+                                        await this.tw.DeleteTweet(post.RetweetedId.ToTwitterStatusId());
                                     }
                                     else
                                     {
                                         // 自分のツイート
                                         //   => ツイートを削除
-                                        await this.twitterApi.StatusesDestroy(post.StatusId)
-                                            .IgnoreResponse();
+                                        await this.tw.DeleteTweet(post.StatusId.ToTwitterStatusId());
                                     }
                                 }
                             }
@@ -3278,44 +2438,39 @@ namespace OpenTween
                 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();
             }
         }
@@ -3332,11 +2487,10 @@ namespace OpenTween
                 {
                     this.statuses.SetReadAllTab(statusId, read: true);
                     var idx = tab.IndexOf(statusId);
-                    this.ChangeCacheStyleRead(true, idx);
+                    this.listCache?.RefreshStyle(idx);
                 }
-                this.ColorizeList();
             }
-            if (SettingManager.Common.TabIconDisp)
+            if (this.settings.Common.TabIconDisp)
             {
                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
                 {
@@ -3348,7 +2502,7 @@ namespace OpenTween
                     }
                 }
             }
-            if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
+            if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
         }
 
         private void UnreadStripMenuItem_Click(object sender, EventArgs e)
@@ -3360,11 +2514,10 @@ namespace OpenTween
                 {
                     this.statuses.SetReadAllTab(statusId, read: false);
                     var idx = tab.IndexOf(statusId);
-                    this.ChangeCacheStyleRead(false, idx);
+                    this.listCache?.RefreshStyle(idx);
                 }
-                this.ColorizeList();
             }
-            if (SettingManager.Common.TabIconDisp)
+            if (this.settings.Common.TabIconDisp)
             {
                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
                 {
@@ -3376,7 +2529,7 @@ namespace OpenTween
                     }
                 }
             }
-            if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
+            if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
         }
 
         private async void RefreshStripMenuItem_Click(object sender, EventArgs e)
@@ -3388,21 +2541,15 @@ namespace OpenTween
         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);
@@ -3416,7 +2563,7 @@ namespace OpenTween
             {
                 lock (this.syncObject)
                 {
-                    settingDialog.SaveConfig(SettingManager.Common, SettingManager.Local);
+                    settingDialog.SaveConfig(this.settings.Common, this.settings.Local);
                 }
             }
 
@@ -3426,37 +2573,32 @@ namespace OpenTween
         private async void SettingStripMenuItem_Click(object sender, EventArgs e)
         {
             // 設定画面表示前のユーザー情報
-            var oldUser = new { this.tw.AccessToken, this.tw.AccessTokenSecret, this.tw.Username, this.tw.UserId };
-
-            var oldIconSz = SettingManager.Common.IconSize;
+            var previousUserId = this.settings.Common.UserId;
+            var oldIconCol = this.Use2ColumnsMode;
 
             if (this.ShowSettingDialog() == DialogResult.OK)
             {
                 lock (this.syncObject)
                 {
-                    this.tw.RestrictFavCheck = SettingManager.Common.RestrictFavCheck;
-                    this.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;
-
-                    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.ImageSelector.Reset(this.tw, this.tw.Configuration);
+                    this.settings.ApplySettings();
+
+                    if (MyCommon.IsNullOrEmpty(this.settings.Common.Token))
+                        this.tw.ClearAuthInfo();
+
+                    var account = this.settings.Common.SelectedAccount;
+                    if (account != null)
+                        this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+                    else
+                        this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
+
+                    this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
+                    this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
+
+                    this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
 
                     try
                     {
-                        if (SettingManager.Common.TabIconDisp)
+                        if (this.settings.Common.TabIconDisp)
                         {
                             this.ListTab.DrawItem -= this.ListTab_DrawItem;
                             this.ListTab.DrawMode = TabDrawMode.Normal;
@@ -3479,11 +2621,11 @@ namespace OpenTween
 
                     try
                     {
-                        if (!SettingManager.Common.UnreadManage)
+                        if (!this.settings.Common.UnreadManage)
                         {
                             this.ReadedStripMenuItem.Enabled = false;
                             this.UnreadStripMenuItem.Enabled = false;
-                            if (SettingManager.Common.TabIconDisp)
+                            if (this.settings.Common.TabIconDisp)
                             {
                                 foreach (TabPage myTab in this.ListTab.TabPages)
                                 {
@@ -3507,57 +2649,31 @@ namespace OpenTween
                     // タブの表示位置の決定
                     this.SetTabAlignment();
 
-                    this.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;
-                    this.fntUnread = SettingManager.Local.FontUnread;
-                    this.clUnread = SettingManager.Local.ColorUnread;
-                    this.fntReaded = SettingManager.Local.FontRead;
-                    this.clReaded = SettingManager.Local.ColorRead;
-                    this.clFav = SettingManager.Local.ColorFav;
-                    this.clOWL = SettingManager.Local.ColorOWL;
-                    this.clRetweet = SettingManager.Local.ColorRetweet;
-                    this.fntDetail = SettingManager.Local.FontDetail;
-                    this.clDetail = SettingManager.Local.ColorDetail;
-                    this.clDetailLink = SettingManager.Local.ColorDetailLink;
-                    this.clDetailBackcolor = SettingManager.Local.ColorDetailBackcolor;
-                    this.clSelf = SettingManager.Local.ColorSelf;
-                    this.clAtSelf = SettingManager.Local.ColorAtSelf;
-                    this.clTarget = SettingManager.Local.ColorTarget;
-                    this.clAtTarget = SettingManager.Local.ColorAtTarget;
-                    this.clAtFromTarget = SettingManager.Local.ColorAtFromTarget;
-                    this.clAtTo = SettingManager.Local.ColorAtTo;
-                    this.clListBackcolor = SettingManager.Local.ColorListBackcolor;
-                    this.clInputBackcolor = SettingManager.Local.ColorInputBackcolor;
-                    this.clInputFont = SettingManager.Local.ColorInputFont;
-                    this.fntInputFont = SettingManager.Local.FontInputFont;
-                    this.brsBackColorMine.Dispose();
-                    this.brsBackColorAt.Dispose();
-                    this.brsBackColorYou.Dispose();
-                    this.brsBackColorAtYou.Dispose();
-                    this.brsBackColorAtFromTarget.Dispose();
-                    this.brsBackColorAtTo.Dispose();
-                    this.brsBackColorNone.Dispose();
-                    this.brsBackColorMine = new SolidBrush(this.clSelf);
-                    this.brsBackColorAt = new SolidBrush(this.clAtSelf);
-                    this.brsBackColorYou = new SolidBrush(this.clTarget);
-                    this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
-                    this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
-                    this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
-                    this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
+                    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 (this.StatusText.Focused) this.StatusText.BackColor = this.clInputBackcolor;
-                        this.StatusText.Font = this.fntInputFont;
-                        this.StatusText.ForeColor = this.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)
                     {
@@ -3577,7 +2693,7 @@ namespace OpenTween
 
                     try
                     {
-                        if (SettingManager.Common.TabIconDisp)
+                        if (this.settings.Common.TabIconDisp)
                         {
                             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
                             {
@@ -3598,10 +2714,7 @@ namespace OpenTween
 
                     try
                     {
-                        var oldIconCol = this.iconCol;
-
-                        if (SettingManager.Common.IconSize != oldIconSz)
-                            this.ApplyListViewIconSize(SettingManager.Common.IconSize);
+                        this.ApplyListViewIconSize(this.settings.Common.IconSize);
 
                         foreach (TabPage tp in this.ListTab.TabPages)
                         {
@@ -3609,11 +2722,9 @@ namespace OpenTween
 
                             using (ControlTransaction.Update(lst))
                             {
-                                lst.GridLines = SettingManager.Common.ShowGrid;
-                                lst.Font = this.fntReaded;
-                                lst.BackColor = this.clListBackcolor;
+                                lst.GridLines = this.settings.Common.ShowGrid;
 
-                                if (this.iconCol != oldIconCol)
+                                if (this.Use2ColumnsMode != oldIconCol)
                                     this.ResetColumns(lst);
                             }
                         }
@@ -3628,28 +2739,28 @@ namespace OpenTween
                     this.SetMainWindowTitle();
                     this.SetNotifyIconText();
 
-                    this.PurgeListViewItemCache();
+                    this.listCache?.PurgeCache();
                     this.CurrentListView.Refresh();
                     this.ListTab.Refresh();
 
                     this.hookGlobalHotkey.UnregisterAllOriginalHotkey();
-                    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;
 
-                        this.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) this.gh.RegisterGrowl();
+                    if (this.settings.Common.IsUseNotifyGrowl) this.gh.RegisterGrowl();
                     try
                     {
                         this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
@@ -3659,18 +2770,13 @@ namespace OpenTween
                     }
                 }
             }
-            else
-            {
-                // キャンセル時は Twitter クラスの認証情報を画面表示前の状態に戻す
-                this.tw.Initialize(oldUser.AccessToken, oldUser.AccessTokenSecret, oldUser.Username, oldUser.UserId);
-            }
 
             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
 
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
             this.SaveConfigsAll(false);
 
-            if (this.tw.UserId != oldUser.UserId)
+            if (this.tw.UserId != previousUserId)
                 await this.DoGetFollowersMenu();
         }
 
@@ -3679,52 +2785,28 @@ namespace OpenTween
         /// </summary>
         private void SetTabAlignment()
         {
-            var newAlignment = SettingManager.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top;
+            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);
 
             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(forceScroll: true);
         }
 
         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;
-
-            this.PurgeListViewItemCache();
-
-            if (this.iconSz > 0)
+            if (this.listDrawer != null)
             {
-                // ディスプレイの DPI 設定を考慮したサイズを設定する
-                this.listViewImageList.ImageSize = new Size(
-                    1,
-                    (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Height));
-            }
-            else
-            {
-                this.listViewImageList.ImageSize = new Size(1, 1);
+                this.listDrawer.IconSize = iconSz;
+                this.listDrawer.UpdateItemHeight();
             }
+
+            this.listCache?.PurgeCache();
         }
 
         private void ResetColumns(DetailsListView list)
@@ -4016,14 +3098,10 @@ namespace OpenTween
                 listCustom.View = View.Details;
                 listCustom.OwnerDraw = true;
                 listCustom.VirtualMode = true;
-                listCustom.Font = this.fntReaded;
-                listCustom.BackColor = this.clListBackcolor;
 
-                listCustom.GridLines = SettingManager.Common.ShowGrid;
+                listCustom.GridLines = this.settings.Common.ShowGrid;
                 listCustom.AllowDrop = true;
 
-                listCustom.SmallImageList = this.listViewImageList;
-
                 this.InitColumns(listCustom, startup);
 
                 listCustom.SelectedIndexChanged += this.MyList_SelectedIndexChanged;
@@ -4033,16 +3111,15 @@ namespace OpenTween
                 listCustom.DragDrop += this.TweenMain_DragDrop;
                 listCustom.DragEnter += this.TweenMain_DragEnter;
                 listCustom.DragOver += this.TweenMain_DragOver;
-                listCustom.DrawItem += this.MyList_DrawItem;
                 listCustom.MouseClick += this.MyList_MouseClick;
                 listCustom.ColumnReordered += this.MyList_ColumnReordered;
                 listCustom.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
-                listCustom.CacheVirtualItems += this.MyList_CacheVirtualItems;
-                listCustom.RetrieveVirtualItem += this.MyList_RetrieveVirtualItem;
-                listCustom.DrawSubItem += this.MyList_DrawSubItem;
                 listCustom.HScrolled += this.MyList_HScrolled;
             }
 
+            var state = new TimelineListViewState(listCustom, tab);
+            this.listViewState[tab.TabName] = state;
+
             return true;
         }
 
@@ -4075,6 +3152,8 @@ namespace OpenTween
 
             this.SetListProperty();   // 他のタブに列幅等を反映
 
+            this.listViewState.Remove(tabName);
+
             // オブジェクトインスタンスの削除
             var listCustom = (DetailsListView)tabPage.Tag;
             tabPage.Tag = null;
@@ -4131,13 +3210,9 @@ namespace OpenTween
                 listCustom.DragDrop -= this.TweenMain_DragDrop;
                 listCustom.DragEnter -= this.TweenMain_DragEnter;
                 listCustom.DragOver -= this.TweenMain_DragOver;
-                listCustom.DrawItem -= this.MyList_DrawItem;
                 listCustom.MouseClick -= this.MyList_MouseClick;
                 listCustom.ColumnReordered -= this.MyList_ColumnReordered;
                 listCustom.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
-                listCustom.CacheVirtualItems -= this.MyList_CacheVirtualItems;
-                listCustom.RetrieveVirtualItem -= this.MyList_RetrieveVirtualItem;
-                listCustom.DrawSubItem -= this.MyList_DrawSubItem;
                 listCustom.HScrolled -= this.MyList_HScrolled;
 
                 var cols = listCustom.Columns.Cast<ColumnHeader>().ToList<ColumnHeader>();
@@ -4153,25 +3228,19 @@ namespace OpenTween
                 listCustom.ListViewItemSorter = null;
 
                 // キャッシュのクリア
-                this.PurgeListViewItemCache();
+                this.listCache?.PurgeCache();
             }
 
             tabPage.Dispose();
             listCustom.Dispose();
             this.statuses.RemoveTab(tabName);
 
-            foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
-            {
-                var lst = (DetailsListView)this.ListTab.TabPages[index].Tag;
-                lst.VirtualListSize = tab.AllCount;
-            }
-
             return true;
         }
 
         private void ListTab_Deselected(object sender, TabControlEventArgs e)
         {
-            this.PurgeListViewItemCache();
+            this.listCache?.PurgeCache();
             this.beforeSelectedTab = e.TabPage;
         }
 
@@ -4179,7 +3248,7 @@ namespace OpenTween
         {
             // タブのD&D
 
-            if (!SettingManager.Common.TabMouseLock && e.Button == MouseButtons.Left && this.tabDrag)
+            if (!this.settings.Common.TabMouseLock && e.Button == MouseButtons.Left && this.tabDrag)
             {
                 var tn = "";
                 var dragEnableRectangle = new Rectangle(this.tabMouseDownPoint.X - (SystemInformation.DragSize.Width / 2), this.tabMouseDownPoint.Y - (SystemInformation.DragSize.Height / 2), SystemInformation.DragSize.Width, SystemInformation.DragSize.Height);
@@ -4270,7 +3339,7 @@ namespace OpenTween
         {
             if (e.KeyChar == '@')
             {
-                if (!SettingManager.Common.UseAtIdSupplement) return;
+                if (!this.settings.Common.UseAtIdSupplement) return;
                 // @マーク
                 var cnt = this.AtIdSupl.ItemCount;
                 this.ShowSuplDialog(this.StatusText, this.AtIdSupl);
@@ -4280,7 +3349,7 @@ namespace OpenTween
             }
             else if (e.KeyChar == '#')
             {
-                if (!SettingManager.Common.UseHashSupplement) return;
+                if (!this.settings.Common.UseHashSupplement) return;
                 this.ShowSuplDialog(this.StatusText, this.HashSupl);
                 e.Handled = true;
             }
@@ -4303,7 +3372,7 @@ namespace OpenTween
             {
                 dialog.ShowDialog();
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
             var selStart = owner.SelectionStart;
             var fHalf = "";
             var eHalf = "";
@@ -4384,7 +3453,7 @@ namespace OpenTween
             }
             else
             {
-                this.StatusText.ForeColor = this.clInputFont;
+                this.StatusText.ForeColor = this.themeManager.ColorInputFont;
             }
 
             this.StatusText.AccessibleDescription = string.Format(Properties.Resources.StatusText_AccessibleDescription, pLen);
@@ -4460,7 +3529,7 @@ namespace OpenTween
             attachmentUrl = null;
 
             // attachment_url は media_id と同時に使用できない
-            if (this.ImageSelector.Visible && this.ImageSelector.SelectedService is TwitterPhoto)
+            if (this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is TwitterPhoto)
                 return statusText;
 
             var match = Twitter.AttachmentUrlRegex.Match(statusText);
@@ -4504,7 +3573,7 @@ namespace OpenTween
                 statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& ");
             }
 
-            if (SettingManager.Common.WideSpaceConvert)
+            if (this.settings.Common.WideSpaceConvert)
             {
                 // 文中の全角スペースを半角スペース1個にする
                 statusText = statusText.Replace(" ", " ");
@@ -4515,13 +3584,13 @@ namespace OpenTween
                 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);
@@ -4555,15 +3624,15 @@ namespace OpenTween
 
             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();
                 }
             }
 
@@ -4603,145 +3672,7 @@ namespace OpenTween
         }
 
         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);
-        }
-
-        /// <summary>
-        /// DetailsListView のための ListViewItem のキャッシュを消去する
-        /// </summary>
-        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);
-
-            var scaledIconSz = (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Width);
-
-            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, scaledIconSz);
-            }
-            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, scaledIconSz);
-            }
-            itm.StateIndex = post.StateIndex;
-            itm.Tag = post;
-
-            var read = post.IsRead;
-            // 未読管理していなかったら既読として扱う
-            if (!tab.UnreadManage || !SettingManager.Common.UnreadManage)
-                read = true;
-
-            this.ChangeItemStyleRead(read, itm, post, null);
-
-            if (tab.TabName == this.CurrentTabName)
-                this.ColorizeList(itm, post);
-
-            return itm;
-        }
+            => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null;
 
         /// <summary>
         /// 全てのタブの振り分けルールを反映し直します
@@ -4750,253 +3681,42 @@ namespace OpenTween
         {
             using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
             {
-                this.PurgeListViewItemCache();
                 this.statuses.FilterAll();
 
-                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 (tab.UnreadCount > 0)
-                            tabPage.ImageIndex = 0;
-                        else
-                            tabPage.ImageIndex = -1;
-                    }
-                }
-
-                if (!SettingManager.Common.TabIconDisp)
-                    this.ListTab.Refresh();
-
-                this.SetMainWindowTitle();
-                this.SetStatusLabelUrl();
-            }
-        }
-
-        private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
-            => e.DrawDefault = true;
-
-        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 == this.clSelf)
-                    brs2 = this.brsBackColorMine;
-                else if (e.Item.BackColor == this.clAtSelf)
-                    brs2 = this.brsBackColorAt;
-                else if (e.Item.BackColor == this.clTarget)
-                    brs2 = this.brsBackColorYou;
-                else if (e.Item.BackColor == this.clAtTarget)
-                    brs2 = this.brsBackColorAtYou;
-                else if (e.Item.BackColor == this.clAtFromTarget)
-                    brs2 = this.brsBackColorAtFromTarget;
-                else if (e.Item.BackColor == this.clAtTo)
-                    brs2 = this.brsBackColorAtTo;
-                else
-                    brs2 = this.brsBackColorNone;
-            }
-            else
-            {
-                // 選択中の行
-                if (((Control)sender).Focused)
-                    brs2 = this.brsHighLight;
-                else
-                    brs2 = this.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 (this.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 ? this.clHighLight : // 選択中の行
-                        this.clUnread;
-
-                    if (this.iconCol)
-                    {
-                        var rctB = e.Bounds;
-                        rctB.Width = e.Header.Width;
-                        rctB.Height = fontHeight;
-
-                        using var fnt = new Font(e.Item.Font, FontStyle.Bold);
-                        var formatFlags1 = TextFormatFlags.WordBreak |
-                            TextFormatFlags.EndEllipsis |
-                            TextFormatFlags.GlyphOverhangPadding |
-                            TextFormatFlags.NoPrefix;
-
-                        TextRenderer.DrawText(
-                            e.Graphics,
-                            post.IsDeleted ? "(DELETED)" : post.TextSingleLine,
-                            e.Item.Font,
-                            Rectangle.Round(rct),
-                            color,
-                            formatFlags1);
-
-                        var formatFlags2 = TextFormatFlags.SingleLine |
-                            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,
-                            formatFlags2);
-                    }
-                    else
-                    {
-                        string text;
-                        if (e.ColumnIndex != 2)
-                            text = e.SubItem.Text;
-                        else
-                            text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine;
-
-                        if (drawLineCount == 1)
-                        {
-                            var formatFlags = TextFormatFlags.SingleLine |
-                                TextFormatFlags.EndEllipsis |
-                                TextFormatFlags.GlyphOverhangPadding |
-                                TextFormatFlags.NoPrefix |
-                                TextFormatFlags.VerticalCenter;
-
-                            TextRenderer.DrawText(
-                                e.Graphics,
-                                text,
-                                e.Item.Font,
-                                Rectangle.Round(rct),
-                                color,
-                                formatFlags);
-                        }
-                        else
-                        {
-                            var formatFlags = TextFormatFlags.WordBreak |
-                                TextFormatFlags.EndEllipsis |
-                                TextFormatFlags.GlyphOverhangPadding |
-                                TextFormatFlags.NoPrefix;
-
-                            TextRenderer.DrawText(
-                                e.Graphics,
-                                text,
-                                e.Item.Font,
-                                Rectangle.Round(rct),
-                                color,
-                                formatFlags);
-                        }
-                    }
-                }
-            }
-        }
-
-        private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
-        {
-            if (this.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));
+                var listView = this.CurrentListView;
+                using (ControlTransaction.Update(listView))
+                {
+                    this.listCache?.PurgeCache();
+                    this.listCache?.UpdateListSize();
+                }
 
-                if (iconRect.Width > 0)
+                foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
                 {
-                    e.Graphics.FillRectangle(Brushes.White, iconRect);
-                    e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
-                    try
-                    {
-                        e.Graphics.DrawImage(img.Image, iconRect);
-                    }
-                    catch (ArgumentException)
+                    var tabPage = this.ListTab.TabPages[index];
+
+                    if (this.settings.Common.TabIconDisp)
                     {
+                        if (tab.UnreadCount > 0)
+                            tabPage.ImageIndex = 0;
+                        else
+                            tabPage.ImageIndex = -1;
                     }
                 }
-            }
-            else
-            {
-                iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, new Size(1, 1)), itemRect);
 
-                item.GetImageAsync();
-            }
+                if (!this.settings.Common.TabIconDisp)
+                    this.ListTab.Refresh();
 
-            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);
+                this.SetMainWindowTitle();
+                this.SetStatusLabelUrl();
             }
         }
 
+        private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
+            => e.DrawDefault = true;
+
+        private void MyList_HScrolled(object sender, EventArgs e)
+            => ((DetailsListView)sender).Refresh();
+
         protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
         {
             base.ScaleControl(factor, specified);
@@ -5120,10 +3840,10 @@ namespace OpenTween
         {
             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)
@@ -5219,7 +3939,7 @@ namespace OpenTween
             {
                 about.ShowDialog(this);
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
         }
 
         private void JumpUnreadMenuItem_Click(object sender, EventArgs e)
@@ -5290,8 +4010,9 @@ namespace OpenTween
 
             if (this.statuses.SortMode == ComparerMode.Id)
             {
-                if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - this.iconSz - 10 ||
-                    this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < this.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)
                 {
                     this.MoveTop();
                 }
@@ -5324,8 +4045,8 @@ namespace OpenTween
             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 + "\"",
             };
 
@@ -5401,7 +4122,7 @@ namespace OpenTween
                     return;
                 }
 
-                if (startup && versionInfo.Version <= SettingManager.Common.SkipUpdateVersion)
+                if (startup && versionInfo.Version <= this.settings.Common.SkipUpdateVersion)
                     return;
 
                 using var dialog = new UpdateDialog();
@@ -5412,11 +4133,11 @@ namespace OpenTween
 
                 if (dialog.ShowDialog(this) == DialogResult.Yes)
                 {
-                    await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri.OriginalString);
+                    await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri);
                 }
                 else if (dialog.SkipButtonPressed)
                 {
-                    SettingManager.Common.SkipUpdateVersion = versionInfo.Version;
+                    this.settings.Common.SkipUpdateVersion = versionInfo.Version;
                     this.MarkSettingCommonModified();
                 }
             }
@@ -5438,17 +4159,17 @@ namespace OpenTween
         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)
             {
                 this.SetMainWindowTitle();
             }
             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())
                 {
@@ -5498,15 +4219,17 @@ namespace OpenTween
                 this.tweetDetailsView.ShowPostDetails(currentPost),
             };
 
-            this.SplitContainer3.Panel2Collapsed = true;
-
-            if (SettingManager.Common.PreviewEnable)
+            if (this.settings.Common.PreviewEnable)
             {
                 var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource());
                 oldTokenSource?.Cancel();
 
                 var token = this.thumbnailTokenSource!.Token;
-                loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token));
+                loadTasks.Add(this.PrepareThumbnailControl(currentPost, token));
+            }
+            else
+            {
+                this.SplitContainer3.Panel2Collapsed = true;
             }
 
             async Task DelayedTasks()
@@ -5524,6 +4247,25 @@ namespace OpenTween
             _ = DelayedTasks();
         }
 
+        private async Task PrepareThumbnailControl(PostClass post, CancellationToken token)
+        {
+            var prepareTask = this.tweetThumbnail1.Model.PrepareThumbnails(post, token);
+
+            var timeout = Task.Delay(100);
+            if ((await Task.WhenAny(prepareTask, timeout)) == timeout)
+            {
+                token.ThrowIfCancellationRequested();
+
+                // サムネイル情報の読み込みに時間が掛かっている場合は一旦サムネイル領域を非表示にする
+                this.SplitContainer3.Panel2Collapsed = true;
+            }
+
+            await prepareTask;
+            token.ThrowIfCancellationRequested();
+
+            this.SplitContainer3.Panel2Collapsed = !this.tweetThumbnail1.Model.ThumbnailAvailable;
+        }
+
         private async void MatomeMenuItem_Click(object sender, EventArgs e)
             => await this.OpenApplicationWebsite();
 
@@ -5545,7 +4287,7 @@ namespace OpenTween
             }
 
             if (e.Control || e.Shift || e.Alt)
-                this.anchorFlag = false;
+                tab.ClearAnchor();
 
             if (this.CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask))
             {
@@ -5599,7 +4341,7 @@ namespace OpenTween
                     .NotFocusedOn(FocusedControl.StatusText)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
                     }),
 
@@ -5607,7 +4349,7 @@ namespace OpenTween
                     .NotFocusedOn(FocusedControl.StatusText)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
                     }),
 
@@ -5640,7 +4382,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.GoPost(forward: true);
                     }),
 
@@ -5648,7 +4390,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.GoPost(forward: false);
                     }),
 
@@ -5656,7 +4398,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.MoveTop();
                     }),
 
@@ -5664,7 +4406,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.GoNextTab(forward: true);
                     }),
 
@@ -5672,7 +4414,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.GoNextTab(forward: false);
                     }),
 
@@ -5681,7 +4423,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         return this.GoInReplyToPostTree();
                     }),
 
@@ -5690,7 +4432,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        this.CurrentTab.ClearAnchor();
                         this.GoBackInReplyToPostTree();
                     }),
 
@@ -5698,7 +4440,7 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.ListTab)
                     .Do(() =>
                     {
-                        this.anchorFlag = false;
+                        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)
@@ -5711,7 +4453,7 @@ namespace OpenTween
                 // 上下キー, 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)
@@ -5875,8 +4617,8 @@ namespace OpenTween
                     .FocusedOn(FocusedControl.PostBrowser)
                     .Do(() =>
                     {
-                        var multiline = !SettingManager.Local.StatusMultiline;
-                        SettingManager.Local.StatusMultiline = multiline;
+                        var multiline = !this.settings.Local.StatusMultiline;
+                        this.settings.Local.StatusMultiline = multiline;
                         this.MultiLineMenuItem.Checked = multiline;
                         this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty);
                     }),
@@ -5977,6 +4719,9 @@ namespace OpenTween
                     .OnlyWhen(() => this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
                     .Do(() => this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus()),
 
+                ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.L)
+                    .Do(() => this.DoQuoteOfficial()),
+
                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.S)
                     .Do(() => this.FavoriteChange(favAdd: false)),
 
@@ -6122,15 +4867,15 @@ namespace OpenTween
                     .Do(() => this.CopyUserId()),
 
                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Up)
-                    .Do(() => this.tweetThumbnail1.ScrollUp()),
+                    .Do(() => this.tweetThumbnail1.Model.ScrollUp()),
 
                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Down)
-                    .Do(() => this.tweetThumbnail1.ScrollDown()),
+                    .Do(() => this.tweetThumbnail1.Model.ScrollDown()),
 
                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
                     .FocusedOn(FocusedControl.ListTab)
                     .OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
-                    .Do(() => this.OpenThumbnailPicture(this.tweetThumbnail1.Thumbnail)),
+                    .Do(() => this.tweetThumbnail1.OpenImageInBrowser()),
             };
         }
 
@@ -6295,7 +5040,7 @@ namespace OpenTween
                 return;
 
             var selectedStatusId = tab.SelectedStatusId;
-            if (selectedStatusId == -1)
+            if (selectedStatusId == null)
                 return;
 
             int fIdx, toIdx, stp;
@@ -6436,29 +5181,28 @@ namespace OpenTween
                 stp = -1;
             }
 
-            if (!this.anchorFlag)
+            var anchorPost = tab.AnchorPost;
+            if (anchorPost == null)
             {
                 var currentPost = this.CurrentPost;
-                if (currentPost == null) return;
-                this.anchorPost = currentPost;
-                this.anchorFlag = true;
-            }
-            else
-            {
-                if (this.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 == this.anchorPost.ScreenName ||
-                    post.RetweetedBy == this.anchorPost.ScreenName ||
-                    post.ScreenName == this.anchorPost.RetweetedBy ||
-                    (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == this.anchorPost.RetweetedBy) ||
-                    this.anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) ||
-                    this.anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) ||
-                    post.ReplyToList.Any(x => x.UserId == this.anchorPost.UserId) ||
-                    post.ReplyToList.Any(x => x.UserId == this.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;
                     this.SelectListItem(listView, idx);
@@ -6470,9 +5214,13 @@ namespace OpenTween
 
         private void GoAnchor()
         {
-            if (this.anchorPost == null) return;
-            var idx = this.CurrentTab.IndexOf(this.anchorPost.StatusId);
-            if (idx == -1) return;
+            var anchorStatusId = this.CurrentTab.AnchorStatusId;
+            if (anchorStatusId == null)
+                return;
+
+            var idx = this.CurrentTab.IndexOf(anchorStatusId);
+            if (idx == -1)
+                return;
 
             var listView = this.CurrentListView;
             this.SelectListItem(listView, idx);
@@ -6587,12 +5335,16 @@ namespace OpenTween
             {
                 try
                 {
-                    var post = await this.tw.GetStatusApi(false, currentPost.StatusId);
+                    var post = await this.tw.GetStatusApi(false, currentPost.StatusId.ToTwitterStatusId());
 
-                    currentPost.InReplyToStatusId = post.InReplyToStatusId;
-                    currentPost.InReplyToUser = post.InReplyToUser;
-                    currentPost.IsReply = post.IsReply;
-                    this.PurgeListViewItemCache();
+                    currentPost = currentPost with
+                    {
+                        InReplyToStatusId = post.InReplyToStatusId,
+                        InReplyToUser = post.InReplyToUser,
+                        IsReply = post.IsReply,
+                    };
+                    curTabClass.ReplacePost(currentPost);
+                    this.listCache?.PurgeCache();
 
                     var index = curTabClass.SelectedIndex;
                     this.CurrentListView.RedrawItems(index, index, false);
@@ -6609,11 +5361,11 @@ namespace OpenTween
             {
                 this.replyChains = new Stack<ReplyChain>();
             }
-            this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass));
+            this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId, curTabClass));
 
             int inReplyToIndex;
             string inReplyToTabName;
-            var inReplyToId = currentPost.InReplyToStatusId.Value;
+            var inReplyToId = currentPost.InReplyToStatusId;
             var inReplyToUser = currentPost.InReplyToUser;
 
             var inReplyToPosts = from tab in this.statuses.Tabs
@@ -6631,7 +5383,7 @@ namespace OpenTween
                 {
                     await Task.Run(async () =>
                     {
-                        var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value)
+                        var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.ToTwitterStatusId())
                             .ConfigureAwait(false);
                         post.IsRead = true;
 
@@ -6642,7 +5394,7 @@ namespace OpenTween
                 catch (WebApiException ex)
                 {
                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
-                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
                     return;
                 }
 
@@ -6651,7 +5403,7 @@ namespace OpenTween
                 inReplyPost = inReplyToPosts.FirstOrDefault();
                 if (inReplyPost == null)
                 {
-                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
                     return;
                 }
             }
@@ -6879,10 +5631,8 @@ namespace OpenTween
             }
         }
 
-        private bool GoStatus(long statusId)
+        private bool GoStatus(PostId statusId)
         {
-            if (statusId == 0) return false;
-
             var tab = this.statuses.Tabs
                 .Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage)
                 .Where(x => x.Contains(statusId))
@@ -6903,10 +5653,8 @@ namespace OpenTween
             return true;
         }
 
-        private bool GoDirectMessage(long statusId)
+        private bool GoDirectMessage(PostId statusId)
         {
-            if (statusId == 0) return false;
-
             var tab = this.statuses.DirectMessageTab;
             var index = tab.IndexOf(statusId);
 
@@ -6924,20 +5672,17 @@ namespace OpenTween
         }
 
         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 = this.StatusText;
-            this.StatusText.BackColor = this.clInputBackcolor;
+            this.StatusText.BackColor = this.themeManager.ColorInputBackcolor;
         }
 
         public Color InputBackColor
-        {
-            get => this.clInputBackcolor;
-            set => this.clInputBackcolor = value;
-        }
+            => this.themeManager.ColorInputBackcolor;
 
         private void StatusText_Leave(object sender, EventArgs e)
         {
@@ -6979,11 +5724,11 @@ namespace OpenTween
 
         private void SaveConfigsAtId()
         {
-            if (this.ignoreConfigSave || !SettingManager.Common.UseAtIdSupplement && this.AtIdSupl == null) return;
+            if (this.ignoreConfigSave || !this.settings.Common.UseAtIdSupplement && this.AtIdSupl == null) return;
 
             this.ModifySettingAtId = false;
-            SettingManager.AtIdList.AtIdList = this.AtIdSupl.GetItemList();
-            SettingManager.SaveAtIdList();
+            this.settings.AtIdList.AtIdList = this.AtIdSupl.GetItemList();
+            this.settings.SaveAtIdList();
         }
 
         private void SaveConfigsCommon()
@@ -6993,12 +5738,12 @@ namespace OpenTween
             this.ModifySettingCommon = false;
             lock (this.syncObject)
             {
-                SettingManager.Common.UserName = this.tw.Username;
-                SettingManager.Common.UserId = this.tw.UserId;
-                SettingManager.Common.Token = this.tw.AccessToken;
-                SettingManager.Common.TokenSecret = this.tw.AccessTokenSecret;
-                SettingManager.Common.SortOrder = (int)this.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, // 本文
@@ -7007,22 +5752,22 @@ namespace OpenTween
                     ComparerMode.Source => 7, // Source
                     _ => throw new InvalidOperationException($"Invalid sort mode: {this.statuses.SortMode}"),
                 };
-                SettingManager.Common.HashTags = this.HashMgr.HashHistories;
+                this.settings.Common.HashTags = this.HashMgr.HashHistories;
                 if (this.HashMgr.IsPermanent)
                 {
-                    SettingManager.Common.HashSelected = this.HashMgr.UseHash;
+                    this.settings.Common.HashSelected = this.HashMgr.UseHash;
                 }
                 else
                 {
-                    SettingManager.Common.HashSelected = "";
+                    this.settings.Common.HashSelected = "";
                 }
-                SettingManager.Common.HashIsHead = this.HashMgr.IsHead;
-                SettingManager.Common.HashIsPermanent = this.HashMgr.IsPermanent;
-                SettingManager.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
-                SettingManager.Common.UseImageService = this.ImageSelector.ServiceIndex;
-                SettingManager.Common.UseImageServiceName = this.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.Model.SelectedMediaServiceIndex;
+                this.settings.Common.UseImageServiceName = this.ImageSelector.Model.SelectedMediaServiceName;
 
-                SettingManager.SaveCommon();
+                this.settings.SaveCommon();
             }
         }
 
@@ -7032,38 +5777,16 @@ namespace OpenTween
             lock (this.syncObject)
             {
                 this.ModifySettingLocal = false;
-                SettingManager.Local.ScaleDimension = this.CurrentAutoScaleDimensions;
-                SettingManager.Local.FormSize = this.mySize;
-                SettingManager.Local.FormLocation = this.myLoc;
-                SettingManager.Local.SplitterDistance = this.mySpDis;
-                SettingManager.Local.PreviewDistance = this.mySpDis3;
-                SettingManager.Local.StatusMultiline = this.StatusText.Multiline;
-                SettingManager.Local.StatusTextHeight = this.mySpDis2;
-
-                SettingManager.Local.FontUnread = this.fntUnread;
-                SettingManager.Local.ColorUnread = this.clUnread;
-                SettingManager.Local.FontRead = this.fntReaded;
-                SettingManager.Local.ColorRead = this.clReaded;
-                SettingManager.Local.FontDetail = this.fntDetail;
-                SettingManager.Local.ColorDetail = this.clDetail;
-                SettingManager.Local.ColorDetailBackcolor = this.clDetailBackcolor;
-                SettingManager.Local.ColorDetailLink = this.clDetailLink;
-                SettingManager.Local.ColorFav = this.clFav;
-                SettingManager.Local.ColorOWL = this.clOWL;
-                SettingManager.Local.ColorRetweet = this.clRetweet;
-                SettingManager.Local.ColorSelf = this.clSelf;
-                SettingManager.Local.ColorAtSelf = this.clAtSelf;
-                SettingManager.Local.ColorTarget = this.clTarget;
-                SettingManager.Local.ColorAtTarget = this.clAtTarget;
-                SettingManager.Local.ColorAtFromTarget = this.clAtFromTarget;
-                SettingManager.Local.ColorAtTo = this.clAtTo;
-                SettingManager.Local.ColorListBackcolor = this.clListBackcolor;
-                SettingManager.Local.ColorInputBackcolor = this.clInputBackcolor;
-                SettingManager.Local.ColorInputFont = this.clInputFont;
-                SettingManager.Local.FontInputFont = this.fntInputFont;
+                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;
-                SettingManager.SaveLocal();
+                this.settings.SaveLocal();
             }
         }
 
@@ -7108,12 +5831,23 @@ namespace OpenTween
                 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;
@@ -7121,20 +5855,19 @@ namespace OpenTween
             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;
             }
 
             try
             {
-                var statusId = long.Parse(match.Groups["StatusId"].Value);
+                var statusId = new TwitterStatusId(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);
@@ -7176,7 +5909,7 @@ namespace OpenTween
                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
                                  post.CreatedAt.ToLocalTimeString() + "\t" +
                                  post.ScreenName + "\t" +
-                                 post.StatusId + "\t" +
+                                 post.StatusId.Id + "\t" +
                                  post.ImageUrl + "\t" +
                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
                                  protect);
@@ -7193,14 +5926,14 @@ namespace OpenTween
                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
                                  post.CreatedAt.ToLocalTimeString() + "\t" +
                                  post.ScreenName + "\t" +
-                                 post.StatusId + "\t" +
+                                 post.StatusId.Id + "\t" +
                                  post.ImageUrl + "\t" +
                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
                                  protect);
                     }
                 }
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
         }
 
         public bool TabRename(string origTabName, [NotNullWhen(true)] out string? newTabName)
@@ -7214,7 +5947,7 @@ namespace OpenTween
                 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))
             {
                 // 新タブ名存在チェック
@@ -7234,6 +5967,10 @@ namespace OpenTween
 
                 this.statuses.RenameTab(origTabName, newTabName);
 
+                var state = this.listViewState[origTabName];
+                this.listViewState.Remove(origTabName);
+                this.listViewState[newTabName] = state;
+
                 this.SaveConfigsCommon();
                 this.SaveConfigsTabs();
                 this.rclickTabName = newTabName;
@@ -7266,7 +6003,7 @@ namespace OpenTween
 
         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))
@@ -7343,6 +6080,11 @@ namespace OpenTween
 
             using (ControlTransaction.Layout(this.ListTab))
             {
+                // 選択中のタブを Remove メソッドで取り外すと選択状態が変化して Selecting イベントが発生するが、
+                // この時 TabInformations と TabControl の並び順が不一致なままで ListTabSelect メソッドが呼ばれてしまう。
+                // これを防ぐために、Remove メソッドを呼ぶ前に選択中のタブを切り替えておく必要がある
+                this.ListTab.SelectedIndex = targetIndex == 0 ? 1 : 0;
+
                 var tab = this.statuses.Tabs[targetIndex];
                 var tabPage = this.ListTab.TabPages[targetIndex];
 
@@ -7454,16 +6196,16 @@ namespace OpenTween
             if (busyTasks)
             {
                 this.iconCnt += 1;
-                if (this.iconCnt >= this.nIconRefresh.Length)
+                if (this.iconCnt >= this.iconAssets.IconTrayRefresh.Length)
                     this.iconCnt = 0;
 
-                this.NotifyIcon1.Icon = this.nIconRefresh[this.iconCnt];
+                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)
             {
@@ -7481,7 +6223,7 @@ namespace OpenTween
                 if (this.blinkCnt == 0)
                     this.blink = !this.blink;
 
-                this.NotifyIcon1.Icon = this.blink ? this.replyIconBlink : this.replyIcon;
+                this.NotifyIcon1.Icon = this.blink ? this.iconAssets.IconTrayReplyBlink : this.iconAssets.IconTrayReply;
                 EnableTasktrayAnimation();
                 return;
             }
@@ -7495,13 +6237,13 @@ namespace OpenTween
             // 優先度:リプライ→エラー→オフライン→アイドル
             // エラーは更新アイコンでクリアされる
             if (replyIconType == MyCommon.REPLY_ICONSTATE.StaticIcon && reply)
-                this.NotifyIcon1.Icon = this.replyIcon;
+                this.NotifyIcon1.Icon = this.iconAssets.IconTrayReply;
             else if (this.myStatusError)
-                this.NotifyIcon1.Icon = this.nIconAtRed;
+                this.NotifyIcon1.Icon = this.iconAssets.IconTrayError;
             else if (this.myStatusOnline)
-                this.NotifyIcon1.Icon = this.nIconAt;
+                this.NotifyIcon1.Icon = this.iconAssets.IconTray;
             else
-                this.NotifyIcon1.Icon = this.nIconAtSmoke;
+                this.NotifyIcon1.Icon = this.iconAssets.IconTrayOffline;
         }
 
         private void TimerRefreshIcon_Tick(object sender, EventArgs e)
@@ -7620,7 +6362,7 @@ namespace OpenTween
             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)
@@ -7631,13 +6373,13 @@ namespace OpenTween
 
             if (this.CurrentTabName == tabName)
             {
-                this.PurgeListViewItemCache();
+                this.listCache?.PurgeCache();
                 this.CurrentListView.Refresh();
             }
 
             this.SetMainWindowTitle();
             this.SetStatusLabelUrl();
-            if (!SettingManager.Common.TabIconDisp) this.ListTab.Refresh();
+            if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
         }
 
         private void NotifyDispMenuItem_Click(object sender, EventArgs e)
@@ -7680,7 +6422,7 @@ namespace OpenTween
                 fltDialog.SetCurrent(this.rclickTabName);
                 fltDialog.ShowDialog(this);
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
 
             this.ApplyPostFilters();
             this.SaveConfigsTabs();
@@ -7699,7 +6441,7 @@ namespace OpenTween
                 tabName = inputName.TabName;
                 tabUsage = inputName.Usage;
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
             if (!MyCommon.IsNullOrEmpty(tabName))
             {
                 // List対応
@@ -7781,7 +6523,7 @@ namespace OpenTween
                     fltDialog.ShowDialog(this);
                 }
 
-                this.TopMost = SettingManager.Common.AlwaysTop;
+                this.TopMost = this.settings.Common.AlwaysTop;
             }
 
             this.ApplyPostFilters();
@@ -7798,7 +6540,7 @@ namespace OpenTween
                     var newLine = false;
                     var post = false;
 
-                    if (SettingManager.Common.PostCtrlEnter) // Ctrl+Enter投稿時
+                    if (this.settings.Common.PostCtrlEnter) // Ctrl+Enter投稿時
                     {
                         if (this.StatusText.Multiline)
                         {
@@ -7811,7 +6553,7 @@ namespace OpenTween
                             if ((keyData & Keys.Control) == Keys.Control) post = true;
                         }
                     }
-                    else if (SettingManager.Common.PostShiftEnter) // SHift+Enter投稿時
+                    else if (this.settings.Common.PostShiftEnter) // SHift+Enter投稿時
                     {
                         if (this.StatusText.Multiline)
                         {
@@ -8020,7 +6762,7 @@ namespace OpenTween
                         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);
@@ -8166,7 +6908,7 @@ namespace OpenTween
                 if (this.urlDialog.ShowDialog(this) != DialogResult.OK)
                     return;
 
-                this.TopMost = SettingManager.Common.AlwaysTop;
+                this.TopMost = this.settings.Common.AlwaysTop;
 
                 selectedUrl = this.urlDialog.SelectedUrl;
 
@@ -8197,19 +6939,16 @@ namespace OpenTween
             this.statuses.ClearTabIds(tabName);
             if (this.CurrentTabName == tabName)
             {
-                this.anchorPost = null;
-                this.anchorFlag = false;
-                this.PurgeListViewItemCache();
+                this.CurrentTab.ClearAnchor();
+                this.listCache?.PurgeCache();
+                this.listCache?.UpdateListSize();
             }
 
             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) this.ListTab.Refresh();
+            if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
 
             this.SetMainWindowTitle();
             this.SetStatusLabelUrl();
@@ -8223,10 +6962,10 @@ namespace OpenTween
             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 this.statuses.Tabs)
                 {
@@ -8235,10 +6974,10 @@ namespace OpenTween
                 }
             }
 
-            if (SettingManager.Common.DispUsername) ttl.Append(this.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());
@@ -8313,13 +7052,13 @@ namespace OpenTween
             var homeTab = this.statuses.HomeTab;
 
             slbl.AppendFormat(Properties.Resources.SetStatusLabelText1, tur, tal, ur, al, urat, this.postTimestamps.Count, this.favTimestamps.Count, homeTab.TweetsPerHour);
-            if (SettingManager.Common.TimelinePeriod == 0)
+            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();
         }
@@ -8357,7 +7096,7 @@ namespace OpenTween
                 // 表示中のタブに応じて更新
                 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",
@@ -8373,18 +7112,26 @@ namespace OpenTween
             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;
@@ -8405,7 +7152,7 @@ namespace OpenTween
             // タスクトレイアイコンのツールチップテキスト書き換え
             // Tween [未読/@]
             ur.Remove(0, ur.Length);
-            if (SettingManager.Common.DispUsername)
+            if (this.settings.Common.DispUsername)
             {
                 ur.Append(this.tw.Username);
                 ur.Append(" - ");
@@ -8448,7 +7195,7 @@ namespace OpenTween
             // 本当にリプライ先指定すべきかどうかの判定
             m = Regex.Matches(statusText, "(^|[ -/:-@[-^`{-~])(?<id>@[a-zA-Z0-9_]+)");
 
-            if (SettingManager.Common.UseAtIdSupplement)
+            if (this.settings.Common.UseAtIdSupplement)
             {
                 var bCnt = this.AtIdSupl.ItemCount;
                 foreach (Match mid in m)
@@ -8490,19 +7237,19 @@ namespace OpenTween
 
         private void TweenMain_Resize(object sender, EventArgs e)
         {
-            if (!this.initialLayout && SettingManager.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
+            if (!this.initialLayout && this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
             {
                 this.Visible = false;
             }
-            if (this.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)
                 {
@@ -8510,10 +7257,10 @@ namespace OpenTween
                 }
 
                 // 発言欄複数行
-                this.StatusText.Multiline = SettingManager.Local.StatusMultiline;
+                this.StatusText.Multiline = this.settings.Local.StatusMultiline;
                 if (this.StatusText.Multiline)
                 {
-                    var statusTextHeight = ScaleBy(configScaleFactor.Height, SettingManager.Local.StatusTextHeight);
+                    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)
                     {
@@ -8529,7 +7276,7 @@ namespace OpenTween
                     }
                 }
 
-                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;
@@ -8552,11 +7299,11 @@ namespace OpenTween
             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();
         }
@@ -8593,10 +7340,10 @@ namespace OpenTween
             {
                 if (MyCommon.IsKeyDown(Keys.Shift))
                 {
-                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
+                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
                     return;
                 }
-                if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost))
+                if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId, out var repPost))
                 {
                     MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
                 }
@@ -8604,12 +7351,12 @@ namespace OpenTween
                 {
                     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];
+                        if (tb == null || !tb.Contains(currentPost.InReplyToStatusId)) break;
+                        repPost = tb.Posts[currentPost.InReplyToStatusId];
                         MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
                         return;
                     }
-                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
+                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
                 }
             }
         }
@@ -8626,7 +7373,7 @@ namespace OpenTween
             if (multiline != this.StatusText.Multiline)
             {
                 this.StatusText.Multiline = multiline;
-                SettingManager.Local.StatusMultiline = multiline;
+                this.settings.Local.StatusMultiline = multiline;
                 this.MarkSettingLocalModified();
             }
         }
@@ -8647,7 +7394,7 @@ namespace OpenTween
             // 発言欄複数行
             var menuItemChecked = ((ToolStripMenuItem)sender).Checked;
             this.StatusText.Multiline = menuItemChecked;
-            SettingManager.Local.StatusMultiline = menuItemChecked;
+            this.settings.Local.StatusMultiline = menuItemChecked;
             if (menuItemChecked)
             {
                 if (this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth < 0)
@@ -8667,8 +7414,8 @@ namespace OpenTween
             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;
@@ -8692,7 +7439,7 @@ namespace OpenTween
                     // 文字列が選択されている場合はその文字列について処理
 
                     // nico.ms使用、nicovideoにマッチしたら変換
-                    if (SettingManager.Common.Nicoms && Regex.IsMatch(tmp, nico))
+                    if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico))
                     {
                         result = Nicoms.Shorten(tmp);
                     }
@@ -8768,7 +7515,7 @@ namespace OpenTween
                     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))
+                    if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico))
                     {
                         result = Nicoms.Shorten(tmp);
                     }
@@ -8864,7 +7611,7 @@ namespace OpenTween
 
         private async void UrlConvertAutoToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            if (!await this.UrlConvertAsync(SettingManager.Common.AutoShortUrlFirst))
+            if (!await this.UrlConvertAsync(this.settings.Common.AutoShortUrlFirst))
             {
                 var rnd = new Random();
 
@@ -8874,7 +7621,7 @@ namespace OpenTween
                 {
                     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);
+                while (svc == this.settings.Common.AutoShortUrlFirst || svc == MyCommon.UrlConverter.Nicoms || svc == MyCommon.UrlConverter.Unu);
                 await this.UrlConvertAsync(svc);
             }
         }
@@ -8886,7 +7633,7 @@ namespace OpenTween
         {
             this.NotifyFileMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
             this.NewPostPopMenuItem.Checked = this.NotifyFileMenuItem.Checked;
-            SettingManager.Common.NewAllPop = this.NewPostPopMenuItem.Checked;
+            this.settings.Common.NewAllPop = this.NewPostPopMenuItem.Checked;
             this.MarkSettingCommonModified();
         }
 
@@ -8894,7 +7641,7 @@ namespace OpenTween
         {
             this.ListLockMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
             this.LockListFileMenuItem.Checked = this.ListLockMenuItem.Checked;
-            SettingManager.Common.ListLock = this.ListLockMenuItem.Checked;
+            this.settings.Common.ListLock = this.ListLockMenuItem.Checked;
             this.MarkSettingCommonModified();
         }
 
@@ -8926,62 +7673,24 @@ namespace OpenTween
 
         private void MyList_ColumnReordered(object sender, ColumnReorderedEventArgs e)
         {
-            var lst = (DetailsListView)sender;
-            if (SettingManager.Local == null) return;
-
-            if (this.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();
             this.isColumnChanged = true;
         }
@@ -8989,62 +7698,31 @@ namespace OpenTween
         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 (this.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)
-                {
-                    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)
+                var columnsCount = lst.Columns.Count;
+                for (var i = 0; i < columnsCount; i++)
                 {
-                    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;
                 }
             }
@@ -9212,8 +7890,8 @@ namespace OpenTween
 
             // ユーザープロフィール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/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)$");
                 if (userUriMatch.Success)
@@ -9240,26 +7918,35 @@ namespace OpenTween
             var match = Regex.Match(uri.AbsolutePath, @"^/status/(\d+)$");
             if (match.Success)
             {
-                var statusId = long.Parse(match.Groups[1].Value);
+                var statusId = new TwitterStatusId(match.Groups[1].Value);
                 await this.OpenRelatedTab(statusId);
                 return;
             }
         }
 
-        private void ListTabSelect(TabPage tab)
+        private void ListTabSelect(TabPage tabPage)
         {
             this.SetListProperty();
 
-            this.PurgeListViewItemCache();
+            var previousTabName = this.CurrentTabName;
+            if (this.listViewState.TryGetValue(previousTabName, out var previousListViewState))
+                previousListViewState.Save(this.ListLockMenuItem.Checked);
+
+            this.listCache?.PurgeCache();
 
-            this.statuses.SelectTab(tab.Text);
+            this.statuses.SelectTab(tabPage.Text);
+
+            this.InitializeTimelineListView();
+
+            var tab = this.CurrentTab;
+            tab.ClearAnchor();
 
             var listView = this.CurrentListView;
 
-            this.anchorPost = null;
-            this.anchorFlag = false;
+            var currentListViewState = this.listViewState[tab.TabName];
+            currentListViewState.Restore(forceScroll: true);
 
-            if (this.iconCol)
+            if (this.Use2ColumnsMode)
             {
                 listView.Columns[1].Text = this.columnText[2];
             }
@@ -9272,6 +7959,23 @@ namespace OpenTween
             }
         }
 
+        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);
+            (this.listDrawer, var oldDrawer) = (newDrawer, this.listDrawer);
+            oldDrawer?.Dispose();
+
+            newDrawer.IconSize = this.settings.Common.IconSize;
+            newDrawer.UpdateItemHeight();
+        }
+
         private void ListTab_Selecting(object sender, TabControlCancelEventArgs e)
             => this.ListTabSelect(e.TabPage);
 
@@ -9299,38 +8003,6 @@ namespace OpenTween
             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);
-        }
-
         private async void TweenMain_Shown(object sender, EventArgs e)
         {
             this.NotifyIcon1.Visible = true;
@@ -9351,10 +8023,10 @@ namespace OpenTween
                     this.RefreshTabAsync<ListTimelineTabModel>(),
                 };
 
-                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<FavoritesTabModel>());
 
                 var allTasks = Task.WhenAll(loadTasks);
@@ -9378,7 +8050,7 @@ namespace OpenTween
                 if (ApplicationSettings.VersionInfoUrl != null)
                 {
                     // バージョンチェック(引数:起動時チェックの場合はtrue・・・チェック結果のメッセージを表示しない)
-                    if (SettingManager.Common.StartupVersion)
+                    if (this.settings.Common.StartupVersion)
                         await this.CheckNewVersion(true);
                 }
                 else
@@ -9399,7 +8071,7 @@ namespace OpenTween
                 // 取得失敗の場合は再試行する
                 var reloadTasks = new List<Task>();
 
-                if (!this.tw.GetFollowersSuccess && SettingManager.Common.StartupFollowers)
+                if (!this.tw.GetFollowersSuccess && this.settings.Common.StartupFollowers)
                     reloadTasks.Add(this.RefreshFollowerIdsAsync());
 
                 if (!this.tw.GetNoRetweetSuccess)
@@ -9435,7 +8107,7 @@ namespace OpenTween
             {
                 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.");
@@ -9464,7 +8136,7 @@ namespace OpenTween
                 }
                 else
                 {
-                    if (!SettingManager.Common.RetweetNoConfirm)
+                    if (!this.settings.Common.RetweetNoConfirm)
                     {
                         var questiontext = Properties.Resources.RetweetQuestion1;
                         if (this.doFavRetweetFlags) questiontext = Properties.Resources.FavoritesRetweetQuestionText2;
@@ -9531,13 +8203,13 @@ namespace OpenTween
             // TweetFormatterクラスによって整形された状態のHTMLを元のツイートに復元します
 
             // 通常の URL
-            statusHtml = Regex.Replace(statusHtml, "<a href=\"(?<href>.+?)\" title=\"(?<title>.+?)\">(?<text>.+?)</a>", "${title}");
+            statusHtml = Regex.Replace(statusHtml, """<a href="(?<href>.+?)" title="(?<title>.+?)">(?<text>.+?)</a>""", "${title}");
             // メンション
-            statusHtml = Regex.Replace(statusHtml, "<a class=\"mention\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+            statusHtml = Regex.Replace(statusHtml, """<a class="mention" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
             // ハッシュタグ
-            statusHtml = Regex.Replace(statusHtml, "<a class=\"hashtag\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+            statusHtml = Regex.Replace(statusHtml, """<a class="hashtag" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
             // 絵文字
-            statusHtml = Regex.Replace(statusHtml, "<img class=\"emoji\" src=\".+?\" alt=\"(?<text>.+?)\" />", "${text}");
+            statusHtml = Regex.Replace(statusHtml, """<img class="emoji" src=".+?" alt="(?<text>.+?)" />""", "${text}");
 
             // <br> 除去
             if (multiline)
@@ -9575,17 +8247,17 @@ namespace OpenTween
             => 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();
         }
 
@@ -9593,20 +8265,20 @@ namespace OpenTween
         {
             this.UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit;
             this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand;
-            this.UrlAutoShortenMenuItem.Checked = SettingManager.Common.UrlConvertAuto;
-            this.IdeographicSpaceToSpaceMenuItem.Checked = SettingManager.Common.WideSpaceConvert;
-            this.MultiLineMenuItem.Checked = SettingManager.Local.StatusMultiline;
-            this.FocusLockMenuItem.Checked = SettingManager.Common.FocusLockToStatusText;
+            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)
         {
             this.UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit;
             this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand;
-            this.UrlAutoShortenPullDownMenuItem.Checked = SettingManager.Common.UrlConvertAuto;
-            this.IdeographicSpaceToSpacePullDownMenuItem.Checked = SettingManager.Common.WideSpaceConvert;
-            this.MultiLinePullDownMenuItem.Checked = SettingManager.Local.StatusMultiline;
-            this.FocusLockPullDownMenuItem.Checked = SettingManager.Common.FocusLockToStatusText;
+            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)
@@ -9692,7 +8364,7 @@ namespace OpenTween
             {
                 try
                 {
-                    var task = this.twitterApi.FriendshipsCreate(id).IgnoreResponse();
+                    var task = this.tw.Api.FriendshipsCreate(id).IgnoreResponse();
                     await dialog.WaitForAsync(this, task);
                 }
                 catch (WebApiException ex)
@@ -9733,7 +8405,7 @@ namespace OpenTween
             {
                 try
                 {
-                    var task = this.twitterApi.FriendshipsDestroy(id).IgnoreResponse();
+                    var task = this.tw.Api.FriendshipsDestroy(id).IgnoreResponse();
                     await dialog.WaitForAsync(this, task);
                 }
                 catch (WebApiException ex)
@@ -9777,7 +8449,7 @@ namespace OpenTween
 
                 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;
@@ -9827,7 +8499,7 @@ namespace OpenTween
 
                     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;
@@ -10016,9 +8688,9 @@ namespace OpenTween
                 cmb.Items.Insert(0, tb.SearchWords);
                 cmb.Text = tb.SearchWords;
                 cmb.SelectAll();
-                this.PurgeListViewItemCache();
-                listView.VirtualListSize = 0;
                 this.statuses.ClearTabIds(tbName);
+                this.listCache?.PurgeCache();
+                this.listCache?.UpdateListSize();
                 this.SaveConfigsTabs();   // 検索条件の保存
             }
 
@@ -10040,83 +8712,19 @@ namespace OpenTween
 
         private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e)
         {
-            if (this.statuses.RemovedTab.Count == 0)
-            {
-                MessageBox.Show("There isn't removed tab.", "Undo", MessageBoxButtons.OK, MessageBoxIcon.Information);
-                return;
-            }
-            else
+            try
             {
-                DetailsListView? listView;
-
-                var tb = this.statuses.RemovedTab.Pop();
-                if (tb.TabType == 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);
-
-                        var tabIndex = this.statuses.Tabs.IndexOf(tb);
-                        var tabPage = this.ListTab.TabPages[tabIndex];
-                        listView = (DetailsListView)tabPage.Tag;
-                        this.ListTab.SelectedIndex = tabIndex;
-                    }
-                    else
-                    {
-                        const string TabName = "Related Tweets";
-                        var renamed = TabName;
-                        for (var i = 2; i <= 100; i++)
-                        {
-                            if (!this.statuses.ContainsTab(renamed))
-                                break;
-                            renamed = TabName + i;
-                        }
-                        tb.TabName = renamed;
-
-                        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;
-                        this.ListTab.SelectedIndex = tabIndex;
-                    }
-                }
-                else
-                {
-                    var renamed = tb.TabName;
-                    for (var i = 1; i < int.MaxValue; i++)
-                    {
-                        if (!this.statuses.ContainsTab(renamed))
-                            break;
-                        renamed = tb.TabName + "(" + i + ")";
-                    }
-                    tb.TabName = renamed;
+                var restoredTab = this.statuses.UndoRemovedTab();
+                this.AddNewTab(restoredTab, startup: false);
 
-                    this.statuses.AddTab(tb);
-                    this.AddNewTab(tb, startup: false);
-
-                    var tabIndex = this.statuses.Tabs.Count - 1;
-                    var tabPage = this.ListTab.TabPages[tabIndex];
+                var tabIndex = this.statuses.Tabs.Count - 1;
+                this.ListTab.SelectedIndex = tabIndex;
 
-                    listView = (DetailsListView)tabPage.Tag;
-                    this.ListTab.SelectedIndex = tabIndex;
-                }
                 this.SaveConfigsTabs();
-
-                if (listView != null)
-                {
-                    using (ControlTransaction.Update(listView))
-                    {
-                        listView.VirtualListSize = tb.AllCount;
-                    }
-                }
+            }
+            catch (TabException ex)
+            {
+                MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
             }
         }
 
@@ -10146,7 +8754,7 @@ namespace OpenTween
 
         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);
         }
 
@@ -10189,7 +8797,7 @@ namespace OpenTween
             {
                 return;
             }
-            this.TopMost = SettingManager.Common.AlwaysTop;
+            this.TopMost = this.settings.Common.AlwaysTop;
             if (rslt == DialogResult.Cancel) return;
             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
             {
@@ -10292,7 +8900,7 @@ namespace OpenTween
                 this.OpenStatusOpMenuItem.Enabled = true;
                 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;
@@ -10366,14 +8974,7 @@ namespace OpenTween
 
         private void MenuItemEdit_DropDownOpening(object sender, EventArgs e)
         {
-            if (this.statuses.RemovedTab.Count == 0)
-            {
-                this.UndoRemoveTabMenuItem.Enabled = false;
-            }
-            else
-            {
-                this.UndoRemoveTabMenuItem.Enabled = true;
-            }
+            this.UndoRemoveTabMenuItem.Enabled = this.statuses.CanUndoRemovedTab;
 
             if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
                 this.PublicSearchQueryMenuItem.Enabled = true;
@@ -10429,7 +9030,7 @@ namespace OpenTween
 
                 try
                 {
-                    var task = this.twitterApi.UsersShow(id);
+                    var task = this.tw.Api.UsersShow(id);
                     user = await dialog.WaitForAsync(this, task);
                 }
                 catch (WebApiException ex)
@@ -10448,7 +9049,7 @@ namespace OpenTween
 
         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);
 
@@ -10498,7 +9099,7 @@ namespace OpenTween
 
                 try
                 {
-                    var task = this.twitterApi.StatusesShow(statusId);
+                    var task = this.tw.Api.StatusesShow(statusId.ToTwitterStatusId());
                     status = await dialog.WaitForAsync(this, task);
                 }
                 catch (WebApiException ex)
@@ -10515,41 +9116,6 @@ namespace OpenTween
             MessageBox.Show(status.RetweetCount + Properties.Resources.RtCountText1);
         }
 
-        private readonly HookGlobalHotkey hookGlobalHotkey;
-
-        public TweenMain()
-        {
-            this.hookGlobalHotkey = new HookGlobalHotkey(this);
-
-            // この呼び出しは、Windows フォーム デザイナで必要です。
-            this.InitializeComponent();
-
-            // InitializeComponent() 呼び出しの後で初期化を追加します。
-
-            if (!this.DesignMode)
-            {
-                // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策
-                this.StatusText.Dock = DockStyle.Fill;
-            }
-
-            this.tweetDetailsView.Owner = this;
-
-            this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed;
-            this.gh.NotifyClicked += this.GrowlHelper_Callback;
-
-            // メイリオフォント指定時にタブの最小幅が広くなる問題の対策
-            this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40);
-
-            this.ImageSelector.Visible = false;
-            this.ImageSelector.Enabled = false;
-            this.ImageSelector.FilePickDialog = this.OpenFileDialog1;
-
-            this.workerProgress = new Progress<string>(x => this.StatusLabel.Text = x);
-
-            this.ReplaceAppName();
-            this.InitializeShortcuts();
-        }
-
         private void HookGlobalHotkey_HotkeyPressed(object sender, KeyEventArgs e)
         {
             if ((this.WindowState == FormWindowState.Normal || this.WindowState == FormWindowState.Maximized) && this.Visible && Form.ActiveForm == this)
@@ -10581,7 +9147,7 @@ namespace OpenTween
 
         private void SelectMedia_DragEnter(DragEventArgs e)
         {
-            if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
+            if (this.ImageSelector.Model.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
             {
                 e.Effect = DragDropEffects.Copy;
                 return;
@@ -10593,7 +9159,10 @@ namespace OpenTween
         {
             this.Activate();
             this.BringToFront();
-            this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false));
+
+            var filePathArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
+            this.ImageSelector.BeginSelection();
+            this.ImageSelector.Model.AddMediaItemFromFilePath(filePathArray);
             this.StatusText.Focus();
         }
 
@@ -10642,23 +9211,16 @@ namespace OpenTween
                 }
                 else if (Clipboard.ContainsImage())
                 {
-                    // 画像があるので投稿処理を行う
-                    if (MessageBox.Show(Properties.Resources.PostPictureConfirm3,
-                                       Properties.Resources.PostPictureWarn4,
-                                       MessageBoxButtons.OKCancel,
-                                       MessageBoxIcon.Question,
-                                       MessageBoxDefaultButton.Button2)
-                                   == DialogResult.OK)
-                    {
-                        // clipboardから画像を取得
-                        using var image = Clipboard.GetImage();
-                        this.ImageSelector.BeginSelection(image);
-                    }
+                    // clipboardから画像を取得
+                    using var image = Clipboard.GetImage();
+                    this.ImageSelector.BeginSelection();
+                    this.ImageSelector.Model.AddMediaItemFromImage(image);
                 }
                 else if (Clipboard.ContainsFileDropList())
                 {
                     var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
-                    this.ImageSelector.BeginSelection(files);
+                    this.ImageSelector.BeginSelection();
+                    this.ImageSelector.Model.AddMediaItemFromFilePath(files);
                 }
             }
             catch (ExternalException ex)
@@ -10728,14 +9290,14 @@ namespace OpenTween
         /// </summary>
         /// <param name="statusId">表示するツイートのID</param>
         /// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
-        public async Task OpenRelatedTab(long statusId)
+        public async Task OpenRelatedTab(PostId statusId)
         {
             var post = this.statuses[statusId];
             if (post == null)
             {
                 try
                 {
-                    post = await this.tw.GetStatusApi(false, statusId);
+                    post = await this.tw.GetStatusApi(false, statusId.ToTwitterStatusId());
                 }
                 catch (WebApiException ex)
                 {
@@ -10881,7 +9443,7 @@ namespace OpenTween
             if (curTimeOffset != prevTimeOffset)
             {
                 // タイムゾーンの変更を反映
-                this.PurgeListViewItemCache();
+                this.listCache?.PurgeCache();
                 this.CurrentListView.Refresh();
 
                 this.DispSelectedPost(forceupdate: true);
@@ -10900,25 +9462,25 @@ namespace OpenTween
 
         private async Task OpenUserAppointUrl()
         {
-            if (SettingManager.Common.UserAppointUrl != null)
+            if (!MyCommon.IsNullOrEmpty(this.settings.Common.UserAppointUrl))
             {
-                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;
-                        xUrl = xUrl.Replace("{STATUS}", statusId.ToString());
+                        xUrl = xUrl.Replace("{STATUS}", statusId.Id);
 
                         await MyCommon.OpenInBrowserAsync(this, xUrl);
                     }
                 }
                 else
                 {
-                    await MyCommon.OpenInBrowserAsync(this, SettingManager.Common.UserAppointUrl);
+                    await MyCommon.OpenInBrowserAsync(this, this.settings.Common.UserAppointUrl);
                 }
             }
         }
@@ -10938,11 +9500,11 @@ namespace OpenTween
                     this.BringToFront();
                     if (e.NotifyType == GrowlHelper.NotifyType.DirectMessage)
                     {
-                        if (!this.GoDirectMessage(e.StatusId)) this.StatusText.Focus();
+                        if (!this.GoDirectMessage(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
                     }
                     else
                     {
-                        if (!this.GoStatus(e.StatusId)) this.StatusText.Focus();
+                        if (!this.GoStatus(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
                     }
                 });
             }
@@ -10954,22 +9516,6 @@ namespace OpenTween
             this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text);
         }
 
-        private void TweetThumbnail_ThumbnailLoading(object sender, EventArgs e)
-            => this.SplitContainer3.Panel2Collapsed = false;
-
-        private async void TweetThumbnail_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e)
-            => await this.OpenThumbnailPicture(e.Thumbnail);
-
-        private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e)
-            => await MyCommon.OpenInBrowserAsync(this, e.ImageUrl);
-
-        private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail)
-        {
-            var url = thumbnail.FullSizeImageUrl ?? thumbnail.MediaPageUrl;
-
-            await MyCommon.OpenInBrowserAsync(this, url);
-        }
-
         private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
             => await MyCommon.OpenInBrowserAsync(this, Twitter.ServiceAvailabilityStatusUrl);
 
@@ -10985,13 +9531,13 @@ namespace OpenTween
 
         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)
@@ -11011,14 +9557,14 @@ namespace OpenTween
 
         private void ChangeListViewIconSize(MyCommon.IconSizes iconSize)
         {
-            if (SettingManager.Common.IconSize == iconSize) return;
+            if (this.settings.Common.IconSize == iconSize) return;
 
-            var oldIconCol = this.iconCol;
+            var oldIconCol = this.Use2ColumnsMode;
 
-            SettingManager.Common.IconSize = iconSize;
+            this.settings.Common.IconSize = iconSize;
             this.ApplyListViewIconSize(iconSize);
 
-            if (this.iconCol != oldIconCol)
+            if (this.Use2ColumnsMode != oldIconCol)
             {
                 foreach (TabPage tp in this.ListTab.TabPages)
                 {
@@ -11033,9 +9579,9 @@ namespace OpenTween
         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();
         }