diff --git a/OpenTween/MediaHandler.cs b/OpenTween/MediaHandler.cs
new file mode 100644
index 000000000..7db43799f
--- /dev/null
+++ b/OpenTween/MediaHandler.cs
@@ -0,0 +1,101 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using OpenTween.Models;
+using OpenTween.Thumbnail;
+
+namespace OpenTween
+{
+ public enum MediaHandlerType
+ {
+ /// 外部ブラウザで開く
+ ExternalBrowser,
+
+ /// 軽量ビューアーで開く
+ LightViewer,
+
+ /// 埋め込みブラウザで開く
+ WebBrowserViewer,
+ }
+
+ public class MediaHandler
+ {
+ public MediaHandlerType MediaHandlerType { get; set; }
+
+ public Func? OpenInBrowser { get; set; }
+
+ public async Task OpenMediaViewer(IWin32Window owner, ThumbnailInfo[] thumbnails, int displayIndex)
+ {
+ switch (this.MediaHandlerType)
+ {
+ case MediaHandlerType.ExternalBrowser:
+ await this.OpenMediaInExternalBrowser(owner, thumbnails, displayIndex);
+ break;
+ case MediaHandlerType.LightViewer:
+ await this.OpenMediaInLightViewer(owner, thumbnails, displayIndex);
+ break;
+ case MediaHandlerType.WebBrowserViewer:
+ this.OpenMediaInWebBrowserViewer(owner, thumbnails, displayIndex);
+ break;
+ default:
+ throw new InvalidEnumArgumentException();
+ }
+ }
+
+ public async Task OpenMediaInExternalBrowser(IWin32Window owner, ThumbnailInfo[] thumbnails, int displayIndex)
+ {
+ var mediaUrl = thumbnails[displayIndex].MediaPageUrl;
+ if (this.OpenInBrowser != null)
+ await this.OpenInBrowser(owner, mediaUrl);
+ }
+
+ public async Task OpenMediaInLightViewer(IWin32Window owner, ThumbnailInfo[] thumbnails, int displayIndex)
+ {
+ using var viewer = new MediaViewerLight();
+ using var viewerDialog = new MediaViewerLightDialog(viewer);
+
+ viewer.SetMediaItems(thumbnails);
+ var loadTask = Task.Run(() => viewer.SelectMedia(displayIndex));
+
+ viewerDialog.OpenInBrowser = this.OpenInBrowser;
+ viewerDialog.ShowDialog(owner);
+
+ await loadTask;
+ }
+
+ public void OpenMediaInWebBrowserViewer(IWin32Window owner, ThumbnailInfo[] thumbnails, int displayIndex)
+ {
+ var viewer = new MediaViewerWebBrowser();
+ viewer.SetMediaItems(thumbnails);
+ viewer.SelectMedia(displayIndex);
+
+ using var viewerDialog = new MediaViewerWebBrowserDialog(viewer);
+ viewerDialog.OpenInBrowser = this.OpenInBrowser;
+ viewerDialog.ShowDialog(owner);
+ }
+ }
+}
diff --git a/OpenTween/MediaViewerLightDialog.Designer.cs b/OpenTween/MediaViewerLightDialog.Designer.cs
new file mode 100644
index 000000000..85e7a819b
--- /dev/null
+++ b/OpenTween/MediaViewerLightDialog.Designer.cs
@@ -0,0 +1,82 @@
+namespace OpenTween
+{
+ partial class MediaViewerLightDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.pictureBox = new OpenTween.OTPictureBox();
+ this.progressBar = new System.Windows.Forms.ProgressBar();
+ ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).BeginInit();
+ this.SuspendLayout();
+ //
+ // pictureBox
+ //
+ this.pictureBox.AccessibleRole = System.Windows.Forms.AccessibleRole.Graphic;
+ this.pictureBox.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.pictureBox.Location = new System.Drawing.Point(0, 0);
+ this.pictureBox.Name = "pictureBox";
+ this.pictureBox.Size = new System.Drawing.Size(500, 500);
+ this.pictureBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
+ this.pictureBox.TabIndex = 1;
+ this.pictureBox.TabStop = false;
+ //
+ // progressBar
+ //
+ this.progressBar.Location = new System.Drawing.Point(90, 243);
+ this.progressBar.MarqueeAnimationSpeed = 25;
+ this.progressBar.Name = "progressBar";
+ this.progressBar.Size = new System.Drawing.Size(320, 15);
+ this.progressBar.TabIndex = 2;
+ //
+ // MediaViewerLightDialog
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
+ this.BackColor = System.Drawing.SystemColors.ControlDark;
+ this.ClientSize = new System.Drawing.Size(500, 500);
+ this.Controls.Add(this.progressBar);
+ this.Controls.Add(this.pictureBox);
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "MediaViewerLightDialog";
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show;
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MediaViewerLightDialog_FormClosing);
+ this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.MediaViewerLightDialog_KeyDown);
+ ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private OTPictureBox pictureBox;
+ private System.Windows.Forms.ProgressBar progressBar;
+ }
+}
\ No newline at end of file
diff --git a/OpenTween/MediaViewerLightDialog.cs b/OpenTween/MediaViewerLightDialog.cs
new file mode 100644
index 000000000..88ec7e3ca
--- /dev/null
+++ b/OpenTween/MediaViewerLightDialog.cs
@@ -0,0 +1,165 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using OpenTween.Models;
+
+namespace OpenTween
+{
+ public partial class MediaViewerLightDialog : OTBaseForm
+ {
+ public Func? OpenInBrowser;
+
+ private readonly MediaViewerLight model;
+
+ public MediaViewerLightDialog(MediaViewerLight model)
+ {
+ this.InitializeComponent();
+
+ this.model = model;
+ this.model.PropertyChanged +=
+ (s, e) => this.InvokeAsync(() => this.Model_PropertyChanged(s, e));
+
+ this.UpdateAll();
+ }
+
+ private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(MediaViewerLight.MediaItems):
+ case nameof(MediaViewerLight.DisplayMediaIndex):
+ this.UpdateTitle();
+ break;
+ case nameof(MediaViewerLight.LoadState):
+ this.UpdateLoadState();
+ break;
+ case nameof(MediaViewerLight.ImageSize):
+ case nameof(MediaViewerLight.ReceivedSize):
+ this.UpdateLoadProgress();
+ break;
+ case nameof(MediaViewerLight.Image):
+ this.UpdateImage();
+ break;
+ case "":
+ case null:
+ this.UpdateAll();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void UpdateAll()
+ {
+ this.UpdateTitle();
+ this.UpdateLoadState();
+ this.UpdateLoadProgress();
+ this.UpdateImage();
+ }
+
+ private void UpdateTitle()
+ {
+ const string TITLE_TEMPLATE = "{0}/{1}";
+
+ var mediaCount = this.model.MediaItems.Length;
+ var displayIndex = this.model.DisplayMediaIndex;
+
+ if (mediaCount == 1)
+ this.Text = "";
+ else
+ this.Text = string.Format(TITLE_TEMPLATE, displayIndex + 1, mediaCount);
+ }
+
+ private void UpdateLoadState()
+ {
+ switch (this.model.LoadState)
+ {
+ case MediaViewerLight.LoadStateEnum.BeforeLoad:
+ case MediaViewerLight.LoadStateEnum.HeaderArrived:
+ this.progressBar.Visible = true;
+ this.pictureBox.Visible = false;
+ break;
+ case MediaViewerLight.LoadStateEnum.LoadSuccessed:
+ this.progressBar.Visible = false;
+ this.pictureBox.Visible = true;
+ break;
+ case MediaViewerLight.LoadStateEnum.LoadError:
+ this.progressBar.Visible = false;
+ this.pictureBox.Visible = true;
+ this.pictureBox.ShowErrorImage();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void UpdateLoadProgress()
+ {
+ if (this.model.ImageSize != null && this.model.ReceivedSize != null)
+ {
+ this.progressBar.Maximum = (int?)this.model.ImageSize ?? 0;
+ this.progressBar.Value = (int?)this.model.ReceivedSize ?? 0;
+ this.progressBar.Style = ProgressBarStyle.Continuous;
+ }
+ else
+ {
+ this.progressBar.Style = ProgressBarStyle.Marquee;
+ }
+ }
+
+ private void UpdateImage()
+ => this.pictureBox.Image = this.model.Image;
+
+ private async void MediaViewerLightDialog_KeyDown(object sender, KeyEventArgs e)
+ {
+ switch (e.KeyData)
+ {
+ case Keys.Up:
+ case Keys.Left:
+ await this.model.SelectPreviousMedia();
+ break;
+ case Keys.Down:
+ case Keys.Right:
+ await this.model.SelectNextMedia();
+ break;
+ case Keys.Enter:
+ this.Close();
+ if (this.OpenInBrowser != null)
+ await this.OpenInBrowser(this, this.model.DisplayMedia.MediaPageUrl);
+ break;
+ case Keys.Escape:
+ this.Close();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void MediaViewerLightDialog_FormClosing(object sender, FormClosingEventArgs e)
+ => this.model.AbortLoad();
+ }
+}
diff --git a/OpenTween/MediaViewerLightDialog.resx b/OpenTween/MediaViewerLightDialog.resx
new file mode 100644
index 000000000..cabc33e77
--- /dev/null
+++ b/OpenTween/MediaViewerLightDialog.resx
@@ -0,0 +1,7 @@
+
+ text/microsoft-resx
+ 2.0
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/OpenTween/MediaViewerWebBrowserDialog.Designer.cs b/OpenTween/MediaViewerWebBrowserDialog.Designer.cs
new file mode 100644
index 000000000..2883b2e67
--- /dev/null
+++ b/OpenTween/MediaViewerWebBrowserDialog.Designer.cs
@@ -0,0 +1,72 @@
+namespace OpenTween
+{
+ partial class MediaViewerWebBrowserDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.webBrowser = new System.Windows.Forms.WebBrowser();
+ this.SuspendLayout();
+ //
+ // webBrowser
+ //
+ this.webBrowser.AllowWebBrowserDrop = false;
+ this.webBrowser.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.webBrowser.IsWebBrowserContextMenuEnabled = false;
+ this.webBrowser.Location = new System.Drawing.Point(0, 0);
+ this.webBrowser.MinimumSize = new System.Drawing.Size(20, 20);
+ this.webBrowser.Name = "webBrowser";
+ this.webBrowser.ScriptErrorsSuppressed = true;
+ this.webBrowser.ScrollBarsEnabled = false;
+ this.webBrowser.Size = new System.Drawing.Size(500, 500);
+ this.webBrowser.TabIndex = 0;
+ this.webBrowser.WebBrowserShortcutsEnabled = false;
+ this.webBrowser.DocumentCompleted += new System.Windows.Forms.WebBrowserDocumentCompletedEventHandler(this.WebBrowser_DocumentCompleted);
+ this.webBrowser.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.WebBrowser_PreviewKeyDown);
+ //
+ // MediaViewerWebBrowserDialog
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
+ this.BackColor = System.Drawing.SystemColors.ControlDark;
+ this.ClientSize = new System.Drawing.Size(500, 500);
+ this.Controls.Add(this.webBrowser);
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "MediaViewerWebBrowserDialog";
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show;
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.WebBrowser webBrowser;
+ }
+}
\ No newline at end of file
diff --git a/OpenTween/MediaViewerWebBrowserDialog.cs b/OpenTween/MediaViewerWebBrowserDialog.cs
new file mode 100644
index 000000000..20ce28485
--- /dev/null
+++ b/OpenTween/MediaViewerWebBrowserDialog.cs
@@ -0,0 +1,127 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using OpenTween.Models;
+
+namespace OpenTween
+{
+ public partial class MediaViewerWebBrowserDialog : OTBaseForm
+ {
+ public Func? OpenInBrowser;
+
+ private readonly MediaViewerWebBrowser model;
+
+ public MediaViewerWebBrowserDialog(MediaViewerWebBrowser model)
+ {
+ this.InitializeComponent();
+
+ this.model = model;
+ this.model.SetBackColor(new ColorRGB(this.BackColor));
+
+ this.model.PropertyChanged +=
+ (s, e) => this.InvokeAsync(() => this.Model_PropertyChanged(s, e));
+
+ this.UpdateAll();
+ }
+
+ private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(MediaViewerWebBrowser.MediaItems):
+ case nameof(MediaViewerWebBrowser.DisplayMediaIndex):
+ this.UpdateTitle();
+ break;
+ case nameof(MediaViewerWebBrowser.DisplayHTML):
+ this.UpdateHTML();
+ break;
+ case "":
+ case null:
+ this.UpdateAll();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void UpdateAll()
+ {
+ this.UpdateTitle();
+ this.UpdateHTML();
+ }
+
+ private void UpdateTitle()
+ {
+ const string TITLE_TEMPLATE = "{0}/{1}";
+
+ var mediaCount = this.model.MediaItems.Length;
+ var displayIndex = this.model.DisplayMediaIndex;
+
+ if (mediaCount == 1)
+ this.Text = "";
+ else
+ this.Text = string.Format(TITLE_TEMPLATE, displayIndex + 1, mediaCount);
+ }
+
+ private void UpdateHTML()
+ {
+ using (ControlTransaction.Update(this.webBrowser))
+ this.webBrowser.DocumentText = this.model.DisplayHTML;
+ }
+
+ private async void WebBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
+ {
+ e.IsInputKey = true;
+
+ switch (e.KeyData)
+ {
+ case Keys.Up:
+ case Keys.Left:
+ this.model.SelectPreviousMedia();
+ break;
+ case Keys.Down:
+ case Keys.Right:
+ this.model.SelectNextMedia();
+ break;
+ case Keys.Enter:
+ this.Close();
+ if (this.OpenInBrowser != null)
+ await this.OpenInBrowser.Invoke(this, this.model.DisplayMedia.MediaPageUrl);
+ break;
+ case Keys.Escape:
+ this.Close();
+ break;
+ default:
+ e.IsInputKey = false;
+ break;
+ }
+ }
+
+ private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
+ => this.webBrowser.Document.GetElementById("currentMedia")?.Focus();
+ }
+}
diff --git a/OpenTween/MediaViewerWebBrowserDialog.resx b/OpenTween/MediaViewerWebBrowserDialog.resx
new file mode 100644
index 000000000..cabc33e77
--- /dev/null
+++ b/OpenTween/MediaViewerWebBrowserDialog.resx
@@ -0,0 +1,7 @@
+
+ text/microsoft-resx
+ 2.0
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/OpenTween/Models/ColorRGB.cs b/OpenTween/Models/ColorRGB.cs
new file mode 100644
index 000000000..687eb3244
--- /dev/null
+++ b/OpenTween/Models/ColorRGB.cs
@@ -0,0 +1,44 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+namespace OpenTween.Models
+{
+ public readonly struct ColorRGB
+ {
+ public readonly int R;
+ public readonly int G;
+ public readonly int B;
+
+ public ColorRGB(int r, int g, int b)
+ {
+ this.R = r;
+ this.G = g;
+ this.B = b;
+ }
+
+ public ColorRGB(System.Drawing.Color color)
+ : this(color.R, color.G, color.B)
+ {
+ }
+ }
+}
diff --git a/OpenTween/Models/MediaViewerLight.cs b/OpenTween/Models/MediaViewerLight.cs
new file mode 100644
index 000000000..38bf9baf7
--- /dev/null
+++ b/OpenTween/Models/MediaViewerLight.cs
@@ -0,0 +1,273 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTween.Connection;
+using OpenTween.Thumbnail;
+
+namespace OpenTween.Models
+{
+ public sealed class MediaViewerLight : NotifyPropertyChangedBase, IDisposable
+ {
+ private ThumbnailInfo[] mediaItems = Array.Empty();
+ private int displayMediaIndex;
+ private string? imageUrl;
+ private MemoryImage? image;
+ private LoadStateEnum loadState;
+ private long? imageSize;
+ private long? receivedSize;
+
+ public ThumbnailInfo[] MediaItems
+ {
+ get => this.mediaItems;
+ private set => this.SetProperty(ref this.mediaItems, value);
+ }
+
+ public int DisplayMediaIndex
+ {
+ get => this.displayMediaIndex;
+ private set => this.SetProperty(ref this.displayMediaIndex, value);
+ }
+
+ public ThumbnailInfo DisplayMedia
+ => this.MediaItems[this.DisplayMediaIndex];
+
+ public string? ImageUrl
+ {
+ get => this.imageUrl;
+ private set => this.SetProperty(ref this.imageUrl, value);
+ }
+
+ public MemoryImage? Image
+ {
+ get => this.image;
+ private set => this.SetProperty(ref this.image, value);
+ }
+
+ public LoadStateEnum LoadState
+ {
+ get => this.loadState;
+ private set => this.SetProperty(ref this.loadState, value);
+ }
+
+ public long? ImageSize
+ {
+ get => this.imageSize;
+ private set => this.SetProperty(ref this.imageSize, value);
+ }
+
+ public long? ReceivedSize
+ {
+ get => this.receivedSize;
+ private set => this.SetProperty(ref this.receivedSize, value);
+ }
+
+ private CancellationTokenSource? cts;
+
+ public enum LoadStateEnum
+ {
+ BeforeLoad = 0,
+ HeaderArrived = 1,
+ LoadSuccessed = 2,
+ LoadError = 3,
+ }
+
+ public void SetMediaItems(ThumbnailInfo[] thumbnails)
+ {
+ this.DisplayMediaIndex = 0;
+ this.MediaItems = thumbnails;
+ }
+
+ public async Task SelectMedia(int displayIndex)
+ {
+ this.DisplayMediaIndex = displayIndex;
+
+ var media = this.MediaItems[displayIndex];
+ await this.LoadAsync(media);
+ }
+
+ public async Task SelectPreviousMedia()
+ {
+ var currentIndex = this.DisplayMediaIndex;
+ if (currentIndex == 0)
+ return;
+
+ await this.SelectMedia(currentIndex - 1);
+ }
+
+ public async Task SelectNextMedia()
+ {
+ var currentIndex = this.DisplayMediaIndex;
+ if (currentIndex == this.MediaItems.Length - 1)
+ return;
+
+ await this.SelectMedia(currentIndex + 1);
+ }
+
+ internal async Task LoadAsync(ThumbnailInfo media)
+ {
+ var newCts = new CancellationTokenSource();
+ var oldCts = Interlocked.Exchange(ref this.cts, newCts);
+ if (oldCts != null)
+ {
+ oldCts.Cancel();
+ oldCts.Dispose();
+ }
+
+ var imageUrl = media.FullSizeImageUrl ?? media.ThumbnailImageUrl;
+ if (imageUrl != null)
+ {
+ await this.LoadAsync(imageUrl, newCts.Token);
+ }
+ else
+ {
+ await this.LoadAsync(() => media.LoadThumbnailImageAsync(newCts.Token));
+ }
+ }
+
+ internal async Task LoadAsync(string imageUrl, CancellationToken cancellationToken)
+ {
+ try
+ {
+ this.ImageUrl = imageUrl;
+ this.Image = null;
+ this.ImageSize = null;
+ this.ReceivedSize = null;
+ this.LoadState = LoadStateEnum.BeforeLoad;
+
+ using (var response = await Networking.Http.GetAsync(
+ this.ImageUrl,
+ HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken))
+ {
+ response.EnsureSuccessStatusCode();
+
+ this.ImageSize = response.Content.Headers.ContentLength;
+ this.ReceivedSize = 0;
+ this.LoadState = LoadStateEnum.HeaderArrived;
+
+ var initialSize = (int?)this.ImageSize ?? 2 * 1024 * 1024;
+ using var memstream = new MemoryStream(initialSize);
+
+ using (var responseStream = await response.Content.ReadAsStreamAsync())
+ {
+ var bufferSize = 1 * 1024 * 1024;
+ var buffer = new byte[bufferSize];
+
+ int received;
+ while ((received = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) != 0)
+ {
+ memstream.Write(buffer, 0, received);
+
+ this.ReceivedSize += received;
+ }
+ }
+
+ memstream.Position = 0;
+ this.Image = MemoryImage.CopyFromStream(memstream);
+ }
+
+ this.LoadState = LoadStateEnum.LoadSuccessed;
+ }
+ catch (Exception)
+ {
+ this.LoadState = LoadStateEnum.LoadError;
+
+ try
+ {
+ throw;
+ }
+ catch (HttpRequestException)
+ {
+ }
+ catch (InvalidImageException)
+ {
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ internal async Task LoadAsync(Func> imageTaskFunc)
+ {
+ try
+ {
+ this.ImageUrl = null;
+ this.Image = null;
+ this.ImageSize = null;
+ this.ReceivedSize = null;
+ this.LoadState = LoadStateEnum.BeforeLoad;
+
+ var image = await imageTaskFunc();
+
+ this.Image = image;
+ this.LoadState = LoadStateEnum.LoadSuccessed;
+ }
+ catch (Exception)
+ {
+ this.LoadState = LoadStateEnum.LoadError;
+
+ try
+ {
+ throw;
+ }
+ catch (HttpRequestException)
+ {
+ }
+ catch (InvalidImageException)
+ {
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ public void AbortLoad()
+ {
+ var oldCts = Interlocked.Exchange(ref this.cts, null);
+ if (oldCts != null)
+ {
+ oldCts.Cancel();
+ oldCts.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ this.cts?.Dispose();
+ this.Image?.Dispose();
+ }
+ }
+}
diff --git a/OpenTween/Models/MediaViewerWebBrowser.cs b/OpenTween/Models/MediaViewerWebBrowser.cs
new file mode 100644
index 000000000..7b59349ca
--- /dev/null
+++ b/OpenTween/Models/MediaViewerWebBrowser.cs
@@ -0,0 +1,165 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon)
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see , or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.Net;
+using OpenTween.Thumbnail;
+
+namespace OpenTween.Models
+{
+ public class MediaViewerWebBrowser : NotifyPropertyChangedBase
+ {
+ private ThumbnailInfo[] mediaItems = Array.Empty();
+ private int displayMediaIndex;
+ private string displayHTML = "";
+ private ColorRGB backColor = new ColorRGB(0, 0, 0);
+
+ public ThumbnailInfo[] MediaItems
+ {
+ get => this.mediaItems;
+ private set => this.SetProperty(ref this.mediaItems, value);
+ }
+
+ public int DisplayMediaIndex
+ {
+ get => this.displayMediaIndex;
+ private set => this.SetProperty(ref this.displayMediaIndex, value);
+ }
+
+ public ThumbnailInfo DisplayMedia
+ => this.MediaItems[this.DisplayMediaIndex];
+
+ public string DisplayHTML
+ {
+ get => this.displayHTML;
+ private set => this.SetProperty(ref this.displayHTML, value);
+ }
+
+ public ColorRGB BackColor
+ {
+ get => this.backColor;
+ private set => this.SetProperty(ref this.backColor, value);
+ }
+
+ public void SetMediaItems(ThumbnailInfo[] thumbnails)
+ {
+ this.DisplayMediaIndex = 0;
+ this.MediaItems = thumbnails;
+ }
+
+ public void SelectMedia(int displayIndex)
+ {
+ this.DisplayMediaIndex = displayIndex;
+ this.DisplayHTML = this.CreateDocument();
+ }
+
+ public void SelectPreviousMedia()
+ {
+ var currentIndex = this.DisplayMediaIndex;
+ if (currentIndex == 0)
+ return;
+
+ this.SelectMedia(currentIndex - 1);
+ }
+
+ public void SelectNextMedia()
+ {
+ var currentIndex = this.DisplayMediaIndex;
+ if (currentIndex == this.MediaItems.Length - 1)
+ return;
+
+ this.SelectMedia(currentIndex + 1);
+ }
+
+ public void SetBackColor(ColorRGB color)
+ {
+ this.BackColor = color;
+ this.DisplayHTML = this.CreateDocument();
+ }
+
+ private string CreateDocument()
+ {
+ const string TEMPLATE_HEAD = @"
+
+
+
+MediaViewerWebBrowserDialog
+
+";
+ var bgColor = this.BackColor;
+ var html = TEMPLATE_HEAD
+ .Replace("###BG_COLOR###", $"{bgColor.R},{bgColor.G},{bgColor.B}");
+
+ var media = this.DisplayMedia;
+
+ if (media.VideoUrl != null)
+ {
+ const string TEMPLATE_VIDEO_BODY = @"
+
+
+
+";
+ html += TEMPLATE_VIDEO_BODY
+ .Replace("###VIDEO_URI###", WebUtility.HtmlEncode(media.VideoUrl))
+ .Replace("###MEDIA_TOOLTIP###", WebUtility.HtmlEncode(media.TooltipText));
+ }
+ else
+ {
+ const string TEMPLATE_IMAGE_BODY = @"
+
+";
+ html += TEMPLATE_IMAGE_BODY
+ .Replace("###IMAGE_URI###", WebUtility.HtmlEncode(Uri.EscapeUriString(media.FullSizeImageUrl ?? media.ThumbnailImageUrl ?? "")))
+ .Replace("###MEDIA_TOOLTIP###", WebUtility.HtmlEncode(media.TooltipText));
+ }
+
+ return html;
+ }
+ }
+}
diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj
index 16fd1c0d9..9140c3e59 100644
--- a/OpenTween/OpenTween.csproj
+++ b/OpenTween/OpenTween.csproj
@@ -149,6 +149,19 @@
FilterDialog.cs
+
+
+ Form
+
+
+ MediaViewerLightDialog.cs
+
+
+ Form
+
+
+ MediaViewerWebBrowserDialog.cs
+
Form
@@ -157,16 +170,19 @@
LoginDialog.cs
+
+
+
@@ -488,6 +504,13 @@
HashtagManage.cs
+
+ MediaViewerLightDialog.cs
+ Designer
+
+
+ MediaViewerWebBrowserDialog.cs
+
InputDialog.cs
diff --git a/OpenTween/Setting/SettingLocal.cs b/OpenTween/Setting/SettingLocal.cs
index 872fbbaa3..811c05f1b 100644
--- a/OpenTween/Setting/SettingLocal.cs
+++ b/OpenTween/Setting/SettingLocal.cs
@@ -340,6 +340,11 @@ public string EncryptProxyPassword
///
public bool UseTwemoji = true;
+ ///
+ /// ツイートに添付された画像の詳細表示方法
+ ///
+ public MediaHandlerType MediaHanderType { get; set; } = MediaHandlerType.LightViewer;
+
[XmlIgnore]
private readonly FontConverter fontConverter = new FontConverter();
diff --git a/OpenTween/Thumbnail/Services/TwitterComVideo.cs b/OpenTween/Thumbnail/Services/TwitterComVideo.cs
index 881bd5d80..72984a399 100644
--- a/OpenTween/Thumbnail/Services/TwitterComVideo.cs
+++ b/OpenTween/Thumbnail/Services/TwitterComVideo.cs
@@ -58,8 +58,9 @@ public TwitterComVideo(HttpClient? http)
{
return new ThumbnailInfo
{
- MediaPageUrl = mediaInfo.VideoUrl,
+ MediaPageUrl = MyCommon.GetStatusUrl(post.ScreenName, post.StatusId),
ThumbnailImageUrl = url,
+ VideoUrl = mediaInfo.VideoUrl,
TooltipText = mediaInfo.AltText,
IsPlayable = true,
};
diff --git a/OpenTween/Thumbnail/ThumbnailInfo.cs b/OpenTween/Thumbnail/ThumbnailInfo.cs
index 4dfe2010f..1d4c747b9 100644
--- a/OpenTween/Thumbnail/ThumbnailInfo.cs
+++ b/OpenTween/Thumbnail/ThumbnailInfo.cs
@@ -54,6 +54,9 @@ public class ThumbnailInfo : IEquatable
///
public string? FullSizeImageUrl { get; set; }
+ /// 動画 URL (video/mp4 形式のみ)
+ public string? VideoUrl { get; set; }
+
/// ツールチップとして表示するテキスト
///
/// サムネイル画像にマウスオーバーした際に表示されるテキスト
diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs
index 80d6d5289..d38fc6ff1 100644
--- a/OpenTween/Tween.cs
+++ b/OpenTween/Tween.cs
@@ -6209,7 +6209,7 @@ private void InitializeShortcuts()
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
.FocusedOn(FocusedControl.ListTab)
.OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
- .Do(() => this.OpenThumbnailPicture(this.tweetThumbnail1.Thumbnail)),
+ .Do(() => this.OpenMediaViewer()),
};
}
@@ -11210,16 +11210,23 @@ 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);
+ => await this.OpenMediaViewer();
private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e)
=> await MyCommon.OpenInBrowserAsync(this, e.ImageUrl);
- private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail)
+ private Task OpenMediaViewer()
+ => this.OpenMediaViewer(this.tweetThumbnail1.Thumbnails, this.tweetThumbnail1.DisplayThumbnailIndex);
+
+ private async Task OpenMediaViewer(ThumbnailInfo[] thumbnails, int displayIndex)
{
- var url = thumbnail.FullSizeImageUrl ?? thumbnail.MediaPageUrl;
+ var handler = new MediaHandler
+ {
+ MediaHandlerType = SettingManager.Local.MediaHanderType,
+ OpenInBrowser = (owner, url) => MyCommon.OpenInBrowserAsync(owner, url),
+ };
- await MyCommon.OpenInBrowserAsync(this, url);
+ await handler.OpenMediaViewer(this, thumbnails, displayIndex);
}
private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
diff --git a/OpenTween/TweetThumbnail.cs b/OpenTween/TweetThumbnail.cs
index 6d6b00c1a..7db307906 100644
--- a/OpenTween/TweetThumbnail.cs
+++ b/OpenTween/TweetThumbnail.cs
@@ -52,8 +52,14 @@ public partial class TweetThumbnail : UserControl
public event EventHandler? ThumbnailImageSearchClick;
+ public int DisplayThumbnailIndex
+ => this.scrollBar.Value;
+
+ public ThumbnailInfo[] Thumbnails
+ => this.PictureBox.Select(x => x.Tag).Cast().ToArray();
+
public ThumbnailInfo Thumbnail
- => (ThumbnailInfo)this.PictureBox[this.scrollBar.Value].Tag;
+ => (ThumbnailInfo)this.PictureBox[this.DisplayThumbnailIndex].Tag;
public TweetThumbnail()
=> this.InitializeComponent();
diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs
index 6638b1e35..174120acf 100644
--- a/OpenTween/Twitter.cs
+++ b/OpenTween/Twitter.cs
@@ -1534,10 +1534,14 @@ private void ExtractEntities(TwitterEntities? entities, List<(long UserId, strin
{
if (!media.Any(x => x.Url == ent.MediaUrlHttps))
{
- if (ent.VideoInfo != null &&
- ent.Type == "animated_gif" || ent.Type == "video")
+ if (ent.VideoInfo != null && (ent.Type == "animated_gif" || ent.Type == "video"))
{
- media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
+ var videoUrl = ent.VideoInfo.Variants
+ .Where(v => v.ContentType == "video/mp4")
+ .OrderByDescending(v => v.Bitrate)
+ .Select(v => v.Url).FirstOrDefault();
+
+ media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl));
}
else
{