OSDN Git Service

TwitterEntityUrl.DisplayUrl, ExpandedUrl をNullableに変更
[opentween/open-tween.git] / OpenTween / MediaSelector.cs
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.
5 //
6 // This file is part of OpenTween.
7 //
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)
11 // any later version.
12 //
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
16 // for more details.
17 //
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.
22
23 #nullable enable
24
25 using System;
26 using System.Collections.Generic;
27 using System.ComponentModel;
28 using System.Drawing;
29 using System.IO;
30 using System.Linq;
31 using System.Text;
32 using System.Threading.Tasks;
33 using OpenTween.Api.DataModel;
34 using OpenTween.MediaUploadServices;
35
36 namespace OpenTween
37 {
38     public sealed class MediaSelector : NotifyPropertyChangedBase, IDisposable
39     {
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;
45
46         public bool IsDisposed { get; private set; } = false;
47
48         public KeyValuePair<string, IMediaUploadService>[] MediaServices
49         {
50             get => this.pictureServices;
51             private set => this.SetProperty(ref this.pictureServices, value);
52         }
53
54         public BindingList<IMediaItem> MediaItems
55             => this.mediaItems;
56
57         public MemoryImageList ThumbnailList { get; } = new();
58
59         /// <summary>
60         /// 選択されている投稿先名を取得する。
61         /// </summary>
62         public string SelectedMediaServiceName
63         {
64             get => this.selectedMediaServiceName;
65             set => this.SetProperty(ref this.selectedMediaServiceName, value);
66         }
67
68         /// <summary>
69         /// 選択されている投稿先を示すインデックスを取得する。
70         /// </summary>
71         public int SelectedMediaServiceIndex
72             => this.MediaServices.FindIndex(x => x.Key == this.SelectedMediaServiceName);
73
74         /// <summary>
75         /// 選択されている投稿先の IMediaUploadService を取得する。
76         /// </summary>
77         public IMediaUploadService? SelectedMediaService
78             => this.GetService(this.SelectedMediaServiceName);
79
80         public bool CanUseAltText
81             => this.SelectedMediaService?.CanUseAltText ?? false;
82
83         public Guid? SelectedMediaItemId
84         {
85             get => this.selectedMediaItemId;
86             set
87             {
88                 if (this.selectedMediaItemId == value)
89                     return;
90
91                 this.SetProperty(ref this.selectedMediaItemId, value);
92                 this.LoadSelectedMediaItemImage();
93             }
94         }
95
96         public IMediaItem? SelectedMediaItem
97             => this.SelectedMediaItemId != null ? this.MediaItems.First(x => x.Id == this.SelectedMediaItemId) : null;
98
99         public int SelectedMediaItemIndex
100         {
101             get => this.MediaItems.FindIndex(x => x.Id == this.SelectedMediaItemId);
102             set => this.SelectedMediaItemId = value != -1 ? this.MediaItems[value].Id : null;
103         }
104
105         public MemoryImage? SelectedMediaItemImage
106         {
107             get => this.selectedMediaItemImage;
108             set => this.SetProperty(ref this.selectedMediaItemImage, value);
109         }
110
111         /// <summary>
112         /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
113         /// </summary>
114         public IMediaUploadService? GetService(string serviceName)
115         {
116             var index = this.MediaServices.FindIndex(x => x.Key == serviceName);
117             return index != -1 ? this.MediaServices[index].Value : null;
118         }
119
120         public void InitializeServices(Twitter tw, TwitterConfiguration twitterConfig)
121         {
122             this.MediaServices = new KeyValuePair<string, IMediaUploadService>[]
123             {
124                 new("Twitter", new TwitterPhoto(tw, twitterConfig)),
125                 new("Imgur", new Imgur(twitterConfig)),
126                 new("Mobypicture", new Mobypicture(tw, twitterConfig)),
127             };
128         }
129
130         public void SelectMediaService(string serviceName, int? index = null)
131         {
132             int idx;
133             if (MyCommon.IsNullOrEmpty(serviceName))
134             {
135                 // 引数の index は serviceName が空の場合のみ使用する
136                 idx = index ?? 0;
137             }
138             else
139             {
140                 idx = this.MediaServices.FindIndex(x => x.Key == serviceName);
141
142                 // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
143                 // (廃止されたサービスを選択していた場合の対応)
144                 if (idx == -1)
145                     idx = 0;
146             }
147
148             this.SelectedMediaServiceName = this.MediaServices[idx].Key;
149         }
150
151         /// <summary>
152         /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
153         /// </summary>
154         public bool HasUploadableService(string fileName, bool ignoreSize)
155         {
156             var fl = new FileInfo(fileName);
157             var ext = fl.Extension;
158             var size = ignoreSize ? (long?)null : fl.Length;
159
160             return this.GetAvailableServiceNames(ext, size).Any();
161         }
162
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)))
166                 .Select(x => x.Key)
167                 .ToArray();
168
169         public void AddMediaItemFromImage(Image image)
170         {
171             var mediaItem = this.CreateMemoryImageMediaItem(image);
172             if (mediaItem == null)
173                 return;
174
175             this.AddMediaItem(mediaItem);
176             this.SelectedMediaItemId = mediaItem.Id;
177         }
178
179         public void AddMediaItemFromFilePath(string[] filePathArray)
180         {
181             if (filePathArray.Length == 0)
182                 return;
183
184             var mediaItems = new IMediaItem[filePathArray.Length];
185
186             // 連番のファイル名を一括でアップロードする場合の利便性のためソートする
187             var sortedFilePath = filePathArray.OrderBy(x => x);
188
189             foreach (var (path, index) in sortedFilePath.WithIndex())
190             {
191                 var mediaItem = this.CreateFileMediaItem(path);
192                 if (mediaItem == null)
193                     continue;
194
195                 mediaItems[index] = mediaItem;
196             }
197
198             // 全ての IMediaItem の生成に成功した場合のみ追加する
199             foreach (var mediaItem in mediaItems)
200                 this.AddMediaItem(mediaItem);
201
202             this.SelectedMediaItemId = mediaItems.Last().Id;
203         }
204
205         public void AddMediaItem(IMediaItem item)
206         {
207             var id = item.Id.ToString();
208             var thumbnailImage = this.GenerateThumbnailImage(item);
209             this.ThumbnailList.Add(id, thumbnailImage);
210             this.MediaItems.Add(item);
211         }
212
213         private MemoryImage GenerateThumbnailImage(IMediaItem item)
214         {
215             using var origImage = this.CreateMediaItemImage(item);
216             var origSize = origImage.Image.Size;
217             var thumbSize = this.ThumbnailList.ImageList.ImageSize;
218
219             using var bitmap = new Bitmap(thumbSize.Width, thumbSize.Height);
220
221             // 縦横比を維持したまま thumbSize に収まるサイズに縮小する
222             using (var g = Graphics.FromImage(bitmap))
223             {
224                 var scale = Math.Min(
225                     (float)thumbSize.Width / origSize.Width,
226                     (float)thumbSize.Height / origSize.Height
227                 );
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
232                 );
233                 g.DrawImage(origImage.Image, new RectangleF(pos, fitSize));
234             }
235
236             return MemoryImage.CopyFromImage(bitmap);
237         }
238
239         public void ClearMediaItems()
240         {
241             this.SelectedMediaItemId = null;
242
243             var mediaItems = this.MediaItems.ToList();
244             this.MediaItems.Clear();
245
246             foreach (var mediaItem in mediaItems)
247                 mediaItem.Dispose();
248
249             var thumbnailImages = this.ThumbnailList.ToList();
250             this.ThumbnailList.Clear();
251
252             foreach (var image in thumbnailImages)
253                 image.Dispose();
254         }
255
256         public IMediaItem[] DetachMediaItems()
257         {
258             // ClearMediaItems では MediaItem が破棄されるため、外部で使用する場合はこのメソッドを使用して MediaItems から切り離す
259             var mediaItems = this.MediaItems.ToArray();
260             this.MediaItems.Clear();
261             this.ClearMediaItems();
262
263             return mediaItems;
264         }
265
266         private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image)
267         {
268             if (image == null)
269                 return null;
270
271             MemoryImage? memoryImage = null;
272             try
273             {
274                 // image から png 形式の MemoryImage を生成
275                 memoryImage = MemoryImage.CopyFromImage(image);
276
277                 return new MemoryImageMediaItem(memoryImage);
278             }
279             catch
280             {
281                 memoryImage?.Dispose();
282                 return null;
283             }
284         }
285
286         private FileMediaItem? CreateFileMediaItem(string path)
287         {
288             if (MyCommon.IsNullOrEmpty(path))
289                 return null;
290
291             try
292             {
293                 return new FileMediaItem(path);
294             }
295             catch
296             {
297                 return null;
298             }
299         }
300
301         private void LoadSelectedMediaItemImage()
302         {
303             var previousImage = this.selectedMediaItemImage;
304
305             if (this.SelectedMediaItem == null)
306             {
307                 this.SelectedMediaItemImage = null;
308                 previousImage?.Dispose();
309                 return;
310             }
311
312             this.SelectedMediaItemImage = this.CreateMediaItemImage(this.SelectedMediaItem);
313             previousImage?.Dispose();
314         }
315
316         private MemoryImage CreateMediaItemImage(IMediaItem media)
317         {
318             try
319             {
320                 return media.CreateImage();
321             }
322             catch (InvalidImageException)
323             {
324                 return MemoryImage.CopyFromImage(Properties.Resources.MultiMediaImage);
325             }
326         }
327
328         public void SetSelectedMediaAltText(string altText)
329         {
330             var selectedMedia = this.SelectedMediaItem;
331             if (selectedMedia == null)
332                 return;
333
334             selectedMedia.AltText = altText.Trim();
335         }
336
337         public MediaSelectorErrorType Validate(out IMediaItem? rejectedMedia)
338         {
339             rejectedMedia = null;
340
341             if (this.MediaItems.Count == 0)
342                 return MediaSelectorErrorType.MediaItemNotSet;
343
344             var uploadService = this.SelectedMediaService;
345             if (uploadService == null)
346                 return MediaSelectorErrorType.ServiceNotSelected;
347
348             foreach (var mediaItem in this.MediaItems)
349             {
350                 var error = this.ValidateMediaItem(uploadService, mediaItem);
351                 if (error != MediaSelectorErrorType.None)
352                 {
353                     rejectedMedia = mediaItem;
354                     return error;
355                 }
356             }
357
358             return MediaSelectorErrorType.None;
359         }
360
361         private MediaSelectorErrorType ValidateMediaItem(IMediaUploadService imageService, IMediaItem item)
362         {
363             var ext = item.Extension;
364             var size = item.Size;
365
366             if (!imageService.CheckFileExtension(ext))
367                 return MediaSelectorErrorType.UnsupportedFileExtension;
368
369             if (!imageService.CheckFileSize(ext, size))
370                 return MediaSelectorErrorType.FileSizeExceeded;
371
372             return MediaSelectorErrorType.None;
373         }
374
375         public void MoveSelectedMediaItemToPrevious()
376         {
377             var index = this.SelectedMediaItemIndex;
378             if (index == -1 || index == 0)
379                 return;
380
381             var mediaItem = this.MediaItems[index - 1];
382             this.MediaItems.RemoveAt(index - 1);
383             this.MediaItems.Insert(index, mediaItem);
384         }
385
386         public void MoveSelectedMediaItemToNext()
387         {
388             var index = this.SelectedMediaItemIndex;
389             if (index == -1 || index == (this.MediaItems.Count - 1))
390                 return;
391
392             var mediaItem = this.MediaItems[index + 1];
393             this.MediaItems.RemoveAt(index + 1);
394             this.MediaItems.Insert(index, mediaItem);
395         }
396
397         public void RemoveSelectedMediaItem()
398         {
399             var index = this.SelectedMediaItemIndex;
400             if (index == -1)
401                 return;
402
403             this.MediaItems.RemoveAt(index);
404         }
405
406         public void Dispose()
407         {
408             if (this.IsDisposed)
409                 return;
410
411             this.IsDisposed = true;
412             this.ThumbnailList.Dispose();
413         }
414     }
415
416     public enum MediaSelectorErrorType
417     {
418         None,
419         MediaItemNotSet,
420         ServiceNotSelected,
421         UnsupportedFileExtension,
422         FileSizeExceeded,
423     }
424 }