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 {