1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 spx (@5px)
3 // Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
4 // All rights reserved.
6 // This file is part of OpenTween.
8 // This program is free software; you can redistribute it and/or modify it
9 // under the terms of the GNU General Public License as published by the Free
10 // Software Foundation; either version 3 of the License, or (at your option)
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
18 // You should have received a copy of the GNU General Public License along
19 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
20 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
21 // Boston, MA 02110-1301, USA.
26 using System.Collections.Generic;
27 using System.ComponentModel;
32 using System.Threading.Tasks;
33 using OpenTween.Api.DataModel;
34 using OpenTween.MediaUploadServices;
38 public sealed class MediaSelector : NotifyPropertyChangedBase, IDisposable
40 private KeyValuePair<string, IMediaUploadService>[] pictureServices = Array.Empty<KeyValuePair<string, IMediaUploadService>>();
41 private readonly BindingList<IMediaItem> mediaItems = new();
42 private string selectedMediaServiceName = "";
43 private Guid? selectedMediaItemId = null;
44 private MemoryImage? selectedMediaItemImage = null;
46 public bool IsDisposed { get; private set; } = false;
48 public KeyValuePair<string, IMediaUploadService>[] MediaServices
50 get => this.pictureServices;
51 private set => this.SetProperty(ref this.pictureServices, value);
54 public BindingList<IMediaItem> MediaItems
57 public MemoryImageList ThumbnailList { get; } = new();
62 public string SelectedMediaServiceName
64 get => this.selectedMediaServiceName;
65 set => this.SetProperty(ref this.selectedMediaServiceName, value);
69 /// 選択されている投稿先を示すインデックスを取得する。
71 public int SelectedMediaServiceIndex
72 => this.MediaServices.FindIndex(x => x.Key == this.SelectedMediaServiceName);
75 /// 選択されている投稿先の IMediaUploadService を取得する。
77 public IMediaUploadService? SelectedMediaService
78 => this.GetService(this.SelectedMediaServiceName);
80 public bool CanUseAltText
81 => this.SelectedMediaService?.CanUseAltText ?? false;
83 public Guid? SelectedMediaItemId
85 get => this.selectedMediaItemId;
88 if (this.selectedMediaItemId == value)
91 this.SetProperty(ref this.selectedMediaItemId, value);
92 this.LoadSelectedMediaItemImage();
96 public IMediaItem? SelectedMediaItem
97 => this.SelectedMediaItemId != null ? this.MediaItems.First(x => x.Id == this.SelectedMediaItemId) : null;
99 public int SelectedMediaItemIndex
101 get => this.MediaItems.FindIndex(x => x.Id == this.SelectedMediaItemId);
102 set => this.SelectedMediaItemId = value != -1 ? this.MediaItems[value].Id : null;
105 public MemoryImage? SelectedMediaItemImage
107 get => this.selectedMediaItemImage;
108 set => this.SetProperty(ref this.selectedMediaItemImage, value);
112 /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
114 public IMediaUploadService? GetService(string serviceName)
116 var index = this.MediaServices.FindIndex(x => x.Key == serviceName);
117 return index != -1 ? this.MediaServices[index].Value : null;
120 public void InitializeServices(Twitter tw, TwitterConfiguration twitterConfig)
122 this.MediaServices = new KeyValuePair<string, IMediaUploadService>[]
124 new("Twitter", new TwitterPhoto(tw, twitterConfig)),
125 new("Imgur", new Imgur(twitterConfig)),
126 new("Mobypicture", new Mobypicture(tw, twitterConfig)),
130 public void SelectMediaService(string serviceName, int? index = null)
133 if (MyCommon.IsNullOrEmpty(serviceName))
135 // 引数の index は serviceName が空の場合のみ使用する
140 idx = this.MediaServices.FindIndex(x => x.Key == serviceName);
142 // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
143 // (廃止されたサービスを選択していた場合の対応)
148 this.SelectedMediaServiceName = this.MediaServices[idx].Key;
152 /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
154 public bool HasUploadableService(string fileName, bool ignoreSize)
156 var fl = new FileInfo(fileName);
157 var ext = fl.Extension;
158 var size = ignoreSize ? (long?)null : fl.Length;
160 return this.GetAvailableServiceNames(ext, size).Any();
163 public string[] GetAvailableServiceNames(string extension, long? fileSize)
164 => this.MediaServices
165 .Where(x => x.Value.CheckFileExtension(extension) && (fileSize == null || x.Value.CheckFileSize(extension, fileSize.Value)))
169 public void AddMediaItemFromImage(Image image)
171 var mediaItem = this.CreateMemoryImageMediaItem(image);
172 if (mediaItem == null)
175 this.AddMediaItem(mediaItem);
176 this.SelectedMediaItemId = mediaItem.Id;
179 public void AddMediaItemFromFilePath(string[] filePathArray)
181 if (filePathArray.Length == 0)
184 var mediaItems = new IMediaItem[filePathArray.Length];
186 // 連番のファイル名を一括でアップロードする場合の利便性のためソートする
187 var sortedFilePath = filePathArray.OrderBy(x => x);
189 foreach (var (path, index) in sortedFilePath.WithIndex())
191 var mediaItem = this.CreateFileMediaItem(path);
192 if (mediaItem == null)
195 mediaItems[index] = mediaItem;
198 // 全ての IMediaItem の生成に成功した場合のみ追加する
199 foreach (var mediaItem in mediaItems)
200 this.AddMediaItem(mediaItem);
202 this.SelectedMediaItemId = mediaItems.Last().Id;
205 public void AddMediaItem(IMediaItem item)
207 var id = item.Id.ToString();
208 var thumbnailImage = this.GenerateThumbnailImage(item);
209 this.ThumbnailList.Add(id, thumbnailImage);
210 this.MediaItems.Add(item);
213 private MemoryImage GenerateThumbnailImage(IMediaItem item)
215 using var origImage = this.CreateMediaItemImage(item);
216 var origSize = origImage.Image.Size;
217 var thumbSize = this.ThumbnailList.ImageList.ImageSize;
219 using var bitmap = new Bitmap(thumbSize.Width, thumbSize.Height);
221 // 縦横比を維持したまま thumbSize に収まるサイズに縮小する
222 using (var g = Graphics.FromImage(bitmap))
224 var scale = Math.Min(
225 (float)thumbSize.Width / origSize.Width,
226 (float)thumbSize.Height / origSize.Height
228 var fitSize = new SizeF(origSize.Width * scale, origSize.Height * scale);
229 var pos = new PointF(
230 x: (thumbSize.Width - fitSize.Width) / 2.0f,
231 y: (thumbSize.Height - fitSize.Height) / 2.0f
233 g.DrawImage(origImage.Image, new RectangleF(pos, fitSize));
236 return MemoryImage.CopyFromImage(bitmap);
239 public void ClearMediaItems()
241 this.SelectedMediaItemId = null;
243 var mediaItems = this.MediaItems.ToList();
244 this.MediaItems.Clear();
246 foreach (var mediaItem in mediaItems)
249 var thumbnailImages = this.ThumbnailList.ToList();
250 this.ThumbnailList.Clear();
252 foreach (var image in thumbnailImages)
256 public IMediaItem[] DetachMediaItems()
258 // ClearMediaItems では MediaItem が破棄されるため、外部で使用する場合はこのメソッドを使用して MediaItems から切り離す
259 var mediaItems = this.MediaItems.ToArray();
260 this.MediaItems.Clear();
261 this.ClearMediaItems();
266 private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image)
271 MemoryImage? memoryImage = null;
274 // image から png 形式の MemoryImage を生成
275 memoryImage = MemoryImage.CopyFromImage(image);
277 return new MemoryImageMediaItem(memoryImage);
281 memoryImage?.Dispose();
286 private FileMediaItem? CreateFileMediaItem(string path)
288 if (MyCommon.IsNullOrEmpty(path))
293 return new FileMediaItem(path);
301 private void LoadSelectedMediaItemImage()
303 var previousImage = this.selectedMediaItemImage;
305 if (this.SelectedMediaItem == null)
307 this.SelectedMediaItemImage = null;
308 previousImage?.Dispose();
312 this.SelectedMediaItemImage = this.CreateMediaItemImage(this.SelectedMediaItem);
313 previousImage?.Dispose();
316 private MemoryImage CreateMediaItemImage(IMediaItem media)
320 return media.CreateImage();
322 catch (InvalidImageException)
324 return MemoryImage.CopyFromImage(Properties.Resources.MultiMediaImage);
328 public void SetSelectedMediaAltText(string altText)
330 var selectedMedia = this.SelectedMediaItem;
331 if (selectedMedia == null)
334 selectedMedia.AltText = altText.Trim();
337 public MediaSelectorErrorType Validate(out IMediaItem? rejectedMedia)
339 rejectedMedia = null;
341 if (this.MediaItems.Count == 0)
342 return MediaSelectorErrorType.MediaItemNotSet;
344 var uploadService = this.SelectedMediaService;
345 if (uploadService == null)
346 return MediaSelectorErrorType.ServiceNotSelected;
348 foreach (var mediaItem in this.MediaItems)
350 var error = this.ValidateMediaItem(uploadService, mediaItem);
351 if (error != MediaSelectorErrorType.None)
353 rejectedMedia = mediaItem;
358 return MediaSelectorErrorType.None;
361 private MediaSelectorErrorType ValidateMediaItem(IMediaUploadService imageService, IMediaItem item)
363 var ext = item.Extension;
364 var size = item.Size;
366 if (!imageService.CheckFileExtension(ext))
367 return MediaSelectorErrorType.UnsupportedFileExtension;
369 if (!imageService.CheckFileSize(ext, size))
370 return MediaSelectorErrorType.FileSizeExceeded;
372 return MediaSelectorErrorType.None;
375 public void MoveSelectedMediaItemToPrevious()
377 var index = this.SelectedMediaItemIndex;
378 if (index == -1 || index == 0)
381 var mediaItem = this.MediaItems[index - 1];
382 this.MediaItems.RemoveAt(index - 1);
383 this.MediaItems.Insert(index, mediaItem);
386 public void MoveSelectedMediaItemToNext()
388 var index = this.SelectedMediaItemIndex;
389 if (index == -1 || index == (this.MediaItems.Count - 1))
392 var mediaItem = this.MediaItems[index + 1];
393 this.MediaItems.RemoveAt(index + 1);
394 this.MediaItems.Insert(index, mediaItem);
397 public void RemoveSelectedMediaItem()
399 var index = this.SelectedMediaItemIndex;
403 this.MediaItems.RemoveAt(index);
406 public void Dispose()
411 this.IsDisposed = true;
412 this.ThumbnailList.Dispose();
416 public enum MediaSelectorErrorType
421 UnsupportedFileExtension,