From 132ceddc1ce3eda2810b99a6a576012425b40c73 Mon Sep 17 00:00:00 2001 From: sentouki Date: Wed, 2 Dec 2020 22:50:57 +0100 Subject: [PATCH] - added TimeOut, ConcurrentTasks and DownloadAttempts public properties to ArtAPI - Added new event for login/authenticating process - outsourced the settings properties into a separate SettingsViewModel - refactoring --- ArtAPI/ArtStationAPI.cs | 8 +- ArtAPI/DeviantArtAPI.cs | 7 +- ArtAPI/General.cs | 55 +++++- ArtAPI/PixivAPI.cs | 13 +- ArtAPI/{IRequestArt.cs => RequestArt.cs} | 233 ++++++++++++----------- Artify/App.xaml | 35 +++- Artify/Models/ArtifyModel.cs | 13 +- Artify/Models/Settings.cs | 2 +- Artify/ViewModels/ArtifyViewModel.cs | 70 +++---- Artify/ViewModels/BaseViewModel.cs | 2 +- Artify/ViewModels/RelayCommand.cs | 2 +- Artify/ViewModels/SettingsViewModel.cs | 91 +++++++++ Artify/Views/MainView.xaml | 6 +- Artify/Views/MainView.xaml.cs | 14 +- Artify/Views/SettingsPopUp.xaml | 46 ++++- Artify/Views/SettingsPopUp.xaml.cs | 35 ++-- 16 files changed, 425 insertions(+), 207 deletions(-) rename ArtAPI/{IRequestArt.cs => RequestArt.cs} (62%) create mode 100644 Artify/ViewModels/SettingsViewModel.cs diff --git a/ArtAPI/ArtStationAPI.cs b/ArtAPI/ArtStationAPI.cs index deaeeaf..196ac39 100644 --- a/ArtAPI/ArtStationAPI.cs +++ b/ArtAPI/ArtStationAPI.cs @@ -10,6 +10,12 @@ public sealed class ArtStationAPI : RequestArt private const string ApiUrl = @"https://artstation.com/users/{0}/projects?page="; private const string AssetsUrl = @"https://www.artstation.com/projects/{0}.json"; + public ArtStationAPI() + { + IsLoggedIn = true; + LoginState = LoginStatus.LoggedIn; + } + public override Task CreateUrlFromName(string artistName) { return Task.FromResult(new Uri(string.Format($@"https://www.artstation.com/{artistName}"))); @@ -55,7 +61,7 @@ protected override async Task GetImagesMetadataAsync(string apiUrl) var t = Task.WhenAll(allPages.Select(project => Task.Run(async () => { await GetAssets(project["hash_id"].ToString(), project["title"].ToString()).ConfigureAwait(false); })).ToArray()); try { await t.ConfigureAwait(false); } catch (Exception e) - { OnDownloadStateChanged(new DownloadStateChangedEventArgs(State.DownloadCanceled, e.Message)); return; } + { OnDownloadStateChanged(new DownloadStateChangedEventArgs(State.DownloadCanceled, e.Message)); } } // get all the images from a project diff --git a/ArtAPI/DeviantArtAPI.cs b/ArtAPI/DeviantArtAPI.cs index a8f9811..b11763a 100644 --- a/ArtAPI/DeviantArtAPI.cs +++ b/ArtAPI/DeviantArtAPI.cs @@ -19,8 +19,6 @@ private const string private const int Offset = 20; - public bool IsLoggedIn { get; private set; } - private readonly Regex _scaledDownImgUrlPattern = new Regex(@"(w_[0-9]{3,4}),(h_[0-9]{3,4}),(q_[0-9]{2})", RegexOptions.Compiled); public override Task CreateUrlFromName(string artistName) @@ -104,8 +102,9 @@ private async Task GetOriginImage(string deviationID) } } - public override async Task auth(string refreshToken) + public override async Task Auth(string refreshToken) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Authenticating)); var data = new Dictionary() { {"grant_type", "client_credentials" }, @@ -124,8 +123,10 @@ public override async Task auth(string refreshToken) } catch (HttpRequestException) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Failed)); return false; } + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.LoggedIn)); return IsLoggedIn = true; } } diff --git a/ArtAPI/General.cs b/ArtAPI/General.cs index 26bc8a2..d731d40 100644 --- a/ArtAPI/General.cs +++ b/ArtAPI/General.cs @@ -1,24 +1,19 @@ using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Text; #pragma warning disable CA5351 namespace ArtAPI { - public enum State - { - DownloadPreparing, - DownloadRunning, - DownloadCompleted, - DownloadCanceled, - ExceptionRaised, - } - public static class General { + /// + /// for hashing strings, used for Pixiv API authentication + /// + /// hashed string public static string CreateMD5(string input) { - // Use input string to calculate MD5 hash using (MD5 md5 = MD5.Create()) { byte[] inputBytes = Encoding.ASCII.GetBytes(input); @@ -33,6 +28,36 @@ public static string CreateMD5(string input) return sb.ToString(); } } + /// + /// special characters which cannot be used for file and directory names + /// + private static readonly List SpecialChars = new List() { @"\", "/", ":", "*", "?", "\"", "<", ">", "|" }; + /// + /// remove all the nasty characters that can cause trouble + /// + /// normalized file name + public static string NormalizeFileName(string filename) + { + SpecialChars.ForEach(c => filename = filename.Replace(c, "")); + return filename; + } + + } + public enum State + { + DownloadPreparing, + DownloadRunning, + DownloadCompleted, + DownloadCanceled, + ExceptionRaised, + } + public enum LoginStatus + { + NotLoggedIn, + LoggingIn, + Authenticating, + LoggedIn, + Failed } #region defining EventArgs @@ -65,5 +90,15 @@ public DownloadProgressChangedEventArgs(int progress) CurrentProgress = progress; } } + + public class LoginStatusChangedEventArgs : EventArgs + { + public LoginStatus Status { get; } + + public LoginStatusChangedEventArgs(LoginStatus status) + { + Status = status; + } + } #endregion } diff --git a/ArtAPI/PixivAPI.cs b/ArtAPI/PixivAPI.cs index babf49a..1f059e7 100644 --- a/ArtAPI/PixivAPI.cs +++ b/ArtAPI/PixivAPI.cs @@ -19,7 +19,6 @@ private const string ArtistDetails = @"https://www.pixiv.net/touch/ajax/user/details?id={0}"; private string _artistName; - public bool IsLoggedIn { get; private set; } public string RefreshToken { get; private set; } @@ -202,9 +201,10 @@ private void GetImageURLsWithLogin(JObject responseJson) } } - public override async Task auth(string refreshToken) + public override async Task Auth(string refreshToken) { if (IsLoggedIn) return true; + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Authenticating)); var clientTime = DateTime.UtcNow.ToString("s") + "+00:00"; var data = new Dictionary() { @@ -228,6 +228,7 @@ public override async Task auth(string refreshToken) var jsonResponse = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); if (jsonResponse.ContainsKey("has_error")) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Failed)); return false; } var accessToken = jsonResponse["response"]["access_token"]?.ToString() ?? @@ -239,13 +240,16 @@ public override async Task auth(string refreshToken) } catch (HttpRequestException) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Failed)); return false; } + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.LoggedIn)); return IsLoggedIn = true; } - public override async Task login(string username, string password) + public override async Task Login(string username, string password) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.LoggingIn)); var clientTime = DateTime.UtcNow.ToString("s") + "+00:00"; var data = new Dictionary() { @@ -270,6 +274,7 @@ public override async Task login(string username, string password) var jsonResponse = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); if (jsonResponse.ContainsKey("has_error")) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Failed)); return null; } var accessToken = jsonResponse["response"]["access_token"]?.ToString() ?? @@ -281,9 +286,11 @@ public override async Task login(string username, string password) } catch (HttpRequestException) { + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.Failed)); return null; } IsLoggedIn = true; + OnLoginStatusChanged(new LoginStatusChangedEventArgs(LoginStatus.LoggedIn)); return RefreshToken; } } diff --git a/ArtAPI/IRequestArt.cs b/ArtAPI/RequestArt.cs similarity index 62% rename from ArtAPI/IRequestArt.cs rename to ArtAPI/RequestArt.cs index 6b3fb78..928c8e9 100644 --- a/ArtAPI/IRequestArt.cs +++ b/ArtAPI/RequestArt.cs @@ -9,86 +9,60 @@ namespace ArtAPI { - public interface IRequestArt + public abstract class RequestArt { - #region Events - /// - /// notify the GUI/CLI about states current state - /// - event EventHandler DownloadStateChanged; - /// - /// notify the GUI/CLI about download progress - /// - event EventHandler DownloadProgressChanged; - #endregion - string SavePath { get; set; } - /// - /// public method which will be used to download images - /// - /// artist profile url - Task GetImagesAsync(string artistUrl); - /// - /// public method which will be used to download images - /// - /// Uri object with artist profile url - Task GetImagesAsync(Uri artistUrl); - /// - /// creates API-URL from given username - /// - /// user name of an artist - /// URL with which an API request can be created - Task CreateUrlFromName(string artistName); - void CancelDownload(); - Task CheckArtistExistsAsync(string artistName); - Task auth(string refreshToken); - Task login(string username, string password); - } - - public abstract class RequestArt : IRequestArt - { - private const int - ClientTimeout = 20, - NumberOfDLAttempts = 10, - ConcurrentTasks = 15; - private string ArtistDirSavepath; + #region private fields + private int + _clientTimeout = 20, + _downloadAttempts = 10, + _concurrentTasks = 15; + private string _artistDirSavepath; private int _progress; - public State? CurrentState { get; protected set; } - protected HttpClient Client { get; } - protected HttpClientHandler handler { get; } private CancellationTokenSource cts; + #endregion + #region protected fields + protected HttpClient Client { get; } + protected HttpClientHandler Handler { get; } protected virtual string Header { get; } = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"; protected List ImagesToDownload { get; } = new List(); + #endregion - #region ctor & dtor - protected RequestArt() + #region public properties + + public int ClientTimeout { - handler = new HttpClientHandler() + get => _clientTimeout; + set { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - }; - Client = new HttpClient(handler); - Client.DefaultRequestHeaders.Add("User-Agent", Header); - Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate"); - Client.Timeout = new TimeSpan(0, 0, ClientTimeout); + _clientTimeout = value > 0 ? value : 1; + Client.Timeout = new TimeSpan(0, 0, _clientTimeout); + } } - ~RequestArt() + public int DownloadAttempts { - - Client?.Dispose(); - handler?.Dispose(); + get => _downloadAttempts; + set => _downloadAttempts = value > 0 ? value : 1; } - #endregion - #region properties + public int ConcurrentTasks + { + get => _concurrentTasks; + set => _concurrentTasks = value > 0 ? value : 1; + } + public State CurrentState { get; protected set; } + public LoginStatus LoginState { get; protected set; } /// /// path to where the images should be saved /// public string SavePath { get; set; } + + public bool IsLoggedIn { get; protected set; } /// /// safely increment the progress and notify about the change /// public virtual int Progress { get => _progress; + // ReSharper disable once ValueParameterNotUsed protected set { Interlocked.Increment(ref _progress); @@ -102,10 +76,31 @@ protected set public int FailedDownloads => TotalImageCount - Progress; + #endregion + + #region ctor & dtor + protected RequestArt() + { + Handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + Client = new HttpClient(Handler); + Client.DefaultRequestHeaders.Add("User-Agent", Header); + Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate"); + Client.Timeout = new TimeSpan(0, 0, _clientTimeout); + } + ~RequestArt() + { + + Client?.Dispose(); + Handler?.Dispose(); + } #endregion #region Event handler public event EventHandler DownloadStateChanged; public event EventHandler DownloadProgressChanged; + public event EventHandler LoginStatusChanged; protected void OnDownloadProgressChanged(DownloadProgressChangedEventArgs e) { DownloadProgressChanged?.Invoke(this, e); @@ -118,6 +113,11 @@ protected void OnDownloadStateChanged(DownloadStateChangedEventArgs e) CurrentState = e.state; } } + protected void OnLoginStatusChanged(LoginStatusChangedEventArgs e) + { + LoginStatusChanged?.Invoke(this, e); + LoginState = e.Status; + } #endregion /// @@ -135,20 +135,15 @@ protected async Task DownloadImagesAsync() CheckLocalImages(); OnDownloadStateChanged(new DownloadStateChangedEventArgs(State.DownloadRunning, TotalImageCount: TotalImageCount)); cts = new CancellationTokenSource(); - using var ss = new SemaphoreSlim(ConcurrentTasks); - var tasks = new List(); - foreach (var image in ImagesToDownload) + var ss = new SemaphoreSlim(_concurrentTasks); + var t = Task.WhenAll(ImagesToDownload.Select(image => Task.Run(async () => { + // ReSharper disable once AccessToDisposedClosure await ss.WaitAsync(); - tasks.Add( - Task.Run(async () => - { - await DownloadAsync(image, ArtistDirSavepath).ConfigureAwait(false); - ss.Release(); - }) - ); - } - var t = Task.WhenAll(tasks.ToArray()); + await DownloadAsync(image, _artistDirSavepath).ConfigureAwait(false); + // ReSharper disable once AccessToDisposedClosure + ss.Release(); + })).ToArray()); try { await t.ConfigureAwait(false); @@ -163,6 +158,7 @@ protected async Task DownloadImagesAsync() OnDownloadStateChanged(!cts.IsCancellationRequested ? new DownloadStateChangedEventArgs(State.DownloadCompleted, FailedDownloads: FailedDownloads) : new DownloadStateChangedEventArgs(State.DownloadCanceled, FailedDownloads: FailedDownloads)); + ss.Dispose(); Clear(); } } @@ -174,41 +170,45 @@ protected async Task DownloadImagesAsync() protected async Task DownloadAsync(ImageModel image, string savePath) { if (image == null) return; - var imageName = NormalizeFileName(image.Name); + var imageName = General.NormalizeFileName(image.Name); var imageSavePath = Path.Combine(savePath, $"{imageName}_{image.ID}.{image.FileType}"); - const int tries = NumberOfDLAttempts; try { - for (var i = tries; i > 0; i--) - { // if download fails, try to download again - try + await TryDownloadAsync(image.Url, imageSavePath); + } + catch (Exception e) + { // notify about the exception + OnDownloadStateChanged(new DownloadStateChangedEventArgs(State.ExceptionRaised, e.Message)); + } + } + + private async Task TryDownloadAsync(string imgUrl, string imageSavePath) + { + for (var i = _downloadAttempts; i > 0; i--) + { // if download fails, try to download again + try + { + using (var asyncResponse = await Client.GetAsync(imgUrl, cts.Token).ConfigureAwait(false)) { - using (var asyncResponse = await Client.GetAsync(image.Url, cts.Token).ConfigureAwait(false)) + if (asyncResponse.StatusCode == HttpStatusCode.Unauthorized) { - if (asyncResponse.StatusCode == HttpStatusCode.Unauthorized) - { - // TODO: fix this later; handle this error somehow, like try to auth again - i = 1; - throw new Exception("API token expired"); - } - await using var fstream = new FileStream(imageSavePath, FileMode.Create); - await (await asyncResponse.Content.ReadAsStreamAsync()).CopyToAsync(fstream) - .ConfigureAwait(false); + // TODO: fix this later; handle this error somehow, like try to auth again + i = 1; + throw new Exception("API token expired"); } - Progress++; - return; - } - catch (Exception) - { - if (i == 1 || cts.IsCancellationRequested) throw; - // if there's a some timeout or connection error, wait random amount of time before trying again - await Task.Delay(new Random().Next(500, 3000)).ConfigureAwait(false); + await using var fstream = new FileStream(imageSavePath, FileMode.Create); + await (await asyncResponse.Content.ReadAsStreamAsync()).CopyToAsync(fstream) + .ConfigureAwait(false); } + Progress++; + return; + } + catch (Exception) + { + if (i == 1 || cts.IsCancellationRequested) throw; + // if there's a some timeout or connection error, wait random amount of time before trying again + await Task.Delay(new Random().Next(500, 3000)).ConfigureAwait(false); } - } - catch (Exception e) - { // notify about the exception - OnDownloadStateChanged(new DownloadStateChangedEventArgs(State.ExceptionRaised, e.Message)); } } /// @@ -217,13 +217,12 @@ protected async Task DownloadAsync(ImageModel image, string savePath) /// name of the artist protected void CreateSaveDir(string artistName) { - ArtistDirSavepath = Path.Combine(SavePath, artistName); - Directory.CreateDirectory(ArtistDirSavepath); + _artistDirSavepath = Path.Combine(SavePath, artistName); + Directory.CreateDirectory(_artistDirSavepath); } private void Clear() { _progress = 0; - CurrentState = null; cts?.Dispose(); ImagesToDownload.Clear(); } @@ -232,18 +231,8 @@ private void Clear() /// private void CheckLocalImages() { - var localImages = Directory.GetFiles(ArtistDirSavepath).Select(Path.GetFileNameWithoutExtension).ToArray(); - ImagesToDownload.RemoveAll(image => localImages.Contains($"{NormalizeFileName(image.Name)}_{image.ID}")); - } - /// - /// remove all the nasty characters that can cause trouble - /// - /// normalized file name - private string NormalizeFileName(string filename) - { - var specialChars = new List() { @"\", "/", ":", "*", "?", "\"", "<", ">", "|" }; - specialChars.ForEach(c => filename = filename.Replace(c, "")); - return filename; + var localImages = Directory.GetFiles(_artistDirSavepath).Select(Path.GetFileNameWithoutExtension).ToArray(); + ImagesToDownload.RemoveAll(image => localImages.Contains($"{General.NormalizeFileName(image.Name)}_{image.ID}")); } public void CancelDownload() @@ -257,15 +246,29 @@ public void CancelDownload() catch (ObjectDisposedException) { } } + /// + /// public method which will be used to download images + /// + /// artist profile url public virtual async Task GetImagesAsync(string artistUrl) { await GetImagesAsync(new Uri(artistUrl)).ConfigureAwait(false); } + /// + /// public method which will be used to download images + /// + /// Uri object with artist profile url public abstract Task GetImagesAsync(Uri artistUrl); + + /// + /// creates API-URL from given username + /// + /// user name of an artist + /// URL with which an API request can be created public abstract Task CreateUrlFromName(string artistName); public abstract Task CheckArtistExistsAsync(string artistName); protected abstract Task GetImagesMetadataAsync(string apiUrl); - public virtual Task auth(string refreshToken) => Task.FromResult(true); - public virtual Task login(string username, string password) => Task.FromResult(""); + public virtual Task Auth(string refreshToken) => Task.FromResult(true); + public virtual Task Login(string username, string password) => Task.FromResult(""); } } diff --git a/Artify/App.xaml b/Artify/App.xaml index 8e345b0..094e68c 100644 --- a/Artify/App.xaml +++ b/Artify/App.xaml @@ -109,7 +109,7 @@ + + + + + + + + + + + + + + + + diff --git a/Artify/Models/ArtifyModel.cs b/Artify/Models/ArtifyModel.cs index 280d17a..0257569 100644 --- a/Artify/Models/ArtifyModel.cs +++ b/Artify/Models/ArtifyModel.cs @@ -6,7 +6,7 @@ using ArtAPI; using Newtonsoft.Json; -namespace Artify +namespace Artify.Models { public enum InputType { @@ -22,7 +22,7 @@ public enum InputType public class ArtifyModel { private readonly string SettingsDirPath, SettingsFilePath; - private IRequestArt _platform; + private RequestArt _platform; private readonly Dictionary URLpattern = new Dictionary() { {"general", new Regex(@"(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}(\/?[a-zA-Z0-9]*\/?)*") }, @@ -31,13 +31,13 @@ public class ArtifyModel {"deviantart", new Regex(@"(https://)?(www\.)?deviantart\.com/[0-9a-zA-Z]+/?")} }; // container for the classes - private readonly Dictionary> ArtPlatform = new Dictionary>() + private readonly Dictionary> ArtPlatform = new Dictionary>() { { "artstation", () => new ArtStationAPI() }, { "pixiv", () => new PixivAPI() }, { "deviantart", () => new DeviantArtAPI()} }; - public IRequestArt Platform + public RequestArt Platform { get => _platform; set @@ -52,6 +52,7 @@ public string SavePath get => settings.last_used_savepath ?? Environment.GetFolderPath((Environment.SpecialFolder.MyPictures)); // set the default dir for images set => Platform.SavePath = settings.last_used_savepath = value; } + private string _selectedPlatform; public Settings settings = new Settings(); public ArtifyModel() @@ -156,10 +157,10 @@ public void SelectPlatform(string platformName) public async Task Auth() { - if (_selectedPlatform != "pixiv") return await Platform.auth(null); + if (_selectedPlatform != "pixiv") return await Platform.Auth(null); if (settings.pixiv_refresh_token is { } token && !string.IsNullOrWhiteSpace(token)) { - return await Platform.auth(token); + return await Platform.Auth(token); } return false; } diff --git a/Artify/Models/Settings.cs b/Artify/Models/Settings.cs index a0ba808..fa7cb7c 100644 --- a/Artify/Models/Settings.cs +++ b/Artify/Models/Settings.cs @@ -1,4 +1,4 @@ -namespace Artify +namespace Artify.Models { public class Settings { diff --git a/Artify/ViewModels/ArtifyViewModel.cs b/Artify/ViewModels/ArtifyViewModel.cs index 8cbb4bc..0ca6990 100644 --- a/Artify/ViewModels/ArtifyViewModel.cs +++ b/Artify/ViewModels/ArtifyViewModel.cs @@ -1,52 +1,38 @@ using System; using System.Diagnostics; -using System.Threading.Tasks; using ArtAPI; +using Artify.Models; using Artify.ViewModels.misc; -namespace Artify +namespace Artify.ViewModels { public class ArtifyViewModel : BaseViewModel { private readonly ArtifyModel artifyModel; - private State? _currentState; + private State _currentState; #region public properties - #region mainview properties public string RunDownloadButtonContent { get; set; } = "Download"; public int DownloadProgress { get; private set; } public int TotalImageCount { get; private set; } = 1; public string UserInput { get; set; } public bool RunDLButtonIsEnabled { get; private set; } = true; - public bool LoginStatus { get; set; } = true; + public bool IsLoggedIn { get; set; } = true; public string Notification { get; set; } public string InputErrorMessage { get; set; } public bool IsInputValid { get; set; } = true; public string SelectedPlatform { get; set; } #endregion - #region settings properties - public string UserName { get; set; } - public string UserPassword { private get; set; } - public bool IsLoginInputValid { get; set; } = true; - public string SaveLocation - { - get => artifyModel.SavePath; - set => artifyModel.SavePath = value; - } - #endregion #region commands public RelayCommand RunDownloadCommand { get; private set; } public RelayCommand SelectPlatformCommand { get; } - public RelayCommand LoginCommand { get; } public RelayCommand ShutdownCommand { get; } #endregion - #endregion #region ctor public ArtifyViewModel() { artifyModel = new ArtifyModel(); RunDownloadCommand = new RelayCommand(RunDownload); SelectPlatformCommand = new RelayCommand(SelectPlatform); - LoginCommand = new RelayCommand(async () => await Login()); ShutdownCommand = new RelayCommand(Shutdown); } #endregion @@ -56,6 +42,11 @@ private void Shutdown(IShutDown view) CancelDownload(); view.AppShutDown(_currentState); } + + public SettingsViewModel CreateSettingsVM() + { + return new SettingsViewModel(artifyModel); + } #region ArtAPI event handler private void Platform_DownloadStateChanged(object sender, DownloadStateChangedEventArgs e) { @@ -100,6 +91,30 @@ private void UpdateProgress(object sender, DownloadProgressChangedEventArgs e) { DownloadProgress = e.CurrentProgress; } + private void PlatformOnLoginStatusChanged(object sender, LoginStatusChangedEventArgs e) + { + switch (e.Status) + { + case LoginStatus.LoggingIn: + RunDLButtonIsEnabled = false; + Notification = "Logging in ..."; + break; + case LoginStatus.Authenticating: + Notification = "Authenticating ..."; + RunDLButtonIsEnabled = false; + break; + case LoginStatus.LoggedIn: + Notification = ""; + IsLoggedIn = true; + RunDLButtonIsEnabled = true; + break; + case LoginStatus.Failed: + Notification = "Authenticating failed"; + IsLoggedIn = false; + RunDLButtonIsEnabled = true; + break; + } + } #endregion #region methods private async void SelectPlatform(string platform) @@ -112,7 +127,8 @@ private async void SelectPlatform(string platform) artifyModel.SelectPlatform(platform); artifyModel.Platform.DownloadProgressChanged += UpdateProgress; artifyModel.Platform.DownloadStateChanged += Platform_DownloadStateChanged; - LoginStatus = await artifyModel.Auth(); + artifyModel.Platform.LoginStatusChanged += PlatformOnLoginStatusChanged; + IsLoggedIn = await artifyModel.Auth(); } private async void RunDownload() @@ -173,22 +189,6 @@ private void CancelDownload() { artifyModel.Platform?.CancelDownload(); } - - private async Task Login() - { - if (string.IsNullOrEmpty(UserName) || string.IsNullOrEmpty(UserPassword)) return; - if (await artifyModel.Platform.login(UserName, UserPassword) is { } result) - { - artifyModel.settings.pixiv_refresh_token = result; - artifyModel.UpdateSettings(); - LoginStatus = true; - } - else - { - IsLoginInputValid = false; - } - UserPassword = null; - } #endregion } } diff --git a/Artify/ViewModels/BaseViewModel.cs b/Artify/ViewModels/BaseViewModel.cs index 38621ab..d561395 100644 --- a/Artify/ViewModels/BaseViewModel.cs +++ b/Artify/ViewModels/BaseViewModel.cs @@ -1,6 +1,6 @@ using PropertyChanged; -namespace Artify +namespace Artify.ViewModels { [AddINotifyPropertyChangedInterface] public class BaseViewModel diff --git a/Artify/ViewModels/RelayCommand.cs b/Artify/ViewModels/RelayCommand.cs index 057c6f6..7ce9c54 100644 --- a/Artify/ViewModels/RelayCommand.cs +++ b/Artify/ViewModels/RelayCommand.cs @@ -1,7 +1,7 @@ using System; using System.Windows.Input; -namespace Artify +namespace Artify.ViewModels { public class RelayCommand : ICommand { diff --git a/Artify/ViewModels/SettingsViewModel.cs b/Artify/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..055f8a6 --- /dev/null +++ b/Artify/ViewModels/SettingsViewModel.cs @@ -0,0 +1,91 @@ +using System.Threading.Tasks; +using ArtAPI; +using Artify.Models; + +namespace Artify.ViewModels +{ + public class SettingsViewModel : BaseViewModel + { + private readonly ArtifyModel _artifyModel; + public string UserName { get; set; } + public string UserPassword { private get; set; } + public bool IsLoginInputValid { get; set; } = true; + public bool IsLoginButtonEnabled { get; set; } = true; + public string LoginNotification { get; set; } + public string SaveLocation + { + get => _artifyModel.SavePath; + set => _artifyModel.SavePath = value; + } + public int ClientTimeout + { + get => _artifyModel.Platform.ClientTimeout; + set => _artifyModel.Platform.ClientTimeout = value; + } + public int DownloadAttempts + { + get => _artifyModel.Platform.DownloadAttempts; + set => _artifyModel.Platform.DownloadAttempts = value; + } + public int ConcurrentTasks + { + get => _artifyModel.Platform.ConcurrentTasks; + set => _artifyModel.Platform.ConcurrentTasks = value; + } + + public bool IsLoggedIn { get; set; } + public RelayCommand LoginCommand { get; } + public SettingsViewModel(ArtifyModel artifyM) + { + _artifyModel = artifyM; + _artifyModel.Platform.LoginStatusChanged += Platform_LoginStatusChanged; + Platform_LoginStatusChanged(this, new LoginStatusChangedEventArgs(_artifyModel.Platform.LoginState)); + LoginCommand = new RelayCommand(async () => await Login()); + } + ~SettingsViewModel() + { + _artifyModel.Platform.LoginStatusChanged -= Platform_LoginStatusChanged; + } + + private void Platform_LoginStatusChanged(object sender, LoginStatusChangedEventArgs e) + { + switch (e.Status) + { + case LoginStatus.LoggingIn: + IsLoginButtonEnabled = false; + LoginNotification = "Logging in ..."; + break; + case LoginStatus.Authenticating: + IsLoginButtonEnabled = false; + LoginNotification = "Authenticating ..."; + break; + case LoginStatus.LoggedIn: + LoginNotification = ""; + IsLoggedIn = true; + break; + case LoginStatus.Failed: + LoginNotification = "Authenticating failed"; + IsLoginButtonEnabled = true; + break; + } + } + + private async Task Login() + { + if (string.IsNullOrEmpty(UserName) || string.IsNullOrEmpty(UserPassword)) return; + IsLoginButtonEnabled = false; + if (await _artifyModel.Platform.Login(UserName, UserPassword) is { } result) + { + _artifyModel.settings.pixiv_refresh_token = result; + _artifyModel.UpdateSettings(); + } + else + { + IsLoginInputValid = false; + LoginNotification = "Incorrect username or password"; + } + UserPassword = null; + IsLoginButtonEnabled = true; + } + } +} diff --git a/Artify/Views/MainView.xaml b/Artify/Views/MainView.xaml index 9027e9f..5c05e62 100644 --- a/Artify/Views/MainView.xaml +++ b/Artify/Views/MainView.xaml @@ -1,17 +1,13 @@ - - - - diff --git a/Artify/Views/MainView.xaml.cs b/Artify/Views/MainView.xaml.cs index f95f065..acd4622 100644 --- a/Artify/Views/MainView.xaml.cs +++ b/Artify/Views/MainView.xaml.cs @@ -3,10 +3,10 @@ using System.Windows.Input; using System.Windows.Media.Animation; using ArtAPI; +using Artify.ViewModels; using Artify.ViewModels.misc; -using Artify.Views; -namespace Artify +namespace Artify.Views { public partial class MainWindow : Window, IShutDown { @@ -17,6 +17,7 @@ private Storyboard ShowSelectionMenuAnimation; private SettingsPopUp popUp; + private readonly ArtifyViewModel _artifyVM = new ArtifyViewModel(); public MainWindow() { @@ -30,6 +31,7 @@ public MainWindow() MaxHeight = SystemParameters.WorkArea.Height + 10; MaxWidth = SystemParameters.WorkArea.Width + 10; Opacity = 0; + DataContext = _artifyVM; ResizeButton.DataContext = this; } @@ -45,7 +47,11 @@ private void SelectionMenuButton_Click(object sender, RoutedEventArgs e) } private void SettingsClick(object sender, RoutedEventArgs e) { - popUp = new SettingsPopUp(this); + popUp = new SettingsPopUp + { + Owner = this, + DataContext = _artifyVM.CreateSettingsVM() + }; popUp.ShowDialog(); } @@ -141,7 +147,7 @@ private void LoginNotification_IsVisibleChanged(object sender, DependencyPropert public void AppShutDown(object state) { - var _state = (State?)state; + var _state = (State)state; if (_state == State.DownloadPreparing | _state == State.DownloadRunning) if (MessageBox.Show("Are you sure?", "warning", MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; DataContext = null; diff --git a/Artify/Views/SettingsPopUp.xaml b/Artify/Views/SettingsPopUp.xaml index aa010f0..428b0e0 100644 --- a/Artify/Views/SettingsPopUp.xaml +++ b/Artify/Views/SettingsPopUp.xaml @@ -27,10 +27,11 @@ + - + -