diff --git a/.gitignore b/.gitignore index d05cd606..aeb048b3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ *.suo *.userprefs packages -Components/ +.vs diff --git a/CodeHub.Core/App.cs b/CodeHub.Core/App.cs index 4d2cc925..aa6de46a 100755 --- a/CodeHub.Core/App.cs +++ b/CodeHub.Core/App.cs @@ -1,4 +1,3 @@ -using CodeHub.Core.ViewModels.App; using System.Net; using MvvmCross.Core.ViewModels; @@ -15,9 +14,6 @@ public class App : MvxApplication public override void Initialize() { ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - - //Ensure this is loaded - MvvmCross.Plugins.Messenger.PluginLoader.Instance.EnsureLoaded(); } } } \ No newline at end of file diff --git a/CodeHub.Core/Bootstrap/MessengerPluginBootstrap.cs b/CodeHub.Core/Bootstrap/MessengerPluginBootstrap.cs deleted file mode 100644 index deadaaf7..00000000 --- a/CodeHub.Core/Bootstrap/MessengerPluginBootstrap.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MvvmCross.Platform.Plugins; - -namespace CodeHub.Core.Bootstrap -{ - public class MessengerPluginBootstrap - : MvxPluginBootstrapAction - { - } -} \ No newline at end of file diff --git a/CodeHub.Core/CodeHub.Core.iOS.csproj b/CodeHub.Core/CodeHub.Core.csproj similarity index 57% rename from CodeHub.Core/CodeHub.Core.iOS.csproj rename to CodeHub.Core/CodeHub.Core.csproj index a52a2ede..9f744112 100644 --- a/CodeHub.Core/CodeHub.Core.iOS.csproj +++ b/CodeHub.Core/CodeHub.Core.csproj @@ -35,7 +35,6 @@ - @@ -43,18 +42,9 @@ - - - - - - - - - @@ -64,32 +54,19 @@ - - - - - - - - - - - - - @@ -99,82 +76,87 @@ - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -198,49 +180,107 @@ ..\packages\Splat.1.6.2\lib\Xamarin.iOS10\Splat.dll - - ..\packages\reactiveui-core.6.5.0\lib\Xamarin.iOS10\ReactiveUI.dll + + ..\packages\GitHubClient.1.0.15\lib\portable45-net45+win8+wpa81\GitHubSharp.dll - - ..\packages\Newtonsoft.Json.8.0.3\lib\portable-net40+sl5+wp80+win8+wpa81\Newtonsoft.Json.dll + + ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.dll - - ..\packages\GitHubClient.1.0.14.0\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\GitHubSharp.dll + + ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll - - ..\packages\MvvmCross.Platform.4.1.4\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll + + ..\packages\akavache.core.5.0.0\lib\Xamarin.iOS10\Akavache.dll - - ..\packages\MvvmCross.Platform.4.1.4\lib\Xamarin.iOS10\MvvmCross.Platform.dll + + ..\packages\akavache.sqlite3.5.0.0\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll - - ..\packages\MvvmCross.Plugin.Messenger.4.1.4\lib\Xamarin.iOS10\MvvmCross.Plugins.Messenger.dll + + ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.dll - - ..\packages\MvvmCross.Core.4.1.4\lib\Xamarin.iOS10\MvvmCross.iOS.dll + + ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll - ..\packages\MvvmCross.Core.4.1.4\lib\Xamarin.iOS10\MvvmCross.Core.dll + ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.Core.dll - - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll + + ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.iOS.dll - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Binding.dll + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.dll + + + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Localization.dll + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Localization.dll + + + ..\packages\Microsoft.Net.Http.2.2.29\lib\Xamarin.iOS10\System.Net.Http.Extensions.dll + + + ..\packages\Microsoft.Net.Http.2.2.29\lib\Xamarin.iOS10\System.Net.Http.Primitives.dll + + + ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll + + + ..\packages\reactiveui-core.7.4.0\lib\Xamarin.iOS10\ReactiveUI.dll + + + + ..\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll + + + ..\packages\SQLitePCLRaw.core.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.core.dll + + + ..\packages\SQLitePCLRaw.lib.e_sqlite3.ios_unified.static.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.lib.e_sqlite3.dll + + + ..\packages\SQLitePCLRaw.provider.internal.ios_unified.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.provider.internal.dll + + + ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_e_sqlite3.dll + + + ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll + + + ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll + + + ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.dll + + + ..\packages\Octokit.0.29.0\lib\netstandard1.1\Octokit.dll + + + ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.Abstractions.dll + + + ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.dll + + + ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.Abstractions.dll + + + ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.dll - + + + + \ No newline at end of file diff --git a/CodeHub.Core/Data/Account.cs b/CodeHub.Core/Data/Account.cs index 0ba899c2..bdb590a1 100644 --- a/CodeHub.Core/Data/Account.cs +++ b/CodeHub.Core/Data/Account.cs @@ -1,158 +1,106 @@ -using System; -using System.Globalization; -using System.IO; -using CodeHub.Core.Services; -using MvvmCross.Platform; -using SQLite; - +using System.Collections.Generic; +using Newtonsoft.Json; + namespace CodeHub.Core.Data { - public abstract class Account : IDisposable + public class Account { - private SQLiteConnection _database; - private AccountFilters _filters; - private AccountPinnedRepositories _pinnedRepositories; - - [PrimaryKey] - [AutoIncrement] - public int Id { get; set; } - - /// - /// Gets or sets the username. - /// - /// The username. + public string Id => Username + Domain; + + public string OAuth { get; set; } + + public string Password { get; set; } + + public string Domain { get; set; } + + public string WebDomain { get; set; } + + public bool IsEnterprise { get; set; } + + public bool ShowOrganizationsInEvents { get; set; } = true; + + public bool ExpandOrganizations { get; set; } = true; + + public bool ShowRepositoryDescriptionInList { get; set; } = true; + + public bool? IsPushNotificationsEnabled { get; set; } + public string Username { get; set; } - /// - /// Gets or sets the avatar URL. - /// - /// The avatar URL. - public string AvatarUrl { get; set; } - - /// - /// Gets or sets the name of the startup view when the account is loaded - /// - /// The startup view. - public string DefaultStartupView { get; set; } - - /// - /// Gets or sets a value indicating whether this dont remember. - /// THIS HAS TO BE A NEGATIVE STATEMENT SINCE IT DEFAULTS TO 'FALSE' WHEN RETRIEVING A NULL VIA SQLITE - /// - public bool DontRemember { get; set; } - - [Ignore] - public SQLiteConnection Database - { - get - { - if (_database == null) - { - if (!Directory.Exists(AccountDirectory)) - Directory.CreateDirectory(AccountDirectory); + public string AvatarUrl { get; set; } - var dbPath = Path.Combine(AccountDirectory, "settings.db"); - _database = new SQLiteConnection(dbPath); - return _database; - } + public string DefaultStartupView { get; set; } - return _database; - } + public string CodeEditTheme { get; set; } = "idea"; + + private List _pinnedRepositories = new List(); + public List PinnedRepositories + { + get { return _pinnedRepositories ?? new List(); } + set { _pinnedRepositories = value ?? new List(); } } - [Ignore] - public string AccountDirectory - { - get - { - var accountsDir = Mvx.Resolve().AccountsDir; - return Path.Combine(accountsDir, Id.ToString(CultureInfo.InvariantCulture)); - } - } - - [Ignore] - public AccountFilters Filters - { - get - { - return _filters ?? (_filters = new AccountFilters(Database)); - } - } - - [Ignore] - public AccountPinnedRepositories PinnnedRepositories - { - get - { - return _pinnedRepositories ?? (_pinnedRepositories = new AccountPinnedRepositories(Database)); - } - } - - private void CreateAccountDirectory() - { - if (!Directory.Exists(AccountDirectory)) - Directory.CreateDirectory(AccountDirectory); - } - - /// - /// This creates this account's directory - /// - public void Initialize() + private Dictionary _filters = new Dictionary(); + public Dictionary Filters { - CreateAccountDirectory(); + get { return _filters ?? new Dictionary(); } + set { _filters = value ?? new Dictionary(); } } + } - /// - /// This destorys this account's directory - /// - public void Destory() + public static class AccountExtensions + { + public static T GetFilter(this Account account, string key) where T : class, new() { - if (!Directory.Exists(AccountDirectory)) - return; - Directory.Delete(AccountDirectory, true); + Filter filter = null; + if (account.Filters?.TryGetValue(key, out filter) == false) + return default(T); + return filter?.GetData() ?? new T(); } - - /// - /// Returns a that represents the current . - /// - /// A that represents the current . - public override string ToString() + public static void SetFilter(this Account account, string key, object filter) { - return Username; - } - - public void Dispose() - { - if (_database != null) _database.Dispose(); - } - - /// - /// Determines whether the specified is equal to the current . - /// - /// The to compare with the current . - /// true if the specified is equal to the current - /// ; otherwise, false. - public override bool Equals(object obj) + var f = new Filter(); + f.SetData(filter); + if (account.Filters == null) + account.Filters = new Dictionary(); + account.Filters[key] = f; + } + } + + public class PinnedRepository + { + public string Owner { get; set; } + + public string Slug { get; set; } + + public string Name { get; set; } + + public string ImageUri { get; set; } + } + + public class Filter + { + public string RawData { get; set; } + } + + public static class FilterExtensions + { + public static T GetData(this Filter filter) where T : new() { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - var act = obj as Account; - return act != null && this.Id.Equals(act.Id); - } - - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// - /// 2 - public override int GetHashCode() + try + { + return JsonConvert.DeserializeObject(filter.RawData); + } + catch + { + return default(T); + } + } + + public static void SetData(this Filter filter, object o) { - return this.Id; + filter.RawData = JsonConvert.SerializeObject(o); } } } diff --git a/CodeHub.Core/Data/AccountFilters.cs b/CodeHub.Core/Data/AccountFilters.cs deleted file mode 100755 index 8ebe9776..00000000 --- a/CodeHub.Core/Data/AccountFilters.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using CodeHub.Core.ViewModels; -using SQLite; - -namespace CodeHub.Core.Data -{ - public class AccountFilters : IEnumerable - { - private readonly SQLiteConnection _sqLiteConnection; - - public AccountFilters(SQLiteConnection sqLiteConnection) - { - _sqLiteConnection = sqLiteConnection; - _sqLiteConnection.CreateTable(); - } - - /// - /// Gets the filter. - /// - /// The filter. - /// Key. - public TFilter GetFilter(string key) where TFilter : FilterModel, new() - { - Filter filter; - lock (_sqLiteConnection) - { - filter = _sqLiteConnection.Find(x => x.Type == key); - } - - if (filter == null) - return new TFilter(); - var filterModel = filter.GetData(); - return filterModel ?? new TFilter(); - } - - /// - /// Gets the filter. - /// - /// The filter. - /// Key. - public TFilter GetFilter(object key) where TFilter : FilterModel, new() - { - return GetFilter(key.GetType().Name); - } - - /// - /// Gets a filter by it's primary id - /// - /// The filter. - /// Identifier. - public Filter GetFilter(string key) - { - lock (_sqLiteConnection) - { - return _sqLiteConnection.Find(x => x.Type == key); - } - } - - /// - /// Adds the filter - /// - /// Key. - /// Data. - public void AddFilter(string key, object data) - { - RemoveFilters(key); - var filter = new Filter { Type = key }; - filter.SetData(data); - - lock (_sqLiteConnection) - { - _sqLiteConnection.Insert(filter); - } - } - - /// - /// Removes the filter - /// - /// Identifier. - public void RemoveFilter(int id) - { - lock (_sqLiteConnection) - { - _sqLiteConnection.Delete(new Filter { Id = id }); - } - } - - /// - /// Updates the filter - /// - /// Filter. - public void UpdateFilter(Filter filter) - { - lock (_sqLiteConnection) - { - _sqLiteConnection.Update(filter); - } - } - - /// - /// Removes all filters with a specific key - /// - /// Key. - public void RemoveFilters(string key) - { - lock (_sqLiteConnection) - { - var filters = _sqLiteConnection.Table().Where(x => x.Type == key).ToList(); - foreach (var filter in filters) - _sqLiteConnection.Delete(filter); - } - } - - public IEnumerator GetEnumerator() - { - lock (_sqLiteConnection) - { - return _sqLiteConnection.Table().ToList().GetEnumerator(); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/CodeHub.Core/Data/AccountPinnedRepositories.cs b/CodeHub.Core/Data/AccountPinnedRepositories.cs deleted file mode 100755 index 11a3e00c..00000000 --- a/CodeHub.Core/Data/AccountPinnedRepositories.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using SQLite; - -namespace CodeHub.Core.Data -{ - public class AccountPinnedRepositories : IEnumerable - { - private readonly SQLiteConnection _sqLiteConnection; - - public AccountPinnedRepositories(SQLiteConnection sqLiteConnection) - { - _sqLiteConnection = sqLiteConnection; - _sqLiteConnection.CreateTable(); - } - - /// - /// Adds the pinned repository. - /// - /// Owner. - /// Slug. - /// Name. - /// Image URI. - public void AddPinnedRepository(string owner, string slug, string name, string imageUri) - { - var resource = new PinnedRepository { Owner = owner, Slug = slug, Name = name, ImageUri = imageUri }; - lock (_sqLiteConnection) - { - _sqLiteConnection.Insert(resource); - } - } - - /// - /// Removes the pinned repository. - /// - /// Identifier. - public void RemovePinnedRepository(int id) - { - lock (_sqLiteConnection) - { - _sqLiteConnection.Delete(new PinnedRepository { Id = id }); - } - } - - /// - /// Gets the pinned repository. - /// - /// The pinned repository. - /// Owner. - /// Slug. - public PinnedRepository GetPinnedRepository(string owner, string slug) - { - lock (_sqLiteConnection) - { - return _sqLiteConnection.Find(x => x.Owner == owner && x.Slug == slug); - } - } - - public IEnumerator GetEnumerator() - { - lock (_sqLiteConnection) - { - return _sqLiteConnection.Table().OrderBy(x => x.Name).ToList().GetEnumerator(); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/CodeHub.Core/Data/Filter.cs b/CodeHub.Core/Data/Filter.cs deleted file mode 100755 index ac91c4b7..00000000 --- a/CodeHub.Core/Data/Filter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MvvmCross.Platform; -using SQLite; - -namespace CodeHub.Core.Data -{ - public class Filter - { - [PrimaryKey] - [AutoIncrement] - public int Id { get; set; } - - public string Type { get; set; } - - [MaxLength(2048)] - public string RawData { get; set; } - - /// - /// Gets the data. - /// - /// The data. - /// The 1st type parameter. - public T GetData() where T : new() - { - try - { - var serializer = Mvx.Resolve(); - return serializer.Deserialize(RawData); - } - catch - { - return default(T); - } - } - - /// - /// Sets the data. - /// - /// O. - public void SetData(object o) - { - var serializer = Mvx.Resolve(); - RawData = serializer.Serialize(o); - } - } -} - diff --git a/CodeHub.Core/Data/GitHubAccount.cs b/CodeHub.Core/Data/GitHubAccount.cs deleted file mode 100755 index 1efda8a4..00000000 --- a/CodeHub.Core/Data/GitHubAccount.cs +++ /dev/null @@ -1,72 +0,0 @@ -using CodeHub.Core.Data; - -namespace CodeHub.Core.Data -{ - public class GitHubAccount : Account - { - /// - /// Gets or sets the OAuth string - /// - public string OAuth { get; set; } - - /// - /// The password which is only used on Enterprise accounts since oAuth does not work. - /// - /// The password. - public string Password { get; set; } - - /// - /// Gets or sets the domain (API) - /// - /// The domain. - public string Domain { get; set; } - - /// - /// Gets or sets the web domain. Sort of like the API domain with the API paths - /// - /// The web domain. - public string WebDomain { get; set; } - - /// - /// Gets whether this account is enterprise or not - /// - /// true if enterprise; otherwise, false. - public bool IsEnterprise { get; set; } - - /// - /// Gets or sets whether orgs should be listed in the menu controller under 'events' - /// - public bool ShowOrganizationsInEvents { get; set; } - - /// - /// Gets or sets whether teams & groups should be expanded in the menu controller to their actual contents - /// - public bool ExpandOrganizations { get; set; } - - /// - /// Gets or sets a value indicating whether this hides the repository - /// description in list. - /// - /// true if hide repository description in list; otherwise, false. - public bool ShowRepositoryDescriptionInList { get; set; } - - /// - /// Gets or sets a value indicating whether this push notifications enabled. - /// - /// true if push notifications enabled; otherwise, false. - public bool? IsPushNotificationsEnabled { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public GitHubAccount() - { - //Set some default values - DontRemember = false; - ShowOrganizationsInEvents = true; - ExpandOrganizations = true; - ShowRepositoryDescriptionInList = true; - } - } -} - diff --git a/CodeHub.Core/Data/ImgurResponse.cs b/CodeHub.Core/Data/ImgurResponse.cs new file mode 100644 index 00000000..678eb26c --- /dev/null +++ b/CodeHub.Core/Data/ImgurResponse.cs @@ -0,0 +1,15 @@ +namespace CodeHub.Core.Data +{ + public class ImgurResponse + { + public ImgurDataModel Data { get; set; } + + public bool Success { get; set; } + + public class ImgurDataModel + { + public string Link { get; set; } + } + } +} + diff --git a/CodeHub.Core/Data/LanguageRepository.cs b/CodeHub.Core/Data/LanguageRepository.cs index 04a55394..96dd9949 100644 --- a/CodeHub.Core/Data/LanguageRepository.cs +++ b/CodeHub.Core/Data/LanguageRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Collections.Generic; using System.Net.Http; using GitHubSharp; diff --git a/CodeHub.Core/Data/PinnedRepository.cs b/CodeHub.Core/Data/PinnedRepository.cs deleted file mode 100755 index a70f44f9..00000000 --- a/CodeHub.Core/Data/PinnedRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ -using SQLite; - -namespace CodeHub.Core.Data -{ - public class PinnedRepository - { - [PrimaryKey] - [AutoIncrement] - public int Id { get; set; } - - public string Owner { get; set; } - - public string Slug { get; set; } - - public string Name { get; set; } - - public string ImageUri { get; set; } - } -} - diff --git a/CodeHub.Core/Data/SQLite.cs b/CodeHub.Core/Data/SQLite.cs deleted file mode 100644 index 0df1a7dd..00000000 --- a/CodeHub.Core/Data/SQLite.cs +++ /dev/null @@ -1,3504 +0,0 @@ -// -// Copyright (c) 2009-2015 Krueger Systems, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// -#if WINDOWS_PHONE && !USE_WP8_NATIVE_SQLITE -#define USE_CSHARP_SQLITE -#endif - -#if NETFX_CORE -#define USE_NEW_REFLECTION_API -#endif - -using System; -using System.Diagnostics; -#if !USE_SQLITEPCL_RAW -using System.Runtime.InteropServices; -#endif -using System.Collections.Generic; -#if NO_CONCURRENT -using ConcurrentStringDictionary = System.Collections.Generic.Dictionary; -using SQLite.Extensions; -#else -using ConcurrentStringDictionary = System.Collections.Concurrent.ConcurrentDictionary; -#endif -using System.Reflection; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; - -#if USE_CSHARP_SQLITE -using Sqlite3 = Community.CsharpSqlite.Sqlite3; -using Sqlite3DatabaseHandle = Community.CsharpSqlite.Sqlite3.sqlite3; -using Sqlite3Statement = Community.CsharpSqlite.Sqlite3.Vdbe; -#elif USE_WP8_NATIVE_SQLITE -using Sqlite3 = Sqlite.Sqlite3; -using Sqlite3DatabaseHandle = Sqlite.Database; -using Sqlite3Statement = Sqlite.Statement; -#elif USE_SQLITEPCL_RAW -using Sqlite3DatabaseHandle = SQLitePCL.sqlite3; -using Sqlite3Statement = SQLitePCL.sqlite3_stmt; -using Sqlite3 = SQLitePCL.raw; -#else -using Sqlite3DatabaseHandle = System.IntPtr; -using Sqlite3Statement = System.IntPtr; -#endif - -namespace SQLite -{ -public class SQLiteException : Exception -{ -public SQLite3.Result Result { get; private set; } - -protected SQLiteException (SQLite3.Result r,string message) : base(message) -{ -Result = r; -} - -public static SQLiteException New (SQLite3.Result r, string message) -{ -return new SQLiteException (r, message); -} -} - -public class NotNullConstraintViolationException : SQLiteException -{ -public IEnumerable Columns { get; protected set; } - -protected NotNullConstraintViolationException (SQLite3.Result r, string message) -: this (r, message, null, null) -{ - -} - -protected NotNullConstraintViolationException (SQLite3.Result r, string message, TableMapping mapping, object obj) -: base (r, message) -{ -if (mapping != null && obj != null) { -this.Columns = from c in mapping.Columns -where c.IsNullable == false && c.GetValue (obj) == null -select c; -} -} - -public static new NotNullConstraintViolationException New (SQLite3.Result r, string message) -{ -return new NotNullConstraintViolationException (r, message); -} - -public static NotNullConstraintViolationException New (SQLite3.Result r, string message, TableMapping mapping, object obj) -{ -return new NotNullConstraintViolationException (r, message, mapping, obj); -} - -public static NotNullConstraintViolationException New (SQLiteException exception, TableMapping mapping, object obj) -{ -return new NotNullConstraintViolationException (exception.Result, exception.Message, mapping, obj); -} -} - -[Flags] -public enum SQLiteOpenFlags { -ReadOnly = 1, ReadWrite = 2, Create = 4, -NoMutex = 0x8000, FullMutex = 0x10000, -SharedCache = 0x20000, PrivateCache = 0x40000, -ProtectionComplete = 0x00100000, -ProtectionCompleteUnlessOpen = 0x00200000, -ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, -ProtectionNone = 0x00400000 -} - -[Flags] -public enum CreateFlags -{ -None = 0x000, -ImplicitPK = 0x001, // create a primary key for field called 'Id' (Orm.ImplicitPkName) -ImplicitIndex = 0x002, // create an index for fields ending in 'Id' (Orm.ImplicitIndexSuffix) -AllImplicit = 0x003, // do both above -AutoIncPK = 0x004, // force PK field to be auto inc -FullTextSearch3 = 0x100, // create virtual table using FTS3 -FullTextSearch4 = 0x200 // create virtual table using FTS4 -} - -/// -/// Represents an open connection to a SQLite database. -/// -public partial class SQLiteConnection : IDisposable -{ -private bool _open; -private TimeSpan _busyTimeout; -private Dictionary _mappings = null; -private Dictionary _tables = null; -private System.Diagnostics.Stopwatch _sw; -private long _elapsedMilliseconds = 0; - -private int _transactionDepth = 0; -private Random _rand = new Random (); - -public Sqlite3DatabaseHandle Handle { get; private set; } -internal static readonly Sqlite3DatabaseHandle NullHandle = default(Sqlite3DatabaseHandle); - -public string DatabasePath { get; private set; } - -public bool TimeExecution { get; set; } - -public bool Trace { get; set; } - -public bool StoreDateTimeAsTicks { get; private set; } - -/// -/// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. -/// -/// -/// Specifies the path to the database file. -/// -/// -/// Specifies whether to store DateTime properties as ticks (true) or strings (false). You -/// absolutely do want to store them as Ticks in all new projects. The value of false is -/// only here for backwards compatibility. There is a *significant* speed advantage, with no -/// down sides, when setting storeDateTimeAsTicks = true. -/// If you use DateTimeOffset properties, it will be always stored as ticks regardingless -/// the storeDateTimeAsTicks parameter. -/// -public SQLiteConnection (string databasePath, bool storeDateTimeAsTicks = true) -: this (databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks) -{ -} - -/// -/// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. -/// -/// -/// Specifies the path to the database file. -/// -/// -/// Specifies whether to store DateTime properties as ticks (true) or strings (false). You -/// absolutely do want to store them as Ticks in all new projects. The value of false is -/// only here for backwards compatibility. There is a *significant* speed advantage, with no -/// down sides, when setting storeDateTimeAsTicks = true. -/// If you use DateTimeOffset properties, it will be always stored as ticks regardingless -/// the storeDateTimeAsTicks parameter. -/// -public SQLiteConnection (string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) -{ -if (string.IsNullOrEmpty (databasePath)) -throw new ArgumentException ("Must be specified", "databasePath"); - -DatabasePath = databasePath; - -#if NETFX_CORE -SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); -#endif - -Sqlite3DatabaseHandle handle; - -#if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW -var r = SQLite3.Open (databasePath, out handle, (int)openFlags, IntPtr.Zero); -#else -// open using the byte[] -// in the case where the path may include Unicode -// force open to using UTF-8 using sqlite3_open_v2 -var databasePathAsBytes = GetNullTerminatedUtf8 (DatabasePath); -var r = SQLite3.Open (databasePathAsBytes, out handle, (int) openFlags, IntPtr.Zero); -#endif - -Handle = handle; -if (r != SQLite3.Result.OK) { -throw SQLiteException.New (r, String.Format ("Could not open database file: {0} ({1})", DatabasePath, r)); -} -_open = true; - -StoreDateTimeAsTicks = storeDateTimeAsTicks; - -BusyTimeout = TimeSpan.FromSeconds (0.1); -} - -#if __IOS__ -static SQLiteConnection () -{ -if (_preserveDuringLinkMagic) { -var ti = new ColumnInfo (); -ti.Name = "magic"; -} -} - -/// -/// Used to list some code that we want the MonoTouch linker -/// to see, but that we never want to actually execute. -/// -static bool _preserveDuringLinkMagic; -#endif - -#if !USE_SQLITEPCL_RAW -public void EnableLoadExtension(int onoff) -{ -SQLite3.Result r = SQLite3.EnableLoadExtension(Handle, onoff); -if (r != SQLite3.Result.OK) { -string msg = SQLite3.GetErrmsg (Handle); -throw SQLiteException.New (r, msg); -} -} -#endif - -#if !USE_SQLITEPCL_RAW -static byte[] GetNullTerminatedUtf8 (string s) -{ - var utf8Length = System.Text.Encoding.UTF8.GetByteCount (s); - var bytes = new byte [utf8Length + 1]; - utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); - return bytes; -} -#endif - -/// -/// Sets a busy handler to sleep the specified amount of time when a table is locked. -/// The handler will sleep multiple times until a total time of has accumulated. -/// -public TimeSpan BusyTimeout { - get { return _busyTimeout; } - set { - _busyTimeout = value; - if (Handle != NullHandle) { - SQLite3.BusyTimeout (Handle, (int)_busyTimeout.TotalMilliseconds); - } - } -} - -/// -/// Returns the mappings from types to tables that the connection -/// currently understands. -/// -public IEnumerable TableMappings { - get { - return _tables != null ? _tables.Values : Enumerable.Empty (); - } -} - -/// -/// Retrieves the mapping that is automatically generated for the given type. -/// -/// -/// The type whose mapping to the database is returned. -/// -/// -/// Optional flags allowing implicit PK and indexes based on naming conventions -/// -/// -/// The mapping represents the schema of the columns of the database and contains -/// methods to set and get properties of objects. -/// -public TableMapping GetMapping(Type type, CreateFlags createFlags = CreateFlags.None) -{ - if (_mappings == null) { - _mappings = new Dictionary (); - } - TableMapping map; - if (!_mappings.TryGetValue (type.FullName, out map)) { - map = new TableMapping (type, createFlags); - _mappings [type.FullName] = map; - } - return map; -} - -/// -/// Retrieves the mapping that is automatically generated for the given type. -/// -/// -/// The mapping represents the schema of the columns of the database and contains -/// methods to set and get properties of objects. -/// -public TableMapping GetMapping () -{ - return GetMapping (typeof (T)); -} - -private struct IndexedColumn -{ - public int Order; - public string ColumnName; -} - -private struct IndexInfo -{ - public string IndexName; - public string TableName; - public bool Unique; - public List Columns; -} - -/// -/// Executes a "drop table" on the database. This is non-recoverable. -/// -public int DropTable() -{ - var map = GetMapping (typeof (T)); - - var query = string.Format("drop table if exists \"{0}\"", map.TableName); - - return Execute (query); -} - -/// -/// Executes a "create table if not exists" on the database. It also -/// creates any specified indexes on the columns of the table. It uses -/// a schema automatically generated from the specified type. You can -/// later access this schema by calling GetMapping. -/// -/// -/// The number of entries added to the database schema. -/// -public int CreateTable(CreateFlags createFlags = CreateFlags.None) -{ - return CreateTable(typeof (T), createFlags); -} - -/// -/// Executes a "create table if not exists" on the database. It also -/// creates any specified indexes on the columns of the table. It uses -/// a schema automatically generated from the specified type. You can -/// later access this schema by calling GetMapping. -/// -/// Type to reflect to a database table. -/// Optional flags allowing implicit PK and indexes based on naming conventions. -/// -/// The number of entries added to the database schema. -/// -public int CreateTable(Type ty, CreateFlags createFlags = CreateFlags.None) -{ - if (_tables == null) { - _tables = new Dictionary (); - } - TableMapping map; - if (!_tables.TryGetValue (ty.FullName, out map)) { - map = GetMapping (ty, createFlags); - _tables.Add (ty.FullName, map); - } - - // Present a nice error if no columns specified - if (map.Columns.Length == 0) { - throw new Exception (string.Format ("Cannot create a table with zero columns (does '{0}' have public properties?)", ty.FullName)); - } - - // Facilitate virtual tables a.k.a. full-text search. - bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; - bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; - bool fts = fts3 || fts4; - var @virtual = fts ? "virtual " : string.Empty; - var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; - - // Build query. - var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; - var decls = map.Columns.Select (p => Orm.SqlDecl (p, StoreDateTimeAsTicks)); - var decl = string.Join (",\n", decls.ToArray ()); - query += decl; - query += ")"; - - var count = Execute (query); - - if (count == 0) { //Possible bug: This always seems to return 0? - // Table already exists, migrate it - MigrateTable (map); - } - - var indexes = new Dictionary (); - foreach (var c in map.Columns) { - foreach (var i in c.Indices) { - var iname = i.Name ?? map.TableName + "_" + c.Name; - IndexInfo iinfo; - if (!indexes.TryGetValue (iname, out iinfo)) { - iinfo = new IndexInfo { - IndexName = iname, - TableName = map.TableName, - Unique = i.Unique, - Columns = new List () - }; - indexes.Add (iname, iinfo); - } - - if (i.Unique != iinfo.Unique) - throw new Exception ("All the columns in an index must have the same value for their Unique property"); - - iinfo.Columns.Add (new IndexedColumn { - Order = i.Order, - ColumnName = c.Name - }); - } - } - - foreach (var indexName in indexes.Keys) { - var index = indexes[indexName]; - var columns = index.Columns.OrderBy(i => i.Order).Select(i => i.ColumnName).ToArray(); - count += CreateIndex(indexName, index.TableName, columns, index.Unique); - } - - return count; -} - -/// -/// Creates an index for the specified table and columns. -/// -/// Name of the index to create -/// Name of the database table -/// An array of column names to index -/// Whether the index should be unique -public int CreateIndex(string indexName, string tableName, string[] columnNames, bool unique = false) -{ - const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; - var sql = String.Format(sqlFormat, tableName, string.Join ("\", \"", columnNames), unique ? "unique" : "", indexName); - return Execute(sql); -} - -/// -/// Creates an index for the specified table and column. -/// -/// Name of the index to create -/// Name of the database table -/// Name of the column to index -/// Whether the index should be unique -public int CreateIndex(string indexName, string tableName, string columnName, bool unique = false) -{ - return CreateIndex(indexName, tableName, new string[] { columnName }, unique); -} - -/// -/// Creates an index for the specified table and column. -/// -/// Name of the database table -/// Name of the column to index -/// Whether the index should be unique -public int CreateIndex(string tableName, string columnName, bool unique = false) -{ - return CreateIndex(tableName + "_" + columnName, tableName, columnName, unique); -} - -/// -/// Creates an index for the specified table and columns. -/// -/// Name of the database table -/// An array of column names to index -/// Whether the index should be unique -public int CreateIndex(string tableName, string[] columnNames, bool unique = false) -{ - return CreateIndex(tableName + "_" + string.Join ("_", columnNames), tableName, columnNames, unique); -} - -/// -/// Creates an index for the specified object property. -/// e.g. CreateIndex(c => c.Name); -/// -/// Type to reflect to a database table. -/// Property to index -/// Whether the index should be unique -public void CreateIndex(Expression> property, bool unique = false) -{ - MemberExpression mx; - if (property.Body.NodeType == ExpressionType.Convert) - { - mx = ((UnaryExpression)property.Body).Operand as MemberExpression; - } - else - { - mx= (property.Body as MemberExpression); - } - var propertyInfo = mx.Member as PropertyInfo; - if (propertyInfo == null) - { - throw new ArgumentException("The lambda expression 'property' should point to a valid Property"); - } - - var propName = propertyInfo.Name; - - var map = GetMapping(); - var colName = map.FindColumnWithPropertyName(propName).Name; - - CreateIndex(map.TableName, colName, unique); -} - -public class ColumnInfo -{ - // public int cid { get; set; } - - [Column ("name")] - public string Name { get; set; } - - // [Column ("type")] - // public string ColumnType { get; set; } - - public int notnull { get; set; } - - // public string dflt_value { get; set; } - - // public int pk { get; set; } - - public override string ToString () - { - return Name; - } -} - -public List GetTableInfo (string tableName) -{ - var query = "pragma table_info(\"" + tableName + "\")"; - return Query (query); -} - -void MigrateTable (TableMapping map) -{ - var existingCols = GetTableInfo (map.TableName); - - var toBeAdded = new List (); - - foreach (var p in map.Columns) { - var found = false; - foreach (var c in existingCols) { - found = (string.Compare (p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); - if (found) - break; - } - if (!found) { - toBeAdded.Add (p); - } - } - - foreach (var p in toBeAdded) { - var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl (p, StoreDateTimeAsTicks); - Execute (addCol); - } -} - -/// -/// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. -/// -/// -protected virtual SQLiteCommand NewCommand () -{ - return new SQLiteCommand (this); -} - -/// -/// Creates a new SQLiteCommand given the command text with arguments. Place a '?' -/// in the command text for each of the arguments. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the command text. -/// -/// -/// A -/// -public SQLiteCommand CreateCommand (string cmdText, params object[] ps) -{ - if (!_open) - throw SQLiteException.New (SQLite3.Result.Error, "Cannot create commands from unopened database"); - - var cmd = NewCommand (); - cmd.CommandText = cmdText; - foreach (var o in ps) { - cmd.Bind (o); - } - return cmd; -} - -/// -/// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' -/// in the command text for each of the arguments and then executes that command. -/// Use this method instead of Query when you don't expect rows back. Such cases include -/// INSERTs, UPDATEs, and DELETEs. -/// You can set the Trace or TimeExecution properties of the connection -/// to profile execution. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// The number of rows modified in the database as a result of this execution. -/// -public int Execute (string query, params object[] args) -{ - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteNonQuery (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Debug.WriteLine (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; -} - -public T ExecuteScalar (string query, params object[] args) -{ - var cmd = CreateCommand (query, args); - - if (TimeExecution) { - if (_sw == null) { - _sw = new Stopwatch (); - } - _sw.Reset (); - _sw.Start (); - } - - var r = cmd.ExecuteScalar (); - - if (TimeExecution) { - _sw.Stop (); - _elapsedMilliseconds += _sw.ElapsedMilliseconds; - Debug.WriteLine (string.Format ("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); - } - - return r; -} - -/// -/// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' -/// in the command text for each of the arguments and then executes that command. -/// It returns each row of the result using the mapping automatically generated for -/// the given type. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// An enumerable with one result for each row returned by the query. -/// -public List Query (string query, params object[] args) where T : new() -{ - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (); -} - -/// -/// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' -/// in the command text for each of the arguments and then executes that command. -/// It returns each row of the result using the mapping automatically generated for -/// the given type. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// An enumerable with one result for each row returned by the query. -/// The enumerator will call sqlite3_step on each call to MoveNext, so the database -/// connection must remain open for the lifetime of the enumerator. -/// -public IEnumerable DeferredQuery(string query, params object[] args) where T : new() -{ - var cmd = CreateCommand(query, args); - return cmd.ExecuteDeferredQuery(); -} - -/// -/// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' -/// in the command text for each of the arguments and then executes that command. -/// It returns each row of the result using the specified mapping. This function is -/// only used by libraries in order to query the database via introspection. It is -/// normally not used. -/// -/// -/// A to use to convert the resulting rows -/// into objects. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// An enumerable with one result for each row returned by the query. -/// -public List Query (TableMapping map, string query, params object[] args) -{ - var cmd = CreateCommand (query, args); - return cmd.ExecuteQuery (map); -} - -/// -/// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' -/// in the command text for each of the arguments and then executes that command. -/// It returns each row of the result using the specified mapping. This function is -/// only used by libraries in order to query the database via introspection. It is -/// normally not used. -/// -/// -/// A to use to convert the resulting rows -/// into objects. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// An enumerable with one result for each row returned by the query. -/// The enumerator will call sqlite3_step on each call to MoveNext, so the database -/// connection must remain open for the lifetime of the enumerator. -/// -public IEnumerable DeferredQuery(TableMapping map, string query, params object[] args) -{ - var cmd = CreateCommand(query, args); - return cmd.ExecuteDeferredQuery(map); -} - -/// -/// Returns a queryable interface to the table represented by the given type. -/// -/// -/// A queryable object that is able to translate Where, OrderBy, and Take -/// queries into native SQL. -/// -public TableQuery Table () where T : new() -{ - return new TableQuery (this); -} - -/// -/// Attempts to retrieve an object with the given primary key from the table -/// associated with the specified type. Use of this method requires that -/// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). -/// -/// -/// The primary key. -/// -/// -/// The object with the given primary key. Throws a not found exception -/// if the object is not found. -/// -public T Get (object pk) where T : new() -{ - var map = GetMapping (typeof(T)); - return Query (map.GetByPrimaryKeySql, pk).First (); -} - -/// -/// Attempts to retrieve the first object that matches the predicate from the table -/// associated with the specified type. -/// -/// -/// A predicate for which object to find. -/// -/// -/// The object that matches the given predicate. Throws a not found exception -/// if the object is not found. -/// -public T Get (Expression> predicate) where T : new() -{ - return Table ().Where (predicate).First (); -} - -/// -/// Attempts to retrieve an object with the given primary key from the table -/// associated with the specified type. Use of this method requires that -/// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). -/// -/// -/// The primary key. -/// -/// -/// The object with the given primary key or null -/// if the object is not found. -/// -public T Find (object pk) where T : new () -{ - var map = GetMapping (typeof (T)); - return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); -} - -/// -/// Attempts to retrieve an object with the given primary key from the table -/// associated with the specified type. Use of this method requires that -/// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). -/// -/// -/// The primary key. -/// -/// -/// The TableMapping used to identify the object type. -/// -/// -/// The object with the given primary key or null -/// if the object is not found. -/// -public object Find (object pk, TableMapping map) -{ - return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); -} - -/// -/// Attempts to retrieve the first object that matches the predicate from the table -/// associated with the specified type. -/// -/// -/// A predicate for which object to find. -/// -/// -/// The object that matches the given predicate or null -/// if the object is not found. -/// -public T Find (Expression> predicate) where T : new() -{ - return Table ().Where (predicate).FirstOrDefault (); -} - -/// -/// Attempts to retrieve the first object that matches the query from the table -/// associated with the specified type. -/// -/// -/// The fully escaped SQL. -/// -/// -/// Arguments to substitute for the occurences of '?' in the query. -/// -/// -/// The object that matches the given predicate or null -/// if the object is not found. -/// -public T FindWithQuery (string query, params object[] args) where T : new() -{ - return Query (query, args).FirstOrDefault (); -} - -/// -/// Whether has been called and the database is waiting for a . -/// -public bool IsInTransaction { - get { return _transactionDepth > 0; } -} - -/// -/// Begins a new transaction. Call to end the transaction. -/// -/// Throws if a transaction has already begun. -public void BeginTransaction () -{ - // The BEGIN command only works if the transaction stack is empty, - // or in other words if there are no pending transactions. - // If the transaction stack is not empty when the BEGIN command is invoked, - // then the command fails with an error. - // Rather than crash with an error, we will just ignore calls to BeginTransaction - // that would result in an error. - if (Interlocked.CompareExchange (ref _transactionDepth, 1, 0) == 0) { - try { - Execute ("begin transaction"); - } catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } else { - // Call decrement and not VolatileWrite in case we've already - // created a transaction point in SaveTransactionPoint since the catch. - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - } else { - // Calling BeginTransaction on an already open transaction is invalid - throw new InvalidOperationException ("Cannot begin a transaction while already in a transaction."); - } -} - -/// -/// Creates a savepoint in the database at the current point in the transaction timeline. -/// Begins a new transaction if one is not in progress. -/// -/// Call to undo transactions since the returned savepoint. -/// Call to commit transactions after the savepoint returned here. -/// Call to end the transaction, committing all changes. -/// -/// A string naming the savepoint. -public string SaveTransactionPoint () -{ - int depth = Interlocked.Increment (ref _transactionDepth) - 1; - string retVal = "S" + _rand.Next (short.MaxValue) + "D" + depth; - - try { - Execute ("savepoint " + retVal); - } catch (Exception ex) { - var sqlExp = ex as SQLiteException; - if (sqlExp != null) { - // It is recommended that applications respond to the errors listed below - // by explicitly issuing a ROLLBACK command. - // TODO: This rollback failsafe should be localized to all throw sites. - switch (sqlExp.Result) { - case SQLite3.Result.IOError: - case SQLite3.Result.Full: - case SQLite3.Result.Busy: - case SQLite3.Result.NoMem: - case SQLite3.Result.Interrupt: - RollbackTo (null, true); - break; - } - } else { - Interlocked.Decrement (ref _transactionDepth); - } - - throw; - } - - return retVal; -} - -/// -/// Rolls back the transaction that was begun by or . -/// -public void Rollback () -{ - RollbackTo (null, false); -} - -/// -/// Rolls back the savepoint created by or SaveTransactionPoint. -/// -/// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to -public void RollbackTo (string savepoint) -{ - RollbackTo (savepoint, false); -} - -/// -/// Rolls back the transaction that was begun by . -/// -/// true to avoid throwing exceptions, false otherwise -void RollbackTo (string savepoint, bool noThrow) -{ - // Rolling back without a TO clause rolls backs all transactions - // and leaves the transaction stack empty. - try { - if (String.IsNullOrEmpty (savepoint)) { - if (Interlocked.Exchange (ref _transactionDepth, 0) > 0) { - Execute ("rollback"); - } - } else { - DoSavePointExecute (savepoint, "rollback to "); - } - } catch (SQLiteException) { - if (!noThrow) - throw; - - } - // No need to rollback if there are no transactions open. -} - -/// -/// Releases a savepoint returned from . Releasing a savepoint -/// makes changes since that savepoint permanent if the savepoint began the transaction, -/// or otherwise the changes are permanent pending a call to . -/// -/// The RELEASE command is like a COMMIT for a SAVEPOINT. -/// -/// The name of the savepoint to release. The string should be the result of a call to -public void Release (string savepoint) -{ - DoSavePointExecute (savepoint, "release "); -} - -void DoSavePointExecute (string savepoint, string cmd) -{ - // Validate the savepoint - int firstLen = savepoint.IndexOf ('D'); - if (firstLen >= 2 && savepoint.Length > firstLen + 1) { - int depth; - if (Int32.TryParse (savepoint.Substring (firstLen + 1), out depth)) { - // TODO: Mild race here, but inescapable without locking almost everywhere. - if (0 <= depth && depth < _transactionDepth) { - #if NETFX_CORE || USE_SQLITEPCL_RAW - Volatile.Write (ref _transactionDepth, depth); - #elif SILVERLIGHT - _transactionDepth = depth; - #else - Thread.VolatileWrite (ref _transactionDepth, depth); - #endif - Execute (cmd + savepoint); - return; - } - } - } - - throw new ArgumentException ("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); -} - -/// -/// Commits the transaction that was begun by . -/// -public void Commit () -{ - if (Interlocked.Exchange (ref _transactionDepth, 0) != 0) { - Execute ("commit"); - } - // Do nothing on a commit with no open transaction -} - -/// -/// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an -/// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception -/// is rethrown. -/// -/// -/// The to perform within a transaction. can contain any number -/// of operations on the connection but should never call or -/// . -/// -public void RunInTransaction (Action action) -{ - try { - var savePoint = SaveTransactionPoint (); - action (); - Release (savePoint); - } catch (Exception) { - Rollback (); - throw; - } -} - -/// -/// Inserts all specified objects. -/// -/// -/// An of the objects to insert. -/// -/// A boolean indicating if the inserts should be wrapped in a transaction. -/// -/// -/// The number of rows added to the table. -/// -public int InsertAll (System.Collections.IEnumerable objects, bool runInTransaction=true) -{ - var c = 0; - if (runInTransaction) { - RunInTransaction(() => { - foreach (var r in objects) { - c += Insert (r); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r); - } - } - return c; -} - -/// -/// Inserts all specified objects. -/// -/// -/// An of the objects to insert. -/// -/// -/// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... -/// -/// -/// A boolean indicating if the inserts should be wrapped in a transaction. -/// -/// -/// The number of rows added to the table. -/// -public int InsertAll (System.Collections.IEnumerable objects, string extra, bool runInTransaction=true) -{ - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, extra); - } - }); - } - else { - foreach (var r in objects) { - c+= Insert (r); - } - } - return c; -} - -/// -/// Inserts all specified objects. -/// -/// -/// An of the objects to insert. -/// -/// -/// The type of object to insert. -/// -/// -/// A boolean indicating if the inserts should be wrapped in a transaction. -/// -/// -/// The number of rows added to the table. -/// -public int InsertAll (System.Collections.IEnumerable objects, Type objType, bool runInTransaction=true) -{ - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Insert (r, objType); - } - }); - } - else { - foreach (var r in objects) { - c += Insert (r, objType); - } - } - return c; -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// -/// -/// The object to insert. -/// -/// -/// The number of rows added to the table. -/// -public int Insert (object obj) -{ - if (obj == null) { - return 0; - } - return Insert (obj, "", obj.GetType ()); -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// If a UNIQUE constraint violation occurs with -/// some pre-existing object, this function deletes -/// the old object. -/// -/// -/// The object to insert. -/// -/// -/// The number of rows modified. -/// -public int InsertOrReplace (object obj) -{ - if (obj == null) { - return 0; - } - return Insert (obj, "OR REPLACE", obj.GetType ()); -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// -/// -/// The object to insert. -/// -/// -/// The type of object to insert. -/// -/// -/// The number of rows added to the table. -/// -public int Insert (object obj, Type objType) -{ - return Insert (obj, "", objType); -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// If a UNIQUE constraint violation occurs with -/// some pre-existing object, this function deletes -/// the old object. -/// -/// -/// The object to insert. -/// -/// -/// The type of object to insert. -/// -/// -/// The number of rows modified. -/// -public int InsertOrReplace (object obj, Type objType) -{ - return Insert (obj, "OR REPLACE", objType); -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// -/// -/// The object to insert. -/// -/// -/// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... -/// -/// -/// The number of rows added to the table. -/// -public int Insert (object obj, string extra) -{ - if (obj == null) { - return 0; - } - return Insert (obj, extra, obj.GetType ()); -} - -/// -/// Inserts the given object and retrieves its -/// auto incremented primary key if it has one. -/// -/// -/// The object to insert. -/// -/// -/// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... -/// -/// -/// The type of object to insert. -/// -/// -/// The number of rows added to the table. -/// -public int Insert (object obj, string extra, Type objType) -{ - if (obj == null || objType == null) { - return 0; - } - - - var map = GetMapping (objType); - - #if USE_NEW_REFLECTION_API - if (map.PK != null && map.PK.IsAutoGuid) - { - // no GetProperty so search our way up the inheritance chain till we find it - PropertyInfo prop; - while (objType != null) - { - var info = objType.GetTypeInfo(); - prop = info.GetDeclaredProperty(map.PK.PropertyName); - if (prop != null) - { - if (prop.GetValue(obj, null).Equals(Guid.Empty)) - { - prop.SetValue(obj, Guid.NewGuid(), null); - } - break; - } - - objType = info.BaseType; - } - } - #else - if (map.PK != null && map.PK.IsAutoGuid) { - var prop = objType.GetProperty(map.PK.PropertyName); - if (prop != null) { - if (prop.GetValue(obj, null).Equals(Guid.Empty)) { - prop.SetValue(obj, Guid.NewGuid(), null); - } - } - } - #endif - - - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; - var vals = new object[cols.Length]; - for (var i = 0; i < vals.Length; i++) { - vals [i] = cols [i].GetValue (obj); - } - - var insertCmd = map.GetInsertCommand (this, extra); - int count; - - lock (insertCmd) { - // We lock here to protect the prepared statement returned via GetInsertCommand. - // A SQLite prepared statement can be bound for only one operation at a time. - try { - count = insertCmd.ExecuteNonQuery (vals); - } catch (SQLiteException ex) { - if (SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex.Result, ex.Message, map, obj); - } - throw; - } - - if (map.HasAutoIncPK) { - var id = SQLite3.LastInsertRowid (Handle); - map.SetAutoIncPK (obj, id); - } - } - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Insert); - - return count; -} - -/// -/// Updates all of the columns of a table using the specified object -/// except for its primary key. -/// The object is required to have a primary key. -/// -/// -/// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. -/// -/// -/// The number of rows updated. -/// -public int Update (object obj) -{ - if (obj == null) { - return 0; - } - return Update (obj, obj.GetType ()); -} - -/// -/// Updates all of the columns of a table using the specified object -/// except for its primary key. -/// The object is required to have a primary key. -/// -/// -/// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. -/// -/// -/// The type of object to insert. -/// -/// -/// The number of rows updated. -/// -public int Update (object obj, Type objType) -{ - int rowsAffected = 0; - if (obj == null || objType == null) { - return 0; - } - - var map = GetMapping (objType); - - var pk = map.PK; - - if (pk == null) { - throw new NotSupportedException ("Cannot update " + map.TableName + ": it has no PK"); - } - - var cols = from p in map.Columns - where p != pk - select p; - var vals = from c in cols - select c.GetValue (obj); - var ps = new List (vals); - ps.Add (pk.GetValue (obj)); - var q = string.Format ("update \"{0}\" set {1} where {2} = ? ", map.TableName, string.Join (",", (from c in cols - select "\"" + c.Name + "\" = ? ").ToArray ()), pk.Name); - - try { - rowsAffected = Execute (q, ps.ToArray ()); - } - catch (SQLiteException ex) { - - if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (ex, map, obj); - } - - throw ex; - } - - if (rowsAffected > 0) - OnTableChanged (map, NotifyTableChangedAction.Update); - - return rowsAffected; -} - -/// -/// Updates all specified objects. -/// -/// -/// An of the objects to insert. -/// -/// -/// A boolean indicating if the inserts should be wrapped in a transaction -/// -/// -/// The number of rows modified. -/// -public int UpdateAll (System.Collections.IEnumerable objects, bool runInTransaction=true) -{ - var c = 0; - if (runInTransaction) { - RunInTransaction (() => { - foreach (var r in objects) { - c += Update (r); - } - }); - } - else { - foreach (var r in objects) { - c += Update (r); - } - } - return c; -} - -/// -/// Deletes the given object from the database using its primary key. -/// -/// -/// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. -/// -/// -/// The number of rows deleted. -/// -public int Delete (object objectToDelete) -{ - var map = GetMapping (objectToDelete.GetType ()); - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, pk.GetValue (objectToDelete)); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; -} - -/// -/// Deletes the object with the specified primary key. -/// -/// -/// The primary key of the object to delete. -/// -/// -/// The number of objects deleted. -/// -/// -/// The type of object. -/// -public int Delete (object primaryKey) -{ - var map = GetMapping (typeof (T)); - var pk = map.PK; - if (pk == null) { - throw new NotSupportedException ("Cannot delete " + map.TableName + ": it has no PK"); - } - var q = string.Format ("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); - var count = Execute (q, primaryKey); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; -} - -/// -/// Deletes all the objects from the specified table. -/// WARNING WARNING: Let me repeat. It deletes ALL the objects from the -/// specified table. Do you really want to do that? -/// -/// -/// The number of objects deleted. -/// -/// -/// The type of objects to delete. -/// -public int DeleteAll () -{ - var map = GetMapping (typeof (T)); - var query = string.Format("delete from \"{0}\"", map.TableName); - var count = Execute (query); - if (count > 0) - OnTableChanged (map, NotifyTableChangedAction.Delete); - return count; -} - -~SQLiteConnection () -{ - Dispose (false); -} - -public void Dispose () -{ - Dispose(true); - GC.SuppressFinalize(this); -} - -public void Close() -{ - Dispose(true); -} - -protected virtual void Dispose(bool disposing) -{ - if (_open && Handle != NullHandle) { - try { - if (disposing) { - if (_mappings != null) { - foreach (var sqlInsertCommand in _mappings.Values){ - sqlInsertCommand.Dispose(); - } - } - - var r = SQLite3.Close(Handle); - if (r != SQLite3.Result.OK) - { - string msg = SQLite3.GetErrmsg(Handle); - throw SQLiteException.New(r, msg); - } - } else { - SQLite3.Close2(Handle); - } - } - finally { - Handle = NullHandle; - _open = false; - } - } -} - -void OnTableChanged (TableMapping table, NotifyTableChangedAction action) -{ - var ev = TableChanged; - if (ev != null) - ev (this, new NotifyTableChangedEventArgs (table, action)); -} - -public event EventHandler TableChanged; -} - -public class NotifyTableChangedEventArgs : EventArgs -{ - public TableMapping Table { get; private set; } - public NotifyTableChangedAction Action { get; private set; } - - public NotifyTableChangedEventArgs (TableMapping table, NotifyTableChangedAction action) - { - Table = table; - Action = action; - } -} - -public enum NotifyTableChangedAction -{ - Insert, - Update, - Delete, -} - -/// -/// Represents a parsed connection string. -/// -class SQLiteConnectionString -{ - public string ConnectionString { get; private set; } - public string DatabasePath { get; private set; } - public bool StoreDateTimeAsTicks { get; private set; } - - #if NETFX_CORE - static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; - #endif - - public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks) - { - ConnectionString = databasePath; - StoreDateTimeAsTicks = storeDateTimeAsTicks; - - #if NETFX_CORE - DatabasePath = System.IO.Path.Combine (MetroStyleDataPath, databasePath); - #else - DatabasePath = databasePath; - #endif - } -} - -[AttributeUsage (AttributeTargets.Class)] -public class TableAttribute : Attribute -{ - public string Name { get; set; } - - public TableAttribute (string name) - { - Name = name; - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class ColumnAttribute : Attribute -{ - public string Name { get; set; } - - public ColumnAttribute (string name) - { - Name = name; - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class PrimaryKeyAttribute : Attribute -{ -} - -[AttributeUsage (AttributeTargets.Property)] -public class AutoIncrementAttribute : Attribute -{ -} - -[AttributeUsage (AttributeTargets.Property)] -public class IndexedAttribute : Attribute -{ - public string Name { get; set; } - public int Order { get; set; } - public virtual bool Unique { get; set; } - - public IndexedAttribute() - { - } - - public IndexedAttribute(string name, int order) - { - Name = name; - Order = order; - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class IgnoreAttribute : Attribute -{ -} - -[AttributeUsage (AttributeTargets.Property)] -public class UniqueAttribute : IndexedAttribute -{ - public override bool Unique { - get { return true; } - set { /* throw? */ } - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class MaxLengthAttribute : Attribute -{ - public int Value { get; private set; } - - public MaxLengthAttribute (int length) - { - Value = length; - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class CollationAttribute: Attribute -{ - public string Value { get; private set; } - - public CollationAttribute (string collation) - { - Value = collation; - } -} - -[AttributeUsage (AttributeTargets.Property)] -public class NotNullAttribute : Attribute -{ -} - -public class TableMapping -{ - public Type MappedType { get; private set; } - - public string TableName { get; private set; } - - public Column[] Columns { get; private set; } - - public Column PK { get; private set; } - - public string GetByPrimaryKeySql { get; private set; } - - Column _autoPk; - Column[] _insertColumns; - Column[] _insertOrReplaceColumns; - - public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) - { - MappedType = type; - - #if USE_NEW_REFLECTION_API - var tableAttr = (TableAttribute)System.Reflection.CustomAttributeExtensions - .GetCustomAttribute(type.GetTypeInfo(), typeof(TableAttribute), true); - #else - var tableAttr = (TableAttribute)type.GetCustomAttributes (typeof (TableAttribute), true).FirstOrDefault (); - #endif - - TableName = tableAttr != null ? tableAttr.Name : MappedType.Name; - - #if !USE_NEW_REFLECTION_API - var props = MappedType.GetProperties (BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); - #else - var props = from p in MappedType.GetRuntimeProperties() - where ((p.GetMethod != null && p.GetMethod.IsPublic) || (p.SetMethod != null && p.SetMethod.IsPublic) || (p.GetMethod != null && p.GetMethod.IsStatic) || (p.SetMethod != null && p.SetMethod.IsStatic)) - select p; - #endif - var cols = new List (); - foreach (var p in props) { - #if !USE_NEW_REFLECTION_API - var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Length > 0; - #else - var ignore = p.GetCustomAttributes (typeof(IgnoreAttribute), true).Count() > 0; - #endif - if (p.CanWrite && !ignore) { - cols.Add (new Column (p, createFlags)); - } - } - Columns = cols.ToArray (); - foreach (var c in Columns) { - if (c.IsAutoInc && c.IsPK) { - _autoPk = c; - } - if (c.IsPK) { - PK = c; - } - } - - HasAutoIncPK = _autoPk != null; - - if (PK != null) { - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); - } - else { - // People should not be calling Get/Find without a PK - GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); - } - _insertCommandMap = new ConcurrentStringDictionary (); - } - - public bool HasAutoIncPK { get; private set; } - - public void SetAutoIncPK (object obj, long id) - { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); - } - } - - public Column[] InsertColumns { - get { - if (_insertColumns == null) { - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - } - return _insertColumns; - } - } - - public Column[] InsertOrReplaceColumns { - get { - if (_insertOrReplaceColumns == null) { - _insertOrReplaceColumns = Columns.ToArray (); - } - return _insertOrReplaceColumns; - } - } - - public Column FindColumnWithPropertyName (string propertyName) - { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; - } - - public Column FindColumn (string columnName) - { - var exact = Columns.FirstOrDefault (c => c.Name == columnName); - return exact; - } - - ConcurrentStringDictionary _insertCommandMap; - - public PreparedSqlLiteInsertCommand GetInsertCommand(SQLiteConnection conn, string extra) - { - object prepCmdO; - - if (!_insertCommandMap.TryGetValue (extra, out prepCmdO)) { - var prepCmd = CreateInsertCommand (conn, extra); - prepCmdO = prepCmd; - if (!_insertCommandMap.TryAdd (extra, prepCmd)) { - // Concurrent add attempt beat us. - prepCmd.Dispose (); - _insertCommandMap.TryGetValue (extra, out prepCmdO); - } - } - return (PreparedSqlLiteInsertCommand)prepCmdO; - } - - PreparedSqlLiteInsertCommand CreateInsertCommand(SQLiteConnection conn, string extra) - { - var cols = InsertColumns; - string insertSql; - if (!cols.Any() && Columns.Count() == 1 && Columns[0].IsAutoInc) - { - insertSql = string.Format("insert {1} into \"{0}\" default values", TableName, extra); - } - else - { - var replacing = string.Compare (extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; - - if (replacing) { - cols = InsertOrReplaceColumns; - } - - insertSql = string.Format("insert {3} into \"{0}\"({1}) values ({2})", TableName, - string.Join(",", (from c in cols - select "\"" + c.Name + "\"").ToArray()), - string.Join(",", (from c in cols - select "?").ToArray()), extra); - - } - - var insertCommand = new PreparedSqlLiteInsertCommand(conn); - insertCommand.CommandText = insertSql; - return insertCommand; - } - - protected internal void Dispose() - { - foreach (var pair in _insertCommandMap) { - ((PreparedSqlLiteInsertCommand)pair.Value).Dispose (); - } - _insertCommandMap = null; - } - - public class Column - { - PropertyInfo _prop; - - public string Name { get; private set; } - - public string PropertyName { get { return _prop.Name; } } - - public Type ColumnType { get; private set; } - - public string Collation { get; private set; } - - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } - - public bool IsPK { get; private set; } - - public IEnumerable Indices { get; set; } - - public bool IsNullable { get; private set; } - - public int? MaxStringLength { get; private set; } - - public Column(PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) - { - var colAttr = (ColumnAttribute)prop.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault(); - - _prop = prop; - Name = colAttr == null ? prop.Name : colAttr.Name; - //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead - ColumnType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; - Collation = Orm.Collation(prop); - - IsPK = Orm.IsPK(prop) || - (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && - string.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - - var isAuto = Orm.IsAutoInc(prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof(Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices(prop); - if (!Indices.Any() - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) - { - Indices = new IndexedAttribute[] { new IndexedAttribute() }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull(prop)); - MaxStringLength = Orm.MaxStringLength(prop); - } - - public void SetValue (object obj, object val) - { - _prop.SetValue (obj, val, null); - } - - public object GetValue (object obj) - { - return _prop.GetValue (obj, null); - } - } -} - -public static class Orm -{ - public const int DefaultMaxStringLength = 140; - public const string ImplicitPkName = "Id"; - public const string ImplicitIndexSuffix = "Id"; - - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) - { - string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks) + " "; - - if (p.IsPK) { - decl += "primary key "; - } - if (p.IsAutoInc) { - decl += "autoincrement "; - } - if (!p.IsNullable) { - decl += "not null "; - } - if (!string.IsNullOrEmpty (p.Collation)) { - decl += "collate " + p.Collation + " "; - } - - return decl; - } - - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) - { - var clrType = p.ColumnType; - if (clrType == typeof(Boolean) || clrType == typeof(Byte) || clrType == typeof(UInt16) || clrType == typeof(SByte) || clrType == typeof(Int16) || clrType == typeof(Int32) || clrType == typeof(UInt32) || clrType == typeof(Int64)) - { - return "integer"; - } else if (clrType == typeof(Single) || clrType == typeof(Double) || clrType == typeof(Decimal)) { - return "float"; - } else if (clrType == typeof(String)) { - int? len = p.MaxStringLength; - - if (len.HasValue) - return "varchar(" + len.Value + ")"; - - return "varchar"; - } else if (clrType == typeof(TimeSpan)) { - return "bigint"; - } else if (clrType == typeof(DateTime)) { - return storeDateTimeAsTicks ? "bigint" : "datetime"; - } else if (clrType == typeof(DateTimeOffset)) { - return "bigint"; - #if !USE_NEW_REFLECTION_API - } else if (clrType.IsEnum) { - #else - } else if (clrType.GetTypeInfo().IsEnum) { - #endif - return "integer"; - } else if (clrType == typeof(byte[])) { - return "blob"; - } else if (clrType == typeof(Guid)) { - return "varchar(36)"; - } else { - throw new NotSupportedException ("Don't know about " + clrType); - } - } - - public static bool IsPK (MemberInfo p) - { - var attrs = p.GetCustomAttributes (typeof(PrimaryKeyAttribute), true); - #if !USE_NEW_REFLECTION_API - return attrs.Length > 0; - #else - return attrs.Count() > 0; - #endif - } - - public static string Collation (MemberInfo p) - { - var attrs = p.GetCustomAttributes (typeof(CollationAttribute), true); - #if !USE_NEW_REFLECTION_API - if (attrs.Length > 0) { - return ((CollationAttribute)attrs [0]).Value; - #else - if (attrs.Count() > 0) { - return ((CollationAttribute)attrs.First()).Value; - #endif - } else { - return string.Empty; - } - } - - public static bool IsAutoInc (MemberInfo p) - { - var attrs = p.GetCustomAttributes (typeof(AutoIncrementAttribute), true); - #if !USE_NEW_REFLECTION_API - return attrs.Length > 0; - #else - return attrs.Count() > 0; - #endif - } - - public static IEnumerable GetIndices(MemberInfo p) - { - var attrs = p.GetCustomAttributes(typeof(IndexedAttribute), true); - return attrs.Cast(); - } - - public static int? MaxStringLength(PropertyInfo p) - { - var attrs = p.GetCustomAttributes (typeof(MaxLengthAttribute), true); - #if !USE_NEW_REFLECTION_API - if (attrs.Length > 0) - return ((MaxLengthAttribute)attrs [0]).Value; - #else - if (attrs.Count() > 0) - return ((MaxLengthAttribute)attrs.First()).Value; - #endif - - return null; - } - - public static bool IsMarkedNotNull(MemberInfo p) - { - var attrs = p.GetCustomAttributes (typeof (NotNullAttribute), true); - #if !USE_NEW_REFLECTION_API - return attrs.Length > 0; - #else - return attrs.Count() > 0; - #endif - } -} - -public partial class SQLiteCommand -{ - SQLiteConnection _conn; - private List _bindings; - - public string CommandText { get; set; } - - internal SQLiteCommand (SQLiteConnection conn) - { - _conn = conn; - _bindings = new List (); - CommandText = ""; - } - - public int ExecuteNonQuery () - { - if (_conn.Trace) { - Debug.WriteLine ("Executing: " + this); - } - - var r = SQLite3.Result.OK; - var stmt = Prepare (); - r = SQLite3.Step (stmt); - Finalize (stmt); - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (_conn.Handle); - return rowsAffected; - } else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (_conn.Handle); - throw SQLiteException.New (r, msg); - } - else if (r == SQLite3.Result.Constraint) { - if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - - throw SQLiteException.New(r, r.ToString()); - } - - public IEnumerable ExecuteDeferredQuery () - { - return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))); - } - - public List ExecuteQuery () - { - return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))).ToList(); - } - - public List ExecuteQuery (TableMapping map) - { - return ExecuteDeferredQuery(map).ToList(); - } - - /// - /// Invoked every time an instance is loaded from the database. - /// - /// - /// The newly created object. - /// - /// - /// This can be overridden in combination with the - /// method to hook into the life-cycle of objects. - /// - /// Type safety is not possible because MonoTouch does not support virtual generic methods. - /// - protected virtual void OnInstanceCreated (object obj) - { - // Can be overridden. - } - - public IEnumerable ExecuteDeferredQuery (TableMapping map) - { - if (_conn.Trace) { - Debug.WriteLine ("Executing Query: " + this); - } - - var stmt = Prepare (); - try - { - var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; - - for (int i = 0; i < cols.Length; i++) { - var name = SQLite3.ColumnName16 (stmt, i); - cols [i] = map.FindColumn (name); - } - - while (SQLite3.Step (stmt) == SQLite3.Result.Row) { - var obj = Activator.CreateInstance(map.MappedType); - for (int i = 0; i < cols.Length; i++) { - if (cols [i] == null) - continue; - var colType = SQLite3.ColumnType (stmt, i); - var val = ReadCol (stmt, i, colType, cols [i].ColumnType); - cols [i].SetValue (obj, val); - } - OnInstanceCreated (obj); - yield return (T)obj; - } - } - finally - { - SQLite3.Finalize(stmt); - } - } - - public T ExecuteScalar () - { - if (_conn.Trace) { - Debug.WriteLine ("Executing Query: " + this); - } - - T val = default(T); - - var stmt = Prepare (); - - try - { - var r = SQLite3.Step (stmt); - if (r == SQLite3.Result.Row) { - var colType = SQLite3.ColumnType (stmt, 0); - val = (T)ReadCol (stmt, 0, colType, typeof(T)); - } - else if (r == SQLite3.Result.Done) { - } - else - { - throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle)); - } - } - finally - { - Finalize (stmt); - } - - return val; - } - - public void Bind (string name, object val) - { - _bindings.Add (new Binding { - Name = name, - Value = val - }); - } - - public void Bind (object val) - { - Bind (null, val); - } - - public override string ToString () - { - var parts = new string[1 + _bindings.Count]; - parts [0] = CommandText; - var i = 1; - foreach (var b in _bindings) { - parts [i] = string.Format (" {0}: {1}", i - 1, b.Value); - i++; - } - return string.Join (Environment.NewLine, parts); - } - - Sqlite3Statement Prepare() - { - var stmt = SQLite3.Prepare2 (_conn.Handle, CommandText); - BindAll (stmt); - return stmt; - } - - void Finalize (Sqlite3Statement stmt) - { - SQLite3.Finalize (stmt); - } - - void BindAll (Sqlite3Statement stmt) - { - int nextIdx = 1; - foreach (var b in _bindings) { - if (b.Name != null) { - b.Index = SQLite3.BindParameterIndex (stmt, b.Name); - } else { - b.Index = nextIdx++; - } - - BindParameter (stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks); - } - } - - internal static IntPtr NegativePointer = new IntPtr (-1); - - internal static void BindParameter (Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks) - { - if (value == null) { - SQLite3.BindNull (stmt, index); - } else { - if (value is Int32) { - SQLite3.BindInt (stmt, index, (int)value); - } else if (value is String) { - SQLite3.BindText (stmt, index, (string)value, -1, NegativePointer); - } else if (value is Byte || value is UInt16 || value is SByte || value is Int16) { - SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); - } else if (value is Boolean) { - SQLite3.BindInt (stmt, index, (bool)value ? 1 : 0); - } else if (value is UInt32 || value is Int64) { - SQLite3.BindInt64 (stmt, index, Convert.ToInt64 (value)); - } else if (value is Single || value is Double || value is Decimal) { - SQLite3.BindDouble (stmt, index, Convert.ToDouble (value)); - } else if (value is TimeSpan) { - SQLite3.BindInt64(stmt, index, ((TimeSpan)value).Ticks); - } else if (value is DateTime) { - if (storeDateTimeAsTicks) { - SQLite3.BindInt64 (stmt, index, ((DateTime)value).Ticks); - } - else { - SQLite3.BindText (stmt, index, ((DateTime)value).ToString ("yyyy-MM-dd HH:mm:ss"), -1, NegativePointer); - } - } else if (value is DateTimeOffset) { - SQLite3.BindInt64 (stmt, index, ((DateTimeOffset)value).UtcTicks); - #if !USE_NEW_REFLECTION_API - } else if (value.GetType().IsEnum) { - #else - } else if (value.GetType().GetTypeInfo().IsEnum) { - #endif - SQLite3.BindInt (stmt, index, Convert.ToInt32 (value)); - } else if (value is byte[]){ - SQLite3.BindBlob(stmt, index, (byte[]) value, ((byte[]) value).Length, NegativePointer); - } else if (value is Guid) { - SQLite3.BindText(stmt, index, ((Guid)value).ToString(), 72, NegativePointer); - } else { - throw new NotSupportedException("Cannot store type: " + value.GetType()); - } - } - } - - class Binding - { - public string Name { get; set; } - - public object Value { get; set; } - - public int Index { get; set; } - } - - object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) - { - if (type == SQLite3.ColType.Null) { - return null; - } else { - if (clrType == typeof(String)) { - return SQLite3.ColumnString (stmt, index); - } else if (clrType == typeof(Int32)) { - return (int)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Boolean)) { - return SQLite3.ColumnInt (stmt, index) == 1; - } else if (clrType == typeof(double)) { - return SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(float)) { - return (float)SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(TimeSpan)) { - return new TimeSpan(SQLite3.ColumnInt64(stmt, index)); - } else if (clrType == typeof(DateTime)) { - if (_conn.StoreDateTimeAsTicks) { - return new DateTime (SQLite3.ColumnInt64 (stmt, index)); - } - else { - var text = SQLite3.ColumnString (stmt, index); - return DateTime.Parse (text); - } - } else if (clrType == typeof(DateTimeOffset)) { - return new DateTimeOffset(SQLite3.ColumnInt64 (stmt, index),TimeSpan.Zero); - #if !USE_NEW_REFLECTION_API - } else if (clrType.IsEnum) { - #else - } else if (clrType.GetTypeInfo().IsEnum) { - #endif - return SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Int64)) { - return SQLite3.ColumnInt64 (stmt, index); - } else if (clrType == typeof(UInt32)) { - return (uint)SQLite3.ColumnInt64 (stmt, index); - } else if (clrType == typeof(decimal)) { - return (decimal)SQLite3.ColumnDouble (stmt, index); - } else if (clrType == typeof(Byte)) { - return (byte)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(UInt16)) { - return (ushort)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(Int16)) { - return (short)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(sbyte)) { - return (sbyte)SQLite3.ColumnInt (stmt, index); - } else if (clrType == typeof(byte[])) { - return SQLite3.ColumnByteArray (stmt, index); - } else if (clrType == typeof(Guid)) { - var text = SQLite3.ColumnString(stmt, index); - return new Guid(text); - } else{ - throw new NotSupportedException ("Don't know how to read " + clrType); - } - } - } -} - -/// -/// Since the insert never changed, we only need to prepare once. -/// -public class PreparedSqlLiteInsertCommand : IDisposable -{ - public bool Initialized { get; set; } - - protected SQLiteConnection Connection { get; set; } - - public string CommandText { get; set; } - - protected Sqlite3Statement Statement { get; set; } - internal static readonly Sqlite3Statement NullStatement = default(Sqlite3Statement); - - internal PreparedSqlLiteInsertCommand (SQLiteConnection conn) - { - Connection = conn; - } - - public int ExecuteNonQuery (object[] source) - { - if (Connection.Trace) { - Debug.WriteLine ("Executing: " + CommandText); - } - - var r = SQLite3.Result.OK; - - if (!Initialized) { - Statement = Prepare (); - Initialized = true; - } - - //bind the values. - if (source != null) { - for (int i = 0; i < source.Length; i++) { - SQLiteCommand.BindParameter (Statement, i + 1, source [i], Connection.StoreDateTimeAsTicks); - } - } - r = SQLite3.Step (Statement); - - if (r == SQLite3.Result.Done) { - int rowsAffected = SQLite3.Changes (Connection.Handle); - SQLite3.Reset (Statement); - return rowsAffected; - } else if (r == SQLite3.Result.Error) { - string msg = SQLite3.GetErrmsg (Connection.Handle); - SQLite3.Reset (Statement); - throw SQLiteException.New (r, msg); - } else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) { - SQLite3.Reset (Statement); - throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); - } else { - SQLite3.Reset (Statement); - throw SQLiteException.New (r, r.ToString ()); - } - } - - protected virtual Sqlite3Statement Prepare () - { - var stmt = SQLite3.Prepare2 (Connection.Handle, CommandText); - return stmt; - } - - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - } - - private void Dispose (bool disposing) - { - if (Statement != NullStatement) { - try { - SQLite3.Finalize (Statement); - } finally { - Statement = NullStatement; - Connection = null; - } - } - } - - ~PreparedSqlLiteInsertCommand () - { - Dispose (false); - } -} - -public abstract class BaseTableQuery -{ - protected class Ordering - { - public string ColumnName { get; set; } - public bool Ascending { get; set; } - } -} - -public class TableQuery : BaseTableQuery, IEnumerable -{ - public SQLiteConnection Connection { get; private set; } - - public TableMapping Table { get; private set; } - - Expression _where; - List _orderBys; - int? _limit; - int? _offset; - - BaseTableQuery _joinInner; - Expression _joinInnerKeySelector; - BaseTableQuery _joinOuter; - Expression _joinOuterKeySelector; - Expression _joinSelector; - - Expression _selector; - - TableQuery (SQLiteConnection conn, TableMapping table) - { - Connection = conn; - Table = table; - } - - public TableQuery (SQLiteConnection conn) - { - Connection = conn; - Table = Connection.GetMapping (typeof(T)); - } - - public TableQuery Clone () - { - var q = new TableQuery (Connection, Table); - q._where = _where; - q._deferred = _deferred; - if (_orderBys != null) { - q._orderBys = new List (_orderBys); - } - q._limit = _limit; - q._offset = _offset; - q._joinInner = _joinInner; - q._joinInnerKeySelector = _joinInnerKeySelector; - q._joinOuter = _joinOuter; - q._joinOuterKeySelector = _joinOuterKeySelector; - q._joinSelector = _joinSelector; - q._selector = _selector; - return q; - } - - public TableQuery Where (Expression> predExpr) - { - if (predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - var pred = lambda.Body; - var q = Clone (); - q.AddWhere (pred); - return q; - } else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - public int Delete(Expression> predExpr) - { - if (predExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)predExpr; - var pred = lambda.Body; - var args = new List (); - var w = CompileExpr (pred, args); - var cmdText = "delete from \"" + Table.TableName + "\""; - cmdText += " where " + w.CommandText; - var command = Connection.CreateCommand (cmdText, args.ToArray ()); - - int result = command.ExecuteNonQuery(); - return result; - } else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - public TableQuery Take (int n) - { - var q = Clone (); - q._limit = n; - return q; - } - - public TableQuery Skip (int n) - { - var q = Clone (); - q._offset = n; - return q; - } - - public T ElementAt (int index) - { - return Skip (index).Take (1).First (); - } - - bool _deferred; - public TableQuery Deferred () - { - var q = Clone (); - q._deferred = true; - return q; - } - - public TableQuery OrderBy (Expression> orderExpr) - { - return AddOrderBy (orderExpr, true); - } - - public TableQuery OrderByDescending (Expression> orderExpr) - { - return AddOrderBy (orderExpr, false); - } - - public TableQuery ThenBy(Expression> orderExpr) - { - return AddOrderBy(orderExpr, true); - } - - public TableQuery ThenByDescending(Expression> orderExpr) - { - return AddOrderBy(orderExpr, false); - } - - private TableQuery AddOrderBy (Expression> orderExpr, bool asc) - { - if (orderExpr.NodeType == ExpressionType.Lambda) { - var lambda = (LambdaExpression)orderExpr; - - MemberExpression mem = null; - - var unary = lambda.Body as UnaryExpression; - if (unary != null && unary.NodeType == ExpressionType.Convert) { - mem = unary.Operand as MemberExpression; - } - else { - mem = lambda.Body as MemberExpression; - } - - if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) { - var q = Clone (); - if (q._orderBys == null) { - q._orderBys = new List (); - } - q._orderBys.Add (new Ordering { - ColumnName = Table.FindColumnWithPropertyName(mem.Member.Name).Name, - Ascending = asc - }); - return q; - } else { - throw new NotSupportedException ("Order By does not support: " + orderExpr); - } - } else { - throw new NotSupportedException ("Must be a predicate"); - } - } - - private void AddWhere (Expression pred) - { - if (_where == null) { - _where = pred; - } else { - _where = Expression.AndAlso (_where, pred); - } - } - - public TableQuery Join ( - TableQuery inner, - Expression> outerKeySelector, - Expression> innerKeySelector, - Expression> resultSelector) - { - var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { - _joinOuter = this, - _joinOuterKeySelector = outerKeySelector, - _joinInner = inner, - _joinInnerKeySelector = innerKeySelector, - _joinSelector = resultSelector, - }; - return q; - } - - public TableQuery Select (Expression> selector) - { - var q = Clone (); - q._selector = selector; - return q; - } - - private SQLiteCommand GenerateCommand (string selectionList) - { - if (_joinInner != null && _joinOuter != null) { - throw new NotSupportedException ("Joins are not supported."); - } - else { - var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; - var args = new List (); - if (_where != null) { - var w = CompileExpr (_where, args); - cmdText += " where " + w.CommandText; - } - if ((_orderBys != null) && (_orderBys.Count > 0)) { - var t = string.Join (", ", _orderBys.Select (o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray ()); - cmdText += " order by " + t; - } - if (_limit.HasValue) { - cmdText += " limit " + _limit.Value; - } - if (_offset.HasValue) { - if (!_limit.HasValue) { - cmdText += " limit -1 "; - } - cmdText += " offset " + _offset.Value; - } - return Connection.CreateCommand (cmdText, args.ToArray ()); - } - } - - class CompileResult - { - public string CommandText { get; set; } - - public object Value { get; set; } - } - - private CompileResult CompileExpr (Expression expr, List queryArgs) - { - if (expr == null) { - throw new NotSupportedException ("Expression is NULL"); - } else if (expr is BinaryExpression) { - var bin = (BinaryExpression)expr; - - // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it - // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx - if (bin.Left.NodeType == ExpressionType.Call) { - var call = (MethodCallExpression)bin.Left; - if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" - && call.Method.Name == "CompareString") - bin = Expression.MakeBinary(bin.NodeType, call.Arguments[0], call.Arguments[1]); - } - - - var leftr = CompileExpr (bin.Left, queryArgs); - var rightr = CompileExpr (bin.Right, queryArgs); - - //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") - string text; - if (leftr.CommandText == "?" && leftr.Value == null) - text = CompileNullBinaryExpression(bin, rightr); - else if (rightr.CommandText == "?" && rightr.Value == null) - text = CompileNullBinaryExpression(bin, leftr); - else - text = "(" + leftr.CommandText + " " + GetSqlName(bin) + " " + rightr.CommandText + ")"; - return new CompileResult { CommandText = text }; - } else if (expr.NodeType == ExpressionType.Not) { - var operandExpr = ((UnaryExpression)expr).Operand; - var opr = CompileExpr(operandExpr, queryArgs); - object val = opr.Value; - if (val is bool) - val = !((bool) val); - return new CompileResult - { - CommandText = "NOT(" + opr.CommandText + ")", - Value = val - }; - } else if (expr.NodeType == ExpressionType.Call) { - - var call = (MethodCallExpression)expr; - var args = new CompileResult[call.Arguments.Count]; - var obj = call.Object != null ? CompileExpr (call.Object, queryArgs) : null; - - for (var i = 0; i < args.Length; i++) { - args [i] = CompileExpr (call.Arguments [i], queryArgs); - } - - var sqlCall = ""; - - if (call.Method.Name == "Like" && args.Length == 2) { - sqlCall = "(" + args [0].CommandText + " like " + args [1].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 2) { - sqlCall = "(" + args [1].CommandText + " in " + args [0].CommandText + ")"; - } - else if (call.Method.Name == "Contains" && args.Length == 1) { - if (call.Object != null && call.Object.Type == typeof(string)) { - sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + " || '%'))"; - } - else { - sqlCall = "(" + args [0].CommandText + " in " + obj.CommandText + ")"; - } - } - else if (call.Method.Name == "StartsWith" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " like (" + args [0].CommandText + " || '%'))"; - } - else if (call.Method.Name == "EndsWith" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " like ('%' || " + args [0].CommandText + "))"; - } - else if (call.Method.Name == "Equals" && args.Length == 1) { - sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; - } else if (call.Method.Name == "ToLower") { - sqlCall = "(lower(" + obj.CommandText + "))"; - } else if (call.Method.Name == "ToUpper") { - sqlCall = "(upper(" + obj.CommandText + "))"; - } else { - sqlCall = call.Method.Name.ToLower () + "(" + string.Join (",", args.Select (a => a.CommandText).ToArray ()) + ")"; - } - return new CompileResult { CommandText = sqlCall }; - - } else if (expr.NodeType == ExpressionType.Constant) { - var c = (ConstantExpression)expr; - queryArgs.Add (c.Value); - return new CompileResult { - CommandText = "?", - Value = c.Value - }; - } else if (expr.NodeType == ExpressionType.Convert) { - var u = (UnaryExpression)expr; - var ty = u.Type; - var valr = CompileExpr (u.Operand, queryArgs); - return new CompileResult { - CommandText = valr.CommandText, - Value = valr.Value != null ? ConvertTo (valr.Value, ty) : null - }; - } else if (expr.NodeType == ExpressionType.MemberAccess) { - var mem = (MemberExpression)expr; - - if (mem.Expression!=null && mem.Expression.NodeType == ExpressionType.Parameter) { - // - // This is a column of our table, output just the column name - // Need to translate it if that column name is mapped - // - var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name; - return new CompileResult { CommandText = "\"" + columnName + "\"" }; - } else { - object obj = null; - if (mem.Expression != null) { - var r = CompileExpr (mem.Expression, queryArgs); - if (r.Value == null) { - throw new NotSupportedException ("Member access failed to compile expression"); - } - if (r.CommandText == "?") { - queryArgs.RemoveAt (queryArgs.Count - 1); - } - obj = r.Value; - } - - // - // Get the member value - // - object val = null; - - #if !USE_NEW_REFLECTION_API - if (mem.Member.MemberType == MemberTypes.Property) { - #else - if (mem.Member is PropertyInfo) { - #endif - var m = (PropertyInfo)mem.Member; - val = m.GetValue (obj, null); - #if !USE_NEW_REFLECTION_API - } else if (mem.Member.MemberType == MemberTypes.Field) { - #else - } else if (mem.Member is FieldInfo) { - #endif - #if SILVERLIGHT - val = Expression.Lambda (expr).Compile ().DynamicInvoke (); - #else - var m = (FieldInfo)mem.Member; - val = m.GetValue (obj); - #endif - } else { - #if !USE_NEW_REFLECTION_API - throw new NotSupportedException ("MemberExpr: " + mem.Member.MemberType); - #else - throw new NotSupportedException ("MemberExpr: " + mem.Member.DeclaringType); - #endif - } - - // - // Work special magic for enumerables - // - if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) { - var sb = new System.Text.StringBuilder(); - sb.Append("("); - var head = ""; - foreach (var a in (System.Collections.IEnumerable)val) { - queryArgs.Add(a); - sb.Append(head); - sb.Append("?"); - head = ","; - } - sb.Append(")"); - return new CompileResult { - CommandText = sb.ToString(), - Value = val - }; - } - else { - queryArgs.Add (val); - return new CompileResult { - CommandText = "?", - Value = val - }; - } - } - } - throw new NotSupportedException ("Cannot compile: " + expr.NodeType.ToString ()); - } - - static object ConvertTo (object obj, Type t) - { - Type nut = Nullable.GetUnderlyingType(t); - - if (nut != null) { - if (obj == null) return null; - return Convert.ChangeType (obj, nut); - } else { - return Convert.ChangeType (obj, t); - } - } - - /// - /// Compiles a BinaryExpression where one of the parameters is null. - /// - /// The non-null parameter - private string CompileNullBinaryExpression(BinaryExpression expression, CompileResult parameter) - { - if (expression.NodeType == ExpressionType.Equal) - return "(" + parameter.CommandText + " is ?)"; - else if (expression.NodeType == ExpressionType.NotEqual) - return "(" + parameter.CommandText + " is not ?)"; - else - throw new NotSupportedException("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString()); - } - - string GetSqlName (Expression expr) - { - var n = expr.NodeType; - if (n == ExpressionType.GreaterThan) - return ">"; else if (n == ExpressionType.GreaterThanOrEqual) { - return ">="; - } else if (n == ExpressionType.LessThan) { - return "<"; - } else if (n == ExpressionType.LessThanOrEqual) { - return "<="; - } else if (n == ExpressionType.And) { - return "&"; - } else if (n == ExpressionType.AndAlso) { - return "and"; - } else if (n == ExpressionType.Or) { - return "|"; - } else if (n == ExpressionType.OrElse) { - return "or"; - } else if (n == ExpressionType.Equal) { - return "="; - } else if (n == ExpressionType.NotEqual) { - return "!="; - } else { - throw new NotSupportedException ("Cannot get SQL for: " + n); - } - } - - public int Count () - { - return GenerateCommand("count(*)").ExecuteScalar (); - } - - public int Count (Expression> predExpr) - { - return Where (predExpr).Count (); - } - - public IEnumerator GetEnumerator () - { - if (!_deferred) - return GenerateCommand("*").ExecuteQuery().GetEnumerator(); - - return GenerateCommand("*").ExecuteDeferredQuery().GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - - public T First () - { - var query = Take (1); - return query.ToList().First (); - } - - public T FirstOrDefault () - { - var query = Take (1); - return query.ToList().FirstOrDefault (); - } -} - -public static class SQLite3 -{ - public enum Result : int - { - OK = 0, - Error = 1, - Internal = 2, - Perm = 3, - Abort = 4, - Busy = 5, - Locked = 6, - NoMem = 7, - ReadOnly = 8, - Interrupt = 9, - IOError = 10, - Corrupt = 11, - NotFound = 12, - Full = 13, - CannotOpen = 14, - LockErr = 15, - Empty = 16, - SchemaChngd = 17, - TooBig = 18, - Constraint = 19, - Mismatch = 20, - Misuse = 21, - NotImplementedLFS = 22, - AccessDenied = 23, - Format = 24, - Range = 25, - NonDBFile = 26, - Notice = 27, - Warning = 28, - Row = 100, - Done = 101 - } - - public enum ExtendedResult : int - { - IOErrorRead = (Result.IOError | (1 << 8)), - IOErrorShortRead = (Result.IOError | (2 << 8)), - IOErrorWrite = (Result.IOError | (3 << 8)), - IOErrorFsync = (Result.IOError | (4 << 8)), - IOErrorDirFSync = (Result.IOError | (5 << 8)), - IOErrorTruncate = (Result.IOError | (6 << 8)), - IOErrorFStat = (Result.IOError | (7 << 8)), - IOErrorUnlock = (Result.IOError | (8 << 8)), - IOErrorRdlock = (Result.IOError | (9 << 8)), - IOErrorDelete = (Result.IOError | (10 << 8)), - IOErrorBlocked = (Result.IOError | (11 << 8)), - IOErrorNoMem = (Result.IOError | (12 << 8)), - IOErrorAccess = (Result.IOError | (13 << 8)), - IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), - IOErrorLock = (Result.IOError | (15 << 8)), - IOErrorClose = (Result.IOError | (16 << 8)), - IOErrorDirClose = (Result.IOError | (17 << 8)), - IOErrorSHMOpen = (Result.IOError | (18 << 8)), - IOErrorSHMSize = (Result.IOError | (19 << 8)), - IOErrorSHMLock = (Result.IOError | (20 << 8)), - IOErrorSHMMap = (Result.IOError | (21 << 8)), - IOErrorSeek = (Result.IOError | (22 << 8)), - IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), - IOErrorMMap = (Result.IOError | (24 << 8)), - LockedSharedcache = (Result.Locked | (1 << 8)), - BusyRecovery = (Result.Busy | (1 << 8)), - CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), - CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), - CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), - CorruptVTab = (Result.Corrupt | (1 << 8)), - ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), - ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), - ReadonlyRollback = (Result.ReadOnly | (3 << 8)), - AbortRollback = (Result.Abort | (2 << 8)), - ConstraintCheck = (Result.Constraint | (1 << 8)), - ConstraintCommitHook = (Result.Constraint | (2 << 8)), - ConstraintForeignKey = (Result.Constraint | (3 << 8)), - ConstraintFunction = (Result.Constraint | (4 << 8)), - ConstraintNotNull = (Result.Constraint | (5 << 8)), - ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), - ConstraintTrigger = (Result.Constraint | (7 << 8)), - ConstraintUnique = (Result.Constraint | (8 << 8)), - ConstraintVTab = (Result.Constraint | (9 << 8)), - NoticeRecoverWAL = (Result.Notice | (1 << 8)), - NoticeRecoverRollback = (Result.Notice | (2 << 8)) - } - - - public enum ConfigOption : int - { - SingleThread = 1, - MultiThread = 2, - Serialized = 3 - } - - const string LibraryPath = "sqlite3"; - - #if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE && !USE_SQLITEPCL_RAW - [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)] - public static extern int Threadsafe (); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Open ([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, IntPtr zvfs); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open(byte[] filename, out IntPtr db, int flags, IntPtr zvfs); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention=CallingConvention.Cdecl)] - public static extern Result EnableLoadExtension (IntPtr db, int onoff); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Close (IntPtr db); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Close2(IntPtr db); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Initialize(); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Shutdown(); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Config (ConfigOption option); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)] - public static extern int SetDirectory (uint directoryType, string directoryPath); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)] - public static extern Result BusyTimeout (IntPtr db, int milliseconds); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention=CallingConvention.Cdecl)] - public static extern int Changes (IntPtr db); - - [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention=CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); - - #if NETFX_CORE - [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] - public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); - #endif - - public static IntPtr Prepare2 (IntPtr db, string query) - { - IntPtr stmt; - #if NETFX_CORE - byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); - var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); - #else - var r = Prepare2 (db, query, System.Text.UTF8Encoding.UTF8.GetByteCount (query), out stmt, IntPtr.Zero); - #endif - if (r != Result.OK) { - throw SQLiteException.New (r, GetErrmsg (db)); - } - return stmt; -} - -[DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention=CallingConvention.Cdecl)] -public static extern Result Step (IntPtr stmt); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention=CallingConvention.Cdecl)] -public static extern Result Reset (IntPtr stmt); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention=CallingConvention.Cdecl)] -public static extern Result Finalize (IntPtr stmt); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention=CallingConvention.Cdecl)] -public static extern long LastInsertRowid (IntPtr db); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention=CallingConvention.Cdecl)] -public static extern IntPtr Errmsg (IntPtr db); - -public static string GetErrmsg (IntPtr db) -{ - return Marshal.PtrToStringUni (Errmsg (db)); -} - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindParameterIndex (IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindNull (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindInt (IntPtr stmt, int index, int val); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindInt64 (IntPtr stmt, int index, long val); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindDouble (IntPtr stmt, int index, double val); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Unicode)] -public static extern int BindText (IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention=CallingConvention.Cdecl)] -public static extern int BindBlob (IntPtr stmt, int index, byte[] val, int n, IntPtr free); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention=CallingConvention.Cdecl)] -public static extern int ColumnCount (IntPtr stmt); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention=CallingConvention.Cdecl)] -public static extern IntPtr ColumnName (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention=CallingConvention.Cdecl)] -static extern IntPtr ColumnName16Internal (IntPtr stmt, int index); -public static string ColumnName16(IntPtr stmt, int index) -{ - return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); -} - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention=CallingConvention.Cdecl)] -public static extern ColType ColumnType (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention=CallingConvention.Cdecl)] -public static extern int ColumnInt (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention=CallingConvention.Cdecl)] -public static extern long ColumnInt64 (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention=CallingConvention.Cdecl)] -public static extern double ColumnDouble (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention=CallingConvention.Cdecl)] -public static extern IntPtr ColumnText (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention=CallingConvention.Cdecl)] -public static extern IntPtr ColumnText16 (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention=CallingConvention.Cdecl)] -public static extern IntPtr ColumnBlob (IntPtr stmt, int index); - -[DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention=CallingConvention.Cdecl)] -public static extern int ColumnBytes (IntPtr stmt, int index); - -public static string ColumnString (IntPtr stmt, int index) -{ - return Marshal.PtrToStringUni (SQLite3.ColumnText16 (stmt, index)); -} - -public static byte[] ColumnByteArray (IntPtr stmt, int index) -{ - int length = ColumnBytes (stmt, index); - var result = new byte[length]; - if (length > 0) - Marshal.Copy (ColumnBlob (stmt, index), result, 0, length); - return result; -} - -[DllImport (LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] -public static extern ExtendedResult ExtendedErrCode (IntPtr db); - -[DllImport (LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] -public static extern int LibVersionNumber (); - #else -public static Result Open(string filename, out Sqlite3DatabaseHandle db) -{ - return (Result) Sqlite3.sqlite3_open(filename, out db); -} - -public static Result Open(string filename, out Sqlite3DatabaseHandle db, int flags, IntPtr zVfs) -{ - #if USE_WP8_NATIVE_SQLITE - return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, ""); - #else - return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, null); - #endif -} - -public static Result Close(Sqlite3DatabaseHandle db) -{ - return (Result)Sqlite3.sqlite3_close(db); -} - -public static Result Close2(Sqlite3DatabaseHandle db) -{ - return (Result)Sqlite3.sqlite3_close_v2(db); -} - -public static Result BusyTimeout(Sqlite3DatabaseHandle db, int milliseconds) -{ - return (Result)Sqlite3.sqlite3_busy_timeout(db, milliseconds); -} - -public static int Changes(Sqlite3DatabaseHandle db) -{ - return Sqlite3.sqlite3_changes(db); -} - -public static Sqlite3Statement Prepare2(Sqlite3DatabaseHandle db, string query) -{ - Sqlite3Statement stmt = default(Sqlite3Statement); - #if USE_WP8_NATIVE_SQLITE || USE_SQLITEPCL_RAW - var r = Sqlite3.sqlite3_prepare_v2(db, query, out stmt); - #else - stmt = new Sqlite3Statement(); - var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); - #endif - if (r != 0) - { - throw SQLiteException.New((Result)r, GetErrmsg(db)); - } - return stmt; -} - -public static Result Step(Sqlite3Statement stmt) -{ - return (Result)Sqlite3.sqlite3_step(stmt); -} - -public static Result Reset(Sqlite3Statement stmt) -{ - return (Result)Sqlite3.sqlite3_reset(stmt); -} - -public static Result Finalize(Sqlite3Statement stmt) -{ - return (Result)Sqlite3.sqlite3_finalize(stmt); -} - -public static long LastInsertRowid(Sqlite3DatabaseHandle db) -{ - return Sqlite3.sqlite3_last_insert_rowid(db); -} - -public static string GetErrmsg(Sqlite3DatabaseHandle db) -{ - return Sqlite3.sqlite3_errmsg(db); -} - -public static int BindParameterIndex(Sqlite3Statement stmt, string name) -{ - return Sqlite3.sqlite3_bind_parameter_index(stmt, name); -} - -public static int BindNull(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_bind_null(stmt, index); -} - -public static int BindInt(Sqlite3Statement stmt, int index, int val) -{ - return Sqlite3.sqlite3_bind_int(stmt, index, val); -} - -public static int BindInt64(Sqlite3Statement stmt, int index, long val) -{ - return Sqlite3.sqlite3_bind_int64(stmt, index, val); -} - -public static int BindDouble(Sqlite3Statement stmt, int index, double val) -{ - return Sqlite3.sqlite3_bind_double(stmt, index, val); -} - -public static int BindText(Sqlite3Statement stmt, int index, string val, int n, IntPtr free) -{ - #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_text(stmt, index, val, n); - #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_text(stmt, index, val); - #else - return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); - #endif -} - -public static int BindBlob(Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) -{ - #if USE_WP8_NATIVE_SQLITE - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); - #elif USE_SQLITEPCL_RAW - return Sqlite3.sqlite3_bind_blob(stmt, index, val); - #else - return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); - #endif -} - -public static int ColumnCount(Sqlite3Statement stmt) -{ - return Sqlite3.sqlite3_column_count(stmt); -} - -public static string ColumnName(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_name(stmt, index); -} - -public static string ColumnName16(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_name(stmt, index); -} - -public static ColType ColumnType(Sqlite3Statement stmt, int index) -{ - return (ColType)Sqlite3.sqlite3_column_type(stmt, index); -} - -public static int ColumnInt(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_int(stmt, index); -} - -public static long ColumnInt64(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_int64(stmt, index); -} - -public static double ColumnDouble(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_double(stmt, index); -} - -public static string ColumnText(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_text(stmt, index); -} - -public static string ColumnText16(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_text(stmt, index); -} - -public static byte[] ColumnBlob(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_blob(stmt, index); -} - -public static int ColumnBytes(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_bytes(stmt, index); -} - -public static string ColumnString(Sqlite3Statement stmt, int index) -{ - return Sqlite3.sqlite3_column_text(stmt, index); -} - -public static byte[] ColumnByteArray(Sqlite3Statement stmt, int index) -{ - return ColumnBlob(stmt, index); -} - - #if !USE_SQLITEPCL_RAW -public static Result EnableLoadExtension(Sqlite3DatabaseHandle db, int onoff) -{ - return (Result)Sqlite3.sqlite3_enable_load_extension(db, onoff); -} - #endif - -public static ExtendedResult ExtendedErrCode(Sqlite3DatabaseHandle db) -{ - return (ExtendedResult)Sqlite3.sqlite3_extended_errcode(db); -} - #endif - -public enum ColType : int -{ - Integer = 1, - Float = 2, - Text = 3, - Blob = 4, - Null = 5 -} -} -} - -#if NO_CONCURRENT -namespace SQLite.Extensions -{ -public static class ListEx -{ -public static bool TryAdd (this IDictionary dict, TKey key, TValue value) -{ -try { -dict.Add (key, value); -return true; -} -catch (ArgumentException) { -return false; -} -} -} -} -#endif - diff --git a/CodeHub.Core/Data/TrendingRepository.cs b/CodeHub.Core/Data/TrendingRepository.cs index 5f6db896..925e6b21 100644 --- a/CodeHub.Core/Data/TrendingRepository.cs +++ b/CodeHub.Core/Data/TrendingRepository.cs @@ -2,30 +2,29 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Net.Http; -using GitHubSharp; namespace CodeHub.Core.Data { public interface ITrendingRepository { - Task> GetTrendingRepositories(string since, string language = null); + Task> GetTrendingRepositories(string since, string language = null); } public class TrendingRepository : ITrendingRepository { private const string TrendingUrl = "http://trending.codehub-app.com/v2/trending"; - public async Task> GetTrendingRepositories(string since, string language = null) + public async Task> GetTrendingRepositories(string since, string language = null) { - var query = "?since=" + since; + var query = "?since=" + Uri.EscapeDataString(since); if (!string.IsNullOrEmpty(language)) - query += string.Format("&language={0}", language); + query += string.Format("&language={0}", Uri.EscapeDataString(language)); var client = new HttpClient(); - var serializer = new SimpleJsonSerializer(); + var serializer = new Octokit.Internal.SimpleJsonSerializer(); var msg = await client.GetAsync(TrendingUrl + query).ConfigureAwait(false); var content = await msg.Content.ReadAsStringAsync().ConfigureAwait(false); - return serializer.Deserialize>(content); + return serializer.Deserialize>(content); } } } diff --git a/CodeHub.Core/Extensions/CommandExtensions.cs b/CodeHub.Core/Extensions/CommandExtensions.cs index 0caf63bb..02efbd57 100644 --- a/CodeHub.Core/Extensions/CommandExtensions.cs +++ b/CodeHub.Core/Extensions/CommandExtensions.cs @@ -1,65 +1,21 @@ using System; -using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Windows.Input; -using UIKit; - // ReSharper disable once CheckNamespace namespace ReactiveUI { public static class ReactiveCommandExtensions { - public static void ExecuteIfCan(this ICommand @this, object o) - { - if (@this == null) - return; - - if (@this.CanExecute(o)) - @this.Execute(o); - } - - public static void ExecuteIfCan(this ICommand @this) - { - ExecuteIfCan(@this, null); - } - - public static IReactiveCommand WithSubscription(this IReactiveCommand @this, Action action) - { - @this.Subscribe(action); - return @this; - } - - public static IDisposable ToBarButtonItem(this IObservable @this, UIImage image, Action assignment) - { - return ToBarButtonItem(@this, () => new UIBarButtonItem { Image = image }, assignment); - } - - public static IDisposable ToBarButtonItem(this IObservable @this, UIBarButtonSystemItem systemItem, Action assignment) - { - return ToBarButtonItem(@this, () => new UIBarButtonItem(systemItem), assignment); - } - - public static IDisposable ToBarButtonItem(this IObservable @this, Func creator, Action assignment) - { - var unassignDisposable = Disposable.Create(() => assignment(null)); - IDisposable recentEventDisposable = Disposable.Empty; - - var mainDisposable = @this.Subscribe(x => { - recentEventDisposable?.Dispose(); - - var button = creator(); - var canExecuteDisposable = x.CanExecuteObservable.Subscribe(t => button.Enabled = t); - var clickDisposable = Observable.FromEventPattern(t => button.Clicked += t, t => button.Clicked -= t) - .Select(_ => Unit.Default) - .InvokeCommand(x); + public static IDisposable ExecuteNow( + this ReactiveCommand cmd, TParam param = default(TParam)) + => cmd.CanExecute.Take(1).Where(x => x).Select(_ => param).InvokeReactiveCommand(cmd); - recentEventDisposable = new CompositeDisposable(clickDisposable, canExecuteDisposable); - assignment(button); - }); + public static IDisposable InvokeReactiveCommand( + this IObservable obs, ReactiveCommand cmd) + => obs.InvokeCommand(cmd); - return new CompositeDisposable(mainDisposable, unassignDisposable, Disposable.Create(() => recentEventDisposable.Dispose())); - } + public static IDisposable InvokeReactiveCommand( + this IObservable obs, CombinedReactiveCommand cmd) + => obs.InvokeCommand(cmd); } } \ No newline at end of file diff --git a/CodeHub.Core/Extensions/ExceptionExtensions.cs b/CodeHub.Core/Extensions/ExceptionExtensions.cs new file mode 100644 index 00000000..34fa7bb0 --- /dev/null +++ b/CodeHub.Core/Extensions/ExceptionExtensions.cs @@ -0,0 +1,13 @@ +namespace System +{ + public static class ExceptionExtensions + { + public static Exception GetInnerException(this Exception This) + { + var ex = This; + while (ex.InnerException != null) + ex = ex.InnerException; + return ex; + } + } +} diff --git a/CodeHub.Core/Extensions/FunctionalExtensions.cs b/CodeHub.Core/Extensions/FunctionalExtensions.cs deleted file mode 100644 index b9377938..00000000 --- a/CodeHub.Core/Extensions/FunctionalExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -// Analysis disable once CheckNamespace -public static class FunctionalExtensions -{ - public static void Do(this TSource source, Action selector) - { - if (source == null) - return; - selector(source); - } -} - diff --git a/CodeHub.Core/Extensions/GitHubClientExtensions.cs b/CodeHub.Core/Extensions/GitHubClientExtensions.cs new file mode 100644 index 00000000..3d093bbc --- /dev/null +++ b/CodeHub.Core/Extensions/GitHubClientExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using CodeHub.Core.Utils; + +namespace Octokit +{ + public static class GitHubClientExtensions + { + public static GitHubList RetrieveList( + this GitHubClient client, + Uri uri, + IDictionary parameters = null) + { + return new GitHubList(client, uri, parameters); + } + } +} diff --git a/CodeHub.Core/Extensions/ObservableExtensions.cs b/CodeHub.Core/Extensions/ObservableExtensions.cs index f4d36ea0..68c31159 100644 --- a/CodeHub.Core/Extensions/ObservableExtensions.cs +++ b/CodeHub.Core/Extensions/ObservableExtensions.cs @@ -1,31 +1,21 @@ - -// Analysis disable once CheckNamespace -namespace System.Reactive.Linq -{ - using System; - - public static class ObservableExtensions - { - public static IObservable IsNotNull(this IObservable @this) where T : class - { - return @this.Where(x => x != null); - } - } -} - -// Analysis disable once CheckNamespace -namespace System -{ - using System; - using System.Windows.Input; - using System.Reactive.Disposables; - using System.Reactive.Linq; - - public static class ObservableExtensions - { - public static IDisposable BindCommand(this IObservable @this, ICommand command) - { - return command == null ? Disposable.Empty : @this.Where(x => command.CanExecute(x)).Subscribe(x => command.Execute(x)); - } - } -} +// Analysis disable once CheckNamespace +namespace System +{ + using System; + using System.Windows.Input; + using System.Reactive.Disposables; + using System.Reactive.Linq; + + public static class ObservableExtensions + { + public static IDisposable BindCommand(this IObservable @this, ICommand command) + { + return command == null ? Disposable.Empty : @this.Where(x => command.CanExecute(x)).Subscribe(x => command.Execute(x)); + } + + public static IDisposable SubscribeError(this IObservable @this, Action onError) + { + return @this.Subscribe(_ => { }, onError); + } + } +} diff --git a/CodeHub.Core/Extensions/ReactiveExtensions.cs b/CodeHub.Core/Extensions/ReactiveExtensions.cs deleted file mode 100644 index 8002634d..00000000 --- a/CodeHub.Core/Extensions/ReactiveExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace System -{ - public static class ReactiveExtensions - { - public static IDisposable SubscribeSafe(this IObservable source, Action onNext) - { - if (source == null) - { - throw new ArgumentNullException("source"); - } - if (onNext == null) - { - throw new ArgumentNullException("onNext"); - } - - return source.Subscribe(x => - { - try - { - onNext(x); - } - catch - { - } - }); - } - } -} - diff --git a/CodeHub.Core/Factories/ILoginFactory.cs b/CodeHub.Core/Factories/ILoginFactory.cs deleted file mode 100755 index 2c3b6c17..00000000 --- a/CodeHub.Core/Factories/ILoginFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CodeHub.Core.Data; -using System.Threading.Tasks; - -namespace CodeHub.Core.Factories -{ - public interface ILoginFactory - { - Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain); - - Task LoginAccount(GitHubAccount account); - - Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null); - } - - public class LoginData - { - public GitHubSharp.Client Client { get; set; } - public GitHubAccount Account { get; set; } - } -} \ No newline at end of file diff --git a/CodeHub.Core/Factories/LoginFactory.cs b/CodeHub.Core/Factories/LoginFactory.cs deleted file mode 100755 index fc47bc07..00000000 --- a/CodeHub.Core/Factories/LoginFactory.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using CodeHub.Core.Services; -using CodeHub.Core.Data; -using GitHubSharp; -using System.Threading.Tasks; -using System.Linq; -using System.Collections.Generic; - -namespace CodeHub.Core.Factories -{ - public class LoginFactory : ILoginFactory - { - private static readonly string[] Scopes = { "user", "public_repo", "repo", "notifications", "gist" }; - private readonly IAccountsService _accounts; - - public LoginFactory(IAccountsService accounts) - { - _accounts = accounts; - } - - public async Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain) - { - var token = await Client.RequestAccessToken(clientId, clientSecret, code, redirect, requestDomain); - var client = Client.BasicOAuth(token.AccessToken, apiDomain); - var info = await client.ExecuteAsync(client.AuthenticatedUser.GetInfo()); - var username = info.Data.Login; - - //Does this user exist? - - var account = _accounts.FirstOrDefault(x => string.Equals(x.Username, username) && string.Equals(x.Domain, apiDomain)); - var exists = account != null; - account = account ?? new GitHubAccount { Username = username }; - - account.OAuth = token.AccessToken; - account.AvatarUrl = info.Data.AvatarUrl; - account.Domain = apiDomain; - account.WebDomain = requestDomain; - client.Username = username; - - if (exists) - _accounts.Update(account); - else - _accounts.Insert(account); - return new LoginData { Client = client, Account = account }; - } - - public async Task LoginAccount(GitHubAccount account) - { - //Create the client - Client client = null; - if (!string.IsNullOrEmpty(account.OAuth)) - { - client = Client.BasicOAuth(account.OAuth, account.Domain ?? Client.DefaultApi); - } - else if (account.IsEnterprise || !string.IsNullOrEmpty(account.Password)) - { - client = Client.Basic(account.Username, account.Password, account.Domain ?? Client.DefaultApi); - } - - var data = await client.ExecuteAsync(client.AuthenticatedUser.GetInfo()); - var userInfo = data.Data; - account.Username = userInfo.Login; - account.AvatarUrl = userInfo.AvatarUrl; - client.Username = userInfo.Login; - - if (_accounts.Exists(account)) - _accounts.Update(account); - else - _accounts.Insert(account); - return client; - } - - public async Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null) - { - //Fill these variables in during the proceeding try/catch - var apiUrl = domain; - - //Make some valid checks - if (string.IsNullOrEmpty(user)) - throw new ArgumentException("Username is invalid"); - if (string.IsNullOrEmpty(pass)) - throw new ArgumentException("Password is invalid"); - if (apiUrl != null && !Uri.IsWellFormedUriString(apiUrl, UriKind.Absolute)) - throw new ArgumentException("Domain is invalid"); - - - var client = twoFactor == null ? Client.Basic(user, pass, apiUrl) : Client.BasicTwoFactorAuthentication(user, pass, twoFactor, apiUrl); - var authorization = await client.ExecuteAsync(client.Authorizations.Create(new List(Scopes), "CodeHub: " + user, null, Guid.NewGuid().ToString())); - var existingAccount = _accounts.FirstOrDefault(x => string.Equals(x.Username, user) && string.Equals(x.Domain, apiUrl)); - var account = existingAccount ?? new GitHubAccount { - Username = user, - IsEnterprise = true, - WebDomain = apiUrl, - Domain = apiUrl - }; - - account.OAuth = authorization.Data.Token; - return account; - } - } -} diff --git a/CodeHub.Core/Interactions.cs b/CodeHub.Core/Interactions.cs new file mode 100644 index 00000000..368f141d --- /dev/null +++ b/CodeHub.Core/Interactions.cs @@ -0,0 +1,37 @@ +using System; +using System.Reactive; +using System.Threading.Tasks; +using ReactiveUI; + +namespace CodeHub.Core +{ + public class UserError + { + public string Title { get; } + public string Message { get; } + + public UserError(string message, Exception exception = null) + : this("Error", message, exception) + { + } + + public UserError(string title, string message, Exception exception = null) + { + Title = title; + Message = exception == null ? message : $"{message} {ExceptionMessage(exception)}"; + } + + private static string ExceptionMessage(Exception exception) + { + if (exception is TaskCanceledException) + return "The request timed out waiting for the server to respond."; + else + return exception?.Message; + } + } + + public static class Interactions + { + public static readonly Interaction Errors = new Interaction(); + } +} diff --git a/CodeHub.Core/Messages/GistAddMessage.cs b/CodeHub.Core/Messages/GistAddMessage.cs index 2392d8c3..4d168b31 100644 --- a/CodeHub.Core/Messages/GistAddMessage.cs +++ b/CodeHub.Core/Messages/GistAddMessage.cs @@ -1,15 +1,10 @@ -using System; -using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; - namespace CodeHub.Core.Messages { - public class GistAddMessage : MvxMessage + public class GistAddMessage { - public GistModel Gist { get; private set; } + public Octokit.Gist Gist { get; } - public GistAddMessage(object sender, GistModel gist) - : base(sender) + public GistAddMessage(Octokit.Gist gist) { Gist = gist; } diff --git a/CodeHub.Core/Messages/IssueAddMessage.cs b/CodeHub.Core/Messages/IssueAddMessage.cs index 18c48a1c..7baac975 100644 --- a/CodeHub.Core/Messages/IssueAddMessage.cs +++ b/CodeHub.Core/Messages/IssueAddMessage.cs @@ -1,12 +1,15 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class IssueAddMessage : MvxMessage + public class IssueAddMessage { - public IssueAddMessage(object sender) : base(sender) {} - public IssueModel Issue; + public IssueModel Issue { get; } + + public IssueAddMessage(IssueModel issue) + { + Issue = issue; + } } } diff --git a/CodeHub.Core/Messages/IssueEditMessage.cs b/CodeHub.Core/Messages/IssueEditMessage.cs index 1a1c4a60..b7221db5 100644 --- a/CodeHub.Core/Messages/IssueEditMessage.cs +++ b/CodeHub.Core/Messages/IssueEditMessage.cs @@ -1,12 +1,15 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class IssueEditMessage : MvxMessage + public class IssueEditMessage { - public IssueEditMessage(object sender) : base(sender) {} - public IssueModel Issue; + public IssueModel Issue { get; } + + public IssueEditMessage(IssueModel issue) + { + Issue = issue; + } } } diff --git a/CodeHub.Core/Messages/LogoutMessage.cs b/CodeHub.Core/Messages/LogoutMessage.cs index bd22f46b..141b0e29 100644 --- a/CodeHub.Core/Messages/LogoutMessage.cs +++ b/CodeHub.Core/Messages/LogoutMessage.cs @@ -1,6 +1,4 @@ -using System; - -namespace CodeHub.Core.Messages +namespace CodeHub.Core.Messages { public class LogoutMessage { diff --git a/CodeHub.Core/Messages/NotificationCountMessage.cs b/CodeHub.Core/Messages/NotificationCountMessage.cs index b2e5d207..ad9128b3 100644 --- a/CodeHub.Core/Messages/NotificationCountMessage.cs +++ b/CodeHub.Core/Messages/NotificationCountMessage.cs @@ -1,11 +1,13 @@ -using MvvmCross.Plugins.Messenger; - namespace CodeHub.Core.Messages { - public class NotificationCountMessage : MvxMessage + public class NotificationCountMessage { - public NotificationCountMessage(object sender) : base(sender) {} - public int Count; + public int Count { get; } + + public NotificationCountMessage(int count) + { + Count = count; + } } } diff --git a/CodeHub.Core/Messages/PullRequestEditMessage.cs b/CodeHub.Core/Messages/PullRequestEditMessage.cs index 22374345..360b9375 100644 --- a/CodeHub.Core/Messages/PullRequestEditMessage.cs +++ b/CodeHub.Core/Messages/PullRequestEditMessage.cs @@ -1,12 +1,15 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class PullRequestEditMessage : MvxMessage + public class PullRequestEditMessage { - public PullRequestEditMessage(object sender) : base(sender) {} - public PullRequestModel PullRequest; + public PullRequestModel PullRequest { get; } + + public PullRequestEditMessage(PullRequestModel pullRequest) + { + PullRequest = pullRequest; + } } } diff --git a/CodeHub.Core/Messages/SelectIssueLabelsMessage.cs b/CodeHub.Core/Messages/SelectIssueLabelsMessage.cs index f748209a..243e86e8 100644 --- a/CodeHub.Core/Messages/SelectIssueLabelsMessage.cs +++ b/CodeHub.Core/Messages/SelectIssueLabelsMessage.cs @@ -1,11 +1,17 @@ -using MvvmCross.Plugins.Messenger; +using System.Collections.Generic; +using System.Linq; +using GitHubSharp.Models; namespace CodeHub.Core.Messages { - public class SelectIssueLabelsMessage : MvxMessage + public class SelectIssueLabelsMessage { - public SelectIssueLabelsMessage(object sender) : base(sender) {} - public GitHubSharp.Models.LabelModel[] Labels; + public LabelModel[] Labels { get; } + + public SelectIssueLabelsMessage(IEnumerable labels) + { + Labels = labels.ToArray(); + } } } diff --git a/CodeHub.Core/Messages/SelectedAssignedToMessage.cs b/CodeHub.Core/Messages/SelectedAssignedToMessage.cs index 95e295d8..e0b8b402 100644 --- a/CodeHub.Core/Messages/SelectedAssignedToMessage.cs +++ b/CodeHub.Core/Messages/SelectedAssignedToMessage.cs @@ -1,12 +1,15 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class SelectedAssignedToMessage : MvxMessage + public class SelectedAssignedToMessage { - public SelectedAssignedToMessage(object sender) : base(sender) {} - public BasicUserModel User; + public BasicUserModel User { get; } + + public SelectedAssignedToMessage(BasicUserModel user) + { + User = user; + } } } diff --git a/CodeHub.Core/Messages/SelectedMilestoneMessage.cs b/CodeHub.Core/Messages/SelectedMilestoneMessage.cs index ff3cb3ce..59f87ef0 100644 --- a/CodeHub.Core/Messages/SelectedMilestoneMessage.cs +++ b/CodeHub.Core/Messages/SelectedMilestoneMessage.cs @@ -1,12 +1,15 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class SelectedMilestoneMessage : MvxMessage + public class SelectedMilestoneMessage { - public SelectedMilestoneMessage(object sender) : base(sender) {} - public MilestoneModel Milestone; + public MilestoneModel Milestone { get; } + + public SelectedMilestoneMessage(MilestoneModel milestone) + { + Milestone = milestone; + } } } diff --git a/CodeHub.Core/Messages/SourceEditMessage.cs b/CodeHub.Core/Messages/SourceEditMessage.cs index 7d32a3a1..9b8ad340 100644 --- a/CodeHub.Core/Messages/SourceEditMessage.cs +++ b/CodeHub.Core/Messages/SourceEditMessage.cs @@ -1,12 +1,9 @@ using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; namespace CodeHub.Core.Messages { - public class SourceEditMessage : MvxMessage + public class SourceEditMessage { - public SourceEditMessage(object sender) : base(sender) {} - public string OldSha; public string Data; public ContentUpdateModel Update; diff --git a/CodeHub.Core/Services/AccountsService.cs b/CodeHub.Core/Services/AccountsService.cs index 771cbb77..e1bd68f9 100755 --- a/CodeHub.Core/Services/AccountsService.cs +++ b/CodeHub.Core/Services/AccountsService.cs @@ -1,116 +1,46 @@ -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using CodeHub.Core.Data; -using SQLite; - -namespace CodeHub.Core.Services -{ - public class AccountsService : IAccountsService - { - private readonly SQLiteConnection _userDatabase; - private readonly IDefaultValueService _defaults; - private readonly string _accountsPath; - - public GitHubAccount ActiveAccount { get; private set; } - - public AccountsService(IDefaultValueService defaults, IAccountPreferencesService accountPreferences) - { - _defaults = defaults; - _accountsPath = accountPreferences.AccountsDir; - - // Assure creation of the accounts path - if (!Directory.Exists(_accountsPath)) - Directory.CreateDirectory(_accountsPath); - - _userDatabase = new SQLiteConnection(Path.Combine(_accountsPath, "accounts.db")); - _userDatabase.CreateTable(); - } - - public GitHubAccount GetDefault() - { - int id; - return !_defaults.TryGet("DEFAULT_ACCOUNT", out id) ? null : Find(id); - } - - public void SetDefault(GitHubAccount account) - { - if (account == null) - _defaults.Clear("DEFAULT_ACCOUNT"); - else - _defaults.Set("DEFAULT_ACCOUNT", account.Id); - } - - public void SetActiveAccount(GitHubAccount account) - { - if (account != null) - { - var accountDir = CreateAccountDirectory(account); - if (!Directory.Exists(accountDir)) - Directory.CreateDirectory(accountDir); - } - - ActiveAccount = account; - } - - protected string CreateAccountDirectory(GitHubAccount account) - { - return Path.Combine(_accountsPath, account.Id.ToString(CultureInfo.InvariantCulture)); - } - - public void Insert(GitHubAccount account) - { - lock (_userDatabase) - { - _userDatabase.Insert(account); - } - } - - public void Remove(GitHubAccount account) - { - lock (_userDatabase) - { - _userDatabase.Delete(account); - } - var accountDir = CreateAccountDirectory(account); - - if (!Directory.Exists(accountDir)) - return; - Directory.Delete(accountDir, true); - } - - public void Update(GitHubAccount account) - { - lock (_userDatabase) - { - _userDatabase.Update(account); - } - } - - public bool Exists(GitHubAccount account) - { - return Find(account.Id) != null; - } - - public GitHubAccount Find(int id) - { - lock (_userDatabase) - { - var query = _userDatabase.Find(x => x.Id == id); - return query; - } - } - - public IEnumerator GetEnumerator() - { - foreach (var account in _userDatabase.Table()) - yield return account; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; +using Akavache; +using CodeHub.Core.Data; + +namespace CodeHub.Core.Services +{ + public class AccountsService : IAccountsService + { + public Task GetActiveAccount() + => Get(Settings.DefaultAccount); + + public Task SetActiveAccount(Account account) + { + Settings.DefaultAccount = account == null ? null : GetKey(account); + return Task.FromResult(false); + } + + public Task> GetAccounts() + => BlobCache.UserAccount.GetAllObjects() + .Select(x => x.OrderBy(y => y.Username).AsEnumerable()) + .ToTask(); + + public Task Save(Account account) + => BlobCache.UserAccount.InsertObject(GetKey(account), account).ToTask(); + + public Task Remove(Account account) + => BlobCache.UserAccount.Invalidate(GetKey(account)).ToTask(); + + public Task Get(string domain, string username) => Get(GetKey(username, domain)); + + public Task Get(string key) + => BlobCache.UserAccount.GetObject(key) + .Catch(Observable.Return(null)) + .ToTask(); + + private string GetKey(Account account) + => GetKey(account.Username, account.Domain); + + private string GetKey(string username, string domain) + => "account_" + username + domain; + } +} diff --git a/CodeHub.Core/Services/AnalyticsService.cs b/CodeHub.Core/Services/AnalyticsService.cs deleted file mode 100644 index 0d3d2e75..00000000 --- a/CodeHub.Core/Services/AnalyticsService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace CodeHub.Core.Services -{ - public class AnalyticsService : IAnalyticsService - { - private readonly ICollection _screens = new LinkedList(); - - public void LogScreen(string screenName) - { - _screens.Add(screenName); - if (_screens.Count > 25) - _screens.Remove(_screens.First()); - } - - public IEnumerable GetVisitedScreens() - { - return _screens.ToList().AsEnumerable(); - } - } -} - diff --git a/CodeHub.Core/Services/ApplicationService.cs b/CodeHub.Core/Services/ApplicationService.cs old mode 100755 new mode 100644 index f4541ad0..0c40eb21 --- a/CodeHub.Core/Services/ApplicationService.cs +++ b/CodeHub.Core/Services/ApplicationService.cs @@ -1,48 +1,74 @@ -using CodeHub.Core.Data; -using CodeHub.Core.Services; -using GitHubSharp; -using System; - -namespace CodeHub.Core.Services -{ - public class ApplicationService : IApplicationService - { - public Client Client { get; private set; } - public GitHubAccount Account { get; private set; } - public IAccountsService Accounts { get; private set; } - - public Action ActivationAction { get; set; } - - public ApplicationService(IAccountsService accountsService) - { - Accounts = accountsService; - } - - public void DeactivateUser() - { - Accounts.SetActiveAccount(null); - Accounts.SetDefault(null); - Account = null; - Client = null; - } - - public void ActivateUser(GitHubAccount account, Client client) - { - Accounts.SetActiveAccount(account); - Account = account; - Client = client; - - //Set the default account - Accounts.SetDefault(account); - } - - public void SetUserActivationAction(Action action) - { - if (Account != null) - action(); - else - ActivationAction = action; - } - - } -} +using CodeHub.Core.Data; +using GitHubSharp; +using System; +using System.Threading.Tasks; +using CodeHub.Core.Utilities; + +namespace CodeHub.Core.Services +{ + public class ApplicationService : IApplicationService + { + private readonly IAccountsService _accountsService; + + public Client Client { get; private set; } + + public Octokit.GitHubClient GitHubClient { get; private set; } + + public Account Account { get; private set; } + + public Action ActivationAction { get; set; } + + public ApplicationService(IAccountsService accountsService) + { + _accountsService = accountsService; + } + + public void DeactivateUser() + { + _accountsService.SetActiveAccount(null).Wait(); + Account = null; + Client = null; + } + + public void SetUserActivationAction(Action action) + { + if (Account != null) + action(); + else + ActivationAction = action; + } + + public Task UpdateActiveAccount() => _accountsService.Save(Account); + + public async Task LoginAccount(Account account) + { + var domain = account.Domain ?? Client.DefaultApi; + Client client = null; + Octokit.Credentials credentials = null; + + if (!string.IsNullOrEmpty(account.OAuth)) + { + client = Client.BasicOAuth(account.OAuth, domain); + credentials = new Octokit.Credentials(account.OAuth); + } + else if (account.IsEnterprise || !string.IsNullOrEmpty(account.Password)) + { + client = Client.Basic(account.Username, account.Password, domain); + credentials = new Octokit.Credentials(account.Username, account.Password); + } + + var octoClient = OctokitClientFactory.Create(new Uri(domain), credentials); + var user = await octoClient.User.Current(); + account.Username = user.Login; + account.AvatarUrl = user.AvatarUrl; + client.Username = user.Login; + + await _accountsService.Save(account); + await _accountsService.SetActiveAccount(account); + + Account = account; + Client = client; + GitHubClient = octoClient; + } + } +} diff --git a/CodeHub.Core/Services/IAccountPreferencesService.cs b/CodeHub.Core/Services/IAccountPreferencesService.cs deleted file mode 100755 index d218fc1c..00000000 --- a/CodeHub.Core/Services/IAccountPreferencesService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CodeHub.Core.Services -{ - public interface IAccountPreferencesService - { - /// - /// Gets the accounts directory - /// - string AccountsDir { get; } - } -} \ No newline at end of file diff --git a/CodeHub.Core/Services/IAccountsService.cs b/CodeHub.Core/Services/IAccountsService.cs old mode 100755 new mode 100644 index b9cfed5f..9035c3a3 --- a/CodeHub.Core/Services/IAccountsService.cs +++ b/CodeHub.Core/Services/IAccountsService.cs @@ -1,54 +1,21 @@ -using System.Collections.Generic; -using CodeHub.Core.Data; - -namespace CodeHub.Core.Services -{ - public interface IAccountsService : IEnumerable - { - /// - /// Gets the active account - /// - GitHubAccount ActiveAccount { get; } - - /// - /// Sets the active account - /// - /// - void SetActiveAccount(GitHubAccount account); - - /// - /// Gets the default account - /// - GitHubAccount GetDefault(); - - /// - /// Sets the default account - /// - void SetDefault(GitHubAccount account); - - /// - /// Insert the specified account. - /// - void Insert(GitHubAccount account); - - /// - /// Remove the specified account. - /// - void Remove(GitHubAccount account); - - /// - /// Update this instance in the database - /// - void Update(GitHubAccount account); - - /// - /// Checks to see whether a specific account exists (Username comparison) - /// - bool Exists(GitHubAccount account); - - /// - /// Find the specified account via it's username - /// - GitHubAccount Find(int id); - } +using System.Collections.Generic; +using System.Threading.Tasks; +using CodeHub.Core.Data; + +namespace CodeHub.Core.Services +{ + public interface IAccountsService + { + Task GetActiveAccount(); + + Task SetActiveAccount(Account account); + + Task> GetAccounts(); + + Task Save(Account account); + + Task Remove(Account account); + + Task Get(string domain, string username); + } } \ No newline at end of file diff --git a/CodeHub.Core/Services/IAlertDialogService.cs b/CodeHub.Core/Services/IAlertDialogService.cs index 50171b93..127a5883 100644 --- a/CodeHub.Core/Services/IAlertDialogService.cs +++ b/CodeHub.Core/Services/IAlertDialogService.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using System.Reactive.Disposables; +using ReactiveUI; namespace CodeHub.Core.Services { @@ -10,6 +12,52 @@ public interface IAlertDialogService Task Alert(string title, string message); Task PromptTextBox(string title, string message, string defaultValue, string okTitle); + + void Show(string text); + + void Hide(); + } + + public static class AlertDialogServiceExtensions + { + public static IDisposable Activate(this IAlertDialogService @this, string text) + { + @this.Show(text); + return Disposable.Create(@this.Hide); + } + + public static IDisposable Activate(this IAlertDialogService @this, IObservable observable, string text) + { + return observable.Subscribe(x => + { + if (x) + @this.Show(text); + else + @this.Hide(); + }); + } + + public static IDisposable Activate(this IAlertDialogService @this, ReactiveCommand command, string text) + { + return command.IsExecuting.Subscribe(x => + { + if (x) + @this.Show(text); + else + @this.Hide(); + }); + } + + public static IDisposable AlertExecuting(this ReactiveCommand @this, IAlertDialogService dialogFactory, string text) + { + return @this.IsExecuting.Subscribe(x => + { + if (x) + dialogFactory.Show(text); + else + dialogFactory.Hide(); + }); + } } } diff --git a/CodeHub.Core/Services/IAnalyticsService.cs b/CodeHub.Core/Services/IAnalyticsService.cs deleted file mode 100644 index 004c8e02..00000000 --- a/CodeHub.Core/Services/IAnalyticsService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace CodeHub.Core.Services -{ - public interface IAnalyticsService - { - void LogScreen(string screenName); - - IEnumerable GetVisitedScreens(); - } -} - diff --git a/CodeHub.Core/Services/IApplicationService.cs b/CodeHub.Core/Services/IApplicationService.cs index 893a99f5..d6886059 100755 --- a/CodeHub.Core/Services/IApplicationService.cs +++ b/CodeHub.Core/Services/IApplicationService.cs @@ -1,23 +1,25 @@ -using System; -using CodeHub.Core.Services; -using CodeHub.Core.Data; - -namespace CodeHub.Core.Services -{ - public interface IApplicationService - { - GitHubSharp.Client Client { get; } - - GitHubAccount Account { get; } - - IAccountsService Accounts { get; } - - void DeactivateUser(); - - void ActivateUser(GitHubAccount account, GitHubSharp.Client client); - - void SetUserActivationAction(Action action); - - Action ActivationAction { get; set; } - } +using System; +using System.Threading.Tasks; +using CodeHub.Core.Data; + +namespace CodeHub.Core.Services +{ + public interface IApplicationService + { + GitHubSharp.Client Client { get; } + + Octokit.GitHubClient GitHubClient { get; } + + Account Account { get; } + + Task UpdateActiveAccount(); + + void DeactivateUser(); + + void SetUserActivationAction(Action action); + + Action ActivationAction { get; set; } + + Task LoginAccount(Account account); + } } \ No newline at end of file diff --git a/CodeHub.Core/Services/IDefaultValueService.cs b/CodeHub.Core/Services/IDefaultValueService.cs deleted file mode 100755 index 2abf1250..00000000 --- a/CodeHub.Core/Services/IDefaultValueService.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace CodeHub.Core.Services -{ - public interface IDefaultValueService - { - bool TryGet(string key, out string value); - - bool TryGet(string key, out int value); - - bool TryGet(string key, out bool value); - - void Set(string key, string value); - - void Set(string key, int? value); - - void Set(string key, bool? value); - - void Clear(string key); - } -} \ No newline at end of file diff --git a/CodeHub.Core/Services/IErrorService.cs b/CodeHub.Core/Services/IErrorService.cs deleted file mode 100644 index 412e66ad..00000000 --- a/CodeHub.Core/Services/IErrorService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace CodeHub.Core.Services -{ - public interface IErrorService - { - void Log(Exception e, bool fatal = false); - - void Init(); - } -} - diff --git a/CodeHub.Core/Services/IImgurService.cs b/CodeHub.Core/Services/IImgurService.cs new file mode 100644 index 00000000..aacf0add --- /dev/null +++ b/CodeHub.Core/Services/IImgurService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using CodeHub.Core.Data; + +namespace CodeHub.Core.Services +{ + public interface IImgurService + { + Task SendImage(byte[] data); + } +} + diff --git a/CodeHub.Core/Services/IJsonSerializationService.cs b/CodeHub.Core/Services/IJsonSerializationService.cs deleted file mode 100644 index 018ea670..00000000 --- a/CodeHub.Core/Services/IJsonSerializationService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CodeHub.Core.Services -{ - public interface IJsonSerializationService - { - string Serialize(object o); - - TData Deserialize(string data); - } -} - diff --git a/CodeHub.Core/Services/IMarkdownService.cs b/CodeHub.Core/Services/IMarkdownService.cs index 9592e7b4..880c166e 100644 --- a/CodeHub.Core/Services/IMarkdownService.cs +++ b/CodeHub.Core/Services/IMarkdownService.cs @@ -1,10 +1,10 @@ -using System; +using System.Threading.Tasks; namespace CodeHub.Core.Services { public interface IMarkdownService { - string Convert(string s); + Task Convert(string s); } } diff --git a/CodeHub.Core/Services/IMessageService.cs b/CodeHub.Core/Services/IMessageService.cs new file mode 100644 index 00000000..d3acc2eb --- /dev/null +++ b/CodeHub.Core/Services/IMessageService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace CodeHub.Core.Services +{ + public interface IMessageService + { + void Send(T message); + + IDisposable Listen(Action action); + } +} \ No newline at end of file diff --git a/CodeHub.Core/Services/INetworkActivityService.cs b/CodeHub.Core/Services/INetworkActivityService.cs new file mode 100644 index 00000000..6bb46231 --- /dev/null +++ b/CodeHub.Core/Services/INetworkActivityService.cs @@ -0,0 +1,21 @@ +using System; +using System.Reactive.Disposables; + +namespace CodeHub.Core.Services +{ + public interface INetworkActivityService + { + void PushNetworkActive(); + + void PopNetworkActive(); + } + + public static class NetworkActivityServiceExtensions + { + public static IDisposable ActivateNetwork(this INetworkActivityService @this) + { + @this.PushNetworkActive(); + return Disposable.Create(@this.PopNetworkActive); + } + } +} diff --git a/CodeHub.Core/Services/ImgurService.cs b/CodeHub.Core/Services/ImgurService.cs new file mode 100644 index 00000000..717cf7ff --- /dev/null +++ b/CodeHub.Core/Services/ImgurService.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using System.Net.Http; +using Newtonsoft.Json; +using CodeHub.Core.Data; + +namespace CodeHub.Core.Services +{ + public class ImgurService : IImgurService + { + private const string AuthorizationClientId = "4d2779fd2cc56cb"; + private const string ImgurPostUrl = "https://api.imgur.com/3/image"; + + public async Task SendImage(byte[] data) + { + var client = new HttpClient(); + client.Timeout = new TimeSpan(0, 0, 30); + client.DefaultRequestHeaders.Add("Authorization", "Client-ID " + AuthorizationClientId); + var body = JsonConvert.SerializeObject(new { image = Convert.ToBase64String(data) }); + var content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); + var response = await client.PostAsync(ImgurPostUrl, content).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + throw new InvalidOperationException("Unable to post to Imgur! " + response.ReasonPhrase); + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(responseBody); + } + } +} + diff --git a/CodeHub.Core/Services/JsonSerializationService.cs b/CodeHub.Core/Services/JsonSerializationService.cs deleted file mode 100644 index 6eca1665..00000000 --- a/CodeHub.Core/Services/JsonSerializationService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using CodeHub.Core.Services; - -namespace CodeHub.Core.Services -{ - public class JsonSerializationService : IJsonSerializationService - { - private readonly GitHubSharp.SimpleJsonSerializer _serializer; - - public JsonSerializationService() - { - _serializer = new GitHubSharp.SimpleJsonSerializer(); - } - - public string Serialize(object o) - { - return _serializer.Serialize(o); - } - - public TData Deserialize(string data) - { - return _serializer.Deserialize(data); - } - } -} - diff --git a/CodeHub.Core/Services/LoginService.cs b/CodeHub.Core/Services/LoginService.cs new file mode 100644 index 00000000..adeb1673 --- /dev/null +++ b/CodeHub.Core/Services/LoginService.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using CodeHub.Core.Data; +using CodeHub.Core.Utilities; + +namespace CodeHub.Core.Services +{ + public interface ILoginService + { + Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain); + + Task LoginWithToken(string apiDomain, string webDomain, string token, bool enterprise); + + Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null); + } + + public class LoginService : ILoginService + { + private static readonly string[] Scopes = { "user", "repo", "notifications", "gist" }; + + private readonly IApplicationService _applicationService; + private readonly IAccountsService _accountsService; + + public LoginService( + IAccountsService accountsService, + IApplicationService applicationService) + { + _accountsService = accountsService; + _applicationService = applicationService; + } + + public async Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain) + { + var oauthRequest = new Octokit.OauthTokenRequest(clientId, clientSecret, code) + { + RedirectUri = new Uri(redirect) + }; + + var client = new Octokit.GitHubClient(OctokitClientFactory.UserAgent); + var token = await client.Oauth.CreateAccessToken(oauthRequest); + + var credentials = new Octokit.Credentials(token.AccessToken); + client = OctokitClientFactory.Create(new Uri(apiDomain), credentials); + + var user = await client.User.Current(); + var account = await _accountsService.Get(apiDomain, user.Login); + + account = account ?? new Account { Username = user.Login }; + account.OAuth = token.AccessToken; + account.AvatarUrl = user.AvatarUrl; + account.Domain = apiDomain; + account.WebDomain = requestDomain; + + await _accountsService.Save(account); + await _applicationService.LoginAccount(account); + } + + public async Task LoginWithToken(string apiDomain, string webDomain, string token, bool enterprise) + { + if (string.IsNullOrEmpty(token)) + throw new ArgumentException("Token is invalid"); + if (apiDomain != null && !Uri.IsWellFormedUriString(apiDomain, UriKind.Absolute)) + throw new ArgumentException("API Domain is invalid"); + if (webDomain != null && !Uri.IsWellFormedUriString(webDomain, UriKind.Absolute)) + throw new ArgumentException("Web Domain is invalid"); + + var credentials = new Octokit.Credentials(token); + var client = OctokitClientFactory.Create(new Uri(apiDomain), credentials); + var userInfo = await client.User.Current(); + + var scopes = await GetScopes(apiDomain, userInfo.Login, token); + CheckScopes(scopes); + + var account = (await _accountsService.Get(apiDomain, userInfo.Login)) ?? new Account(); + account.Username = userInfo.Login; + account.Domain = apiDomain; + account.WebDomain = webDomain; + account.IsEnterprise = enterprise; + account.OAuth = token; + account.AvatarUrl = userInfo.AvatarUrl; + + await _accountsService.Save(account); + await _applicationService.LoginAccount(account); + } + + public async Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null) + { + if (string.IsNullOrEmpty(user)) + throw new ArgumentException("Username is invalid"); + if (string.IsNullOrEmpty(pass)) + throw new ArgumentException("Password is invalid"); + if (domain == null || !Uri.IsWellFormedUriString(domain, UriKind.Absolute)) + throw new ArgumentException("Domain is invalid"); + + var newAuthorization = new Octokit.NewAuthorization( + $"CodeHub: {user}", Scopes, Guid.NewGuid().ToString()); + + var credentials = new Octokit.Credentials(user, pass); + var client = OctokitClientFactory.Create(new Uri(domain), credentials); + + var authorization = await (twoFactor == null + ? client.Authorization.Create(newAuthorization) + : client.Authorization.Create(newAuthorization, twoFactor)); + + var existingAccount = await _accountsService.Get(domain, user); + var account = existingAccount ?? new Account + { + Username = user, + IsEnterprise = true, + WebDomain = domain, + Domain = domain + }; + + account.OAuth = authorization.Token; + + await _applicationService.LoginAccount(account); + } + + private static async Task> GetScopes(string domain, string username, string token) + { + var client = new HttpClient(); + var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, token))); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken); + domain = (domain.EndsWith("/", StringComparison.Ordinal) ? domain : domain + "/") + "user"; + var response = await client.GetAsync(domain); + + if (!response.Headers.TryGetValues("X-OAuth-Scopes", out IEnumerable scopes)) + return new List(); + + var values = scopes.FirstOrDefault() ?? string.Empty; + return values.Split(',').Select(x => x.Trim()).ToList(); + } + + private static void CheckScopes(IEnumerable scopes) + { + var missing = OctokitClientFactory.Scopes.Except(scopes).ToList(); + if (missing.Any()) + throw new InvalidOperationException("Missing scopes! You are missing access to the following " + + "scopes that are necessary for CodeHub to operate correctly: " + string.Join(", ", missing)); + } + } +} diff --git a/CodeHub.Core/Services/MessageService.cs b/CodeHub.Core/Services/MessageService.cs new file mode 100644 index 00000000..b691162c --- /dev/null +++ b/CodeHub.Core/Services/MessageService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace CodeHub.Core.Services +{ + public class MessageService : IMessageService + { + private readonly IList> _subscriptions = new List>(); + + public IDisposable Listen(Action action) + { + var obj = Tuple.Create(typeof(T), new WeakReference(action)); + lock (_subscriptions) + _subscriptions.Add(obj); + return new Reference(action, () => _subscriptions.Remove(obj)); + } + + public void Send(T message) + { + lock (_subscriptions) + { + var shouldRemove = new LinkedList>(); + + foreach (var sub in _subscriptions) + { + if (!sub.Item2.IsAlive) + shouldRemove.AddLast(sub); + + if (sub.Item1 == typeof(T)) + { + var handle = sub.Item2.Target; + if (handle != null) + { + ((Action)handle).Invoke(message); + } + } + } + + foreach (var r in shouldRemove) + _subscriptions.Remove(r); + } + } + + private class Reference : IDisposable + { + private readonly Action _removal; + private readonly object _handle; + + public Reference(object handle, Action removal) + { + _handle = handle; + _removal = removal; + } + + public void Dispose() => _removal.Invoke(); + } + } +} diff --git a/CodeHub.Core/Settings.cs b/CodeHub.Core/Settings.cs new file mode 100644 index 00000000..0699b351 --- /dev/null +++ b/CodeHub.Core/Settings.cs @@ -0,0 +1,60 @@ +using Plugin.Settings; +using Plugin.Settings.Abstractions; + +namespace CodeHub.Core +{ + public static class Settings + { + private const string DefaultAccountKey = "DEFAULT_ACCOUNT"; + private const string ShouldStarKey = "SHOULD_STAR_CODEHUB"; + private const string ShouldWatchKey = "SHOULD_WATCH_CODEHUB"; + private const string HasSeenWelcomeKey = "HAS_SEEN_WELCOME_INTRO"; + private const string ProEditionKey = "com.dillonbuchanan.codehub.pro"; + private const string HasSeenOAuthKey = "HAS_SEEN_OAUTH_INFO"; + private const string ImgurUploadWarn = "IMGUR_UPLOAD_WARN"; + + private static ISettings AppSettings => CrossSettings.Current; + + public static string DefaultAccount + { + get => AppSettings.GetValueOrDefault(DefaultAccountKey, null); + set => AppSettings.AddOrUpdateValue(DefaultAccountKey, value); + } + + public static bool ShouldStar + { + get => AppSettings.GetValueOrDefault(ShouldStarKey, false); + set => AppSettings.AddOrUpdateValue(ShouldStarKey, value); + } + + public static bool ShouldWatch + { + get => AppSettings.GetValueOrDefault(ShouldWatchKey, false); + set => AppSettings.AddOrUpdateValue(ShouldWatchKey, value); + } + + public static bool HasSeenWelcome + { + get => AppSettings.GetValueOrDefault(HasSeenWelcomeKey, false); + set => AppSettings.AddOrUpdateValue(HasSeenWelcomeKey, value); + } + + public static bool IsProEnabled + { + get => AppSettings.GetValueOrDefault(ProEditionKey, false); + set => AppSettings.AddOrUpdateValue(ProEditionKey, value); + } + + public static bool HasSeenOAuthWelcome + { + get => AppSettings.GetValueOrDefault(HasSeenOAuthKey, false); + set => AppSettings.AddOrUpdateValue(HasSeenOAuthKey, value); + } + + public static bool HasSeenImgurUploadWarn + { + get => AppSettings.GetValueOrDefault(ImgurUploadWarn, false); + set => AppSettings.AddOrUpdateValue(ImgurUploadWarn, value); + } + } +} \ No newline at end of file diff --git a/CodeHub.Core/Utils/DefaultStartupViewCommand.cs b/CodeHub.Core/Utils/DefaultStartupViewCommand.cs deleted file mode 100644 index 5f1c9134..00000000 --- a/CodeHub.Core/Utils/DefaultStartupViewCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace CodeHub.Core.Utils -{ - public class PotentialStartupViewAttribute : Attribute - { - public string Name { get; private set; } - - public PotentialStartupViewAttribute(string name) - { - Name = name; - } - } - - public class DefaultStartupViewAttribute : Attribute - { - } -} - diff --git a/CodeHub.iOS/Utilities/Emojis.cs b/CodeHub.Core/Utils/Emojis.cs similarity index 99% rename from CodeHub.iOS/Utilities/Emojis.cs rename to CodeHub.Core/Utils/Emojis.cs index 02278a33..6764b296 100644 --- a/CodeHub.iOS/Utilities/Emojis.cs +++ b/CodeHub.Core/Utils/Emojis.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace CodeHub.iOS.Utilities +namespace CodeHub.Core.Utilities { public static class Emojis { diff --git a/CodeHub.Core/Utils/FireAndForgetTask.cs b/CodeHub.Core/Utils/FireAndForgetTask.cs deleted file mode 100755 index 716ffa4c..00000000 --- a/CodeHub.Core/Utils/FireAndForgetTask.cs +++ /dev/null @@ -1,24 +0,0 @@ - -namespace System.Threading.Tasks -{ - public static class FireAndForgetTask - { - public static Task FireAndForget(this Task task) - { - return task.ContinueWith(t => - { - if (t.IsFaulted) - { - var aggException = t.Exception.Flatten(); - foreach(var exception in aggException.InnerExceptions) - System.Diagnostics.Debug.WriteLine("Fire and Forget failed: " + exception.Message + " - " + exception.StackTrace); - } - else if (t.IsCanceled) - { - System.Diagnostics.Debug.WriteLine("Fire and forget canceled."); - } - }); - } - } -} - diff --git a/CodeHub.Core/Utils/GitHubList.cs b/CodeHub.Core/Utils/GitHubList.cs new file mode 100644 index 00000000..f127b972 --- /dev/null +++ b/CodeHub.Core/Utils/GitHubList.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Octokit; + +namespace CodeHub.Core.Utils +{ + public class GitHubList + { + private readonly GitHubClient _client; + private Uri _uri; + private readonly IDictionary _parameters; + + public GitHubList( + GitHubClient client, + Uri uri, + IDictionary parameters = null) + { + _client = client; + _uri = uri; + _parameters = parameters; + } + + public async Task> Next() + { + if (_uri == null) + return null; + + var ret = await _client.Connection.Get>( + _uri, _parameters, "application/json"); + + _uri = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") + ? ret.HttpResponse.ApiInfo.Links["next"] + : null; + + return ret.Body; + } + } +} diff --git a/CodeHub.Core/Utils/OctokitClientFactory.cs b/CodeHub.Core/Utils/OctokitClientFactory.cs new file mode 100644 index 00000000..a843bebf --- /dev/null +++ b/CodeHub.Core/Utils/OctokitClientFactory.cs @@ -0,0 +1,38 @@ +using System; +using Octokit.Internal; +using Splat; +using Octokit; +using System.Net.Http; +using CodeHub.Core.Services; + +namespace CodeHub.Core.Utilities +{ + public static class OctokitClientFactory + { + public static Func CreateMessageHandler = () => new HttpClientHandler(); + public static readonly string[] Scopes = { "user", "repo", "gist", "notifications" }; + public static readonly ProductHeaderValue UserAgent = new ProductHeaderValue("CodeHub"); + + public static GitHubClient Create( + Uri domain, + Credentials credentials, + TimeSpan? requestTimeout = null) + { + var networkActivityService = Locator.Current.GetService(); + var client = new HttpClientAdapter(CreateMessageHandler); + var httpClient = new OctokitNetworkClient(client, networkActivityService); + + var connection = new Connection( + UserAgent, + domain, + new InMemoryCredentialStore(credentials), + httpClient, + new SimpleJsonSerializer()); + + var gitHubClient = new GitHubClient(connection); + gitHubClient.SetRequestTimeout(requestTimeout ?? TimeSpan.FromSeconds(20)); + return gitHubClient; + } + } +} + diff --git a/CodeHub.Core/Utils/OctokitNetworkClient.cs b/CodeHub.Core/Utils/OctokitNetworkClient.cs new file mode 100644 index 00000000..322ce830 --- /dev/null +++ b/CodeHub.Core/Utils/OctokitNetworkClient.cs @@ -0,0 +1,35 @@ +using Octokit.Internal; +using Octokit; +using System.Threading.Tasks; +using CodeHub.Core.Services; +using System; + +namespace CodeHub.Core.Utilities +{ + /// + /// A decorator class for the object which will + /// trigger the network activity spinner + /// + class OctokitNetworkClient : IHttpClient + { + private readonly IHttpClient _httpClient; + private readonly INetworkActivityService _networkActivity; + + public OctokitNetworkClient(IHttpClient httpClient, INetworkActivityService networkActivity) + { + _httpClient = httpClient; + _networkActivity = networkActivity; + } + + public async Task Send(IRequest request, System.Threading.CancellationToken cancellationToken) + { + using (_networkActivity.ActivateNetwork()) + return await _httpClient.Send(request, cancellationToken); + } + + public void Dispose() => _httpClient.Dispose(); + + public void SetRequestTimeout(TimeSpan timeout) => _httpClient.SetRequestTimeout(timeout); + } +} + diff --git a/CodeHub.Core/Utils/RepositoryIdentifier.cs b/CodeHub.Core/Utils/RepositoryIdentifier.cs index 1b1e143c..052f25cf 100755 --- a/CodeHub.Core/Utils/RepositoryIdentifier.cs +++ b/CodeHub.Core/Utils/RepositoryIdentifier.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace CodeHub.Core.Utils { public class RepositoryIdentifier @@ -14,7 +16,9 @@ public RepositoryIdentifier(string owner, string name) public static RepositoryIdentifier FromFullName(string id) { var split = id.Split(new [] { '/' }, 2); - return split.Length != 2 ? null : new RepositoryIdentifier(split[0], split[1]); + if (split.Length != 2 || split.Any(string.IsNullOrEmpty)) + return null; + return new RepositoryIdentifier(split[0], split[1]); } } } diff --git a/CodeHub.Core/ViewModels/Accounts/AddAccountViewModel.cs b/CodeHub.Core/ViewModels/Accounts/AddAccountViewModel.cs index fd11c5a8..83d72dce 100755 --- a/CodeHub.Core/ViewModels/Accounts/AddAccountViewModel.cs +++ b/CodeHub.Core/ViewModels/Accounts/AddAccountViewModel.cs @@ -1,28 +1,20 @@ using System; using System.Threading.Tasks; -using CodeHub.Core.Data; using CodeHub.Core.Services; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Factories; -using MvvmCross.Core.ViewModels; using CodeHub.Core.Messages; -using GitHubSharp; using System.Reactive; +using ReactiveUI; +using Splat; +using System.Reactive.Linq; +using GitHubSharp; +using System.Reactive.Threading.Tasks; namespace CodeHub.Core.ViewModels.Accounts { - public class AddAccountViewModel : BaseViewModel + public class AddAccountViewModel : ReactiveObject { - private GitHubAccount _attemptedAccount; - private readonly IApplicationService _application; - private readonly ILoginFactory _loginFactory; - - private bool _isLoggingIn; - public bool IsLoggingIn - { - get { return _isLoggingIn; } - set { this.RaiseAndSetIfChanged(ref _isLoggingIn, value); } - } + private readonly ILoginService _loginService; + private readonly IAlertDialogService _alertDialogService; private string _username; public string Username @@ -45,71 +37,110 @@ public string Domain set { this.RaiseAndSetIfChanged(ref _domain, value); } } + private string _token; + public string Token + { + get { return _token; } + set { this.RaiseAndSetIfChanged(ref _token, value); } + } + + private bool _tokenAuthentication; + public bool TokenAuthentication + { + get { return _tokenAuthentication; } + set { this.RaiseAndSetIfChanged(ref _tokenAuthentication, value); } + } + public string TwoFactor { get; set; } - public ReactiveUI.IReactiveCommand LoginCommand { get; } + public ReactiveCommand LoginCommand { get; } - public AddAccountViewModel(IApplicationService application, ILoginFactory loginFactory) + public AddAccountViewModel( + ILoginService loginService = null, + IAlertDialogService alertDialogService = null) { - _application = application; - _loginFactory = loginFactory; - LoginCommand = ReactiveUI.ReactiveCommand.CreateAsyncTask(_ => Login()); + _loginService = loginService ?? Locator.Current.GetService(); + _alertDialogService = alertDialogService ?? Locator.Current.GetService(); + + LoginCommand = ReactiveCommand.CreateFromTask(Login); + + LoginCommand + .Subscribe(x => MessageBus.Current.SendMessage(new LogoutMessage())); + + LoginCommand + .ThrownExceptions + .SelectMany(HandleLoginException) + .SelectMany(Interactions.Errors.Handle) + .Subscribe(); } - public void Init(NavObject navObject) + private IObservable HandleLoginException(Exception e) { - if (navObject.AttemptedAccountId >= 0) - _attemptedAccount = this.GetApplication().Accounts.Find(navObject.AttemptedAccountId); + TwoFactor = null; + + if (e is Octokit.TwoFactorRequiredException) + { + _alertDialogService + .PromptTextBox("Authentication Error", + "Please provide the two-factor authentication code for this account.", + string.Empty, "Login") + .ToObservable() + .Do(x => TwoFactor = x) + .Select(_ => Unit.Default) + .InvokeReactiveCommand(LoginCommand); + + return Observable.Empty(); + } + + if (e is Octokit.NotFoundException err) + { + return Observable.Return( + new UserError($"The provided domain was incorrect. The host could not be found.")); + } - if (_attemptedAccount != null) + if (e is Octokit.ForbiddenException && TokenAuthentication) { - Username = _attemptedAccount.Username; - Domain = _attemptedAccount.Domain; + return Observable.Return( + new UserError("The provided token is invalid! Please try again or " + + "create a new token as this one might have been revoked.")); } + + return Observable.Return(new UserError("Unable to login!", e)); } private async Task Login() { - if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) - return; + if (string.IsNullOrEmpty(Domain)) + throw new ArgumentException("Must have a valid GitHub domain"); + if (!Uri.TryCreate(Domain, UriKind.Absolute, out Uri domainUri)) + throw new Exception("The provided domain is not a valid URL."); var apiUrl = Domain; if (apiUrl != null) { - if (!apiUrl.StartsWith("http://") && !apiUrl.StartsWith("https://")) - apiUrl = "https://" + apiUrl; - if (!apiUrl.EndsWith("/")) + if (!apiUrl.EndsWith("/", StringComparison.Ordinal)) apiUrl += "/"; - if (!apiUrl.Contains("/api/")) + if (!apiUrl.Contains("/api/")) apiUrl += "api/v3/"; } - try + if (TokenAuthentication) { - IsLoggingIn = true; - var account = await _loginFactory.LoginWithBasic(apiUrl, Username, Password, TwoFactor); - var client = await _loginFactory.LoginAccount(account); - _application.ActivateUser(account, client); - ReactiveUI.MessageBus.Current.SendMessage(new LogoutMessage()); - } - catch (Exception) - { - TwoFactor = null; - throw; - } - finally - { - IsLoggingIn = false; - } - } + var trimmedToken = Token?.Trim() ?? string.Empty; - public class NavObject - { - public int AttemptedAccountId { get; set; } + if (string.IsNullOrEmpty(trimmedToken)) + throw new ArgumentException("Must have a valid token"); - public NavObject() + await _loginService.LoginWithToken(apiUrl, Domain, trimmedToken, true); + } + else { - AttemptedAccountId = int.MinValue; + if (string.IsNullOrEmpty(Username)) + throw new ArgumentException("Must have a valid username"); + if (string.IsNullOrEmpty(Password)) + throw new ArgumentException("Must have a valid password"); + + await _loginService.LoginWithBasic(apiUrl, Username, Password, TwoFactor); } } } diff --git a/CodeHub.Core/ViewModels/Accounts/LoginViewModel.cs b/CodeHub.Core/ViewModels/Accounts/LoginViewModel.cs deleted file mode 100644 index cf89e129..00000000 --- a/CodeHub.Core/ViewModels/Accounts/LoginViewModel.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Data; -using System.Threading.Tasks; -using CodeHub.Core.Factories; -using ReactiveUI; -using CodeHub.Core.Messages; - -namespace CodeHub.Core.ViewModels.Accounts -{ - public class LoginViewModel : BaseViewModel - { - public static readonly string RedirectUri = "http://dillonbuchanan.com/"; - private readonly ILoginFactory _loginFactory; - - private bool _isLoggingIn; - public bool IsLoggingIn - { - get { return _isLoggingIn; } - set { this.RaiseAndSetIfChanged(ref _isLoggingIn, value); } - } - - public string LoginUrl - { - get - { - var web = WebDomain.TrimEnd('/'); - return string.Format( - web + "/login/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}", - Secrets.GithubOAuthId, - Uri.EscapeDataString(LoginViewModel.RedirectUri), - Uri.EscapeDataString("user,repo,notifications,gist")); - } - } - - public GitHubAccount AttemptedAccount { get; private set; } - - public string WebDomain { get; set; } - - public LoginViewModel(ILoginFactory loginFactory) - { - _loginFactory = loginFactory; - Title = "Login"; - } - - public void Init(NavObject navObject) - { - WebDomain = navObject.WebDomain ?? GitHubSharp.Client.AccessTokenUri; - - if (navObject.AttemptedAccountId >= 0) - { - AttemptedAccount = this.GetApplication().Accounts.Find(navObject.AttemptedAccountId); - } - } - - public async Task Login(string code) - { - LoginData loginData = null; - - try - { - IsLoggingIn = true; - loginData = await _loginFactory.LoginWithToken(Secrets.GithubOAuthId, Secrets.GithubOAuthSecret, - code, RedirectUri, WebDomain, GitHubSharp.Client.DefaultApi); - } - catch (Exception e) - { - DisplayAlert(e.Message); - return; - } - finally - { - IsLoggingIn = false; - } - - this.GetApplication().ActivateUser(loginData.Account, loginData.Client); - MessageBus.Current.SendMessage(new LogoutMessage()); - } - - public class NavObject - { - public string Username { get; set; } - public string WebDomain { get; set; } - public int AttemptedAccountId { get; set; } - - public NavObject() - { - AttemptedAccountId = int.MinValue; - } - - public static NavObject CreateDontRemember(GitHubAccount account) - { - return new NavObject - { - WebDomain = account.WebDomain, - Username = account.Username, - AttemptedAccountId = account.Id - }; - } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Accounts/OAuthLoginViewModel.cs b/CodeHub.Core/ViewModels/Accounts/OAuthLoginViewModel.cs new file mode 100644 index 00000000..e055b022 --- /dev/null +++ b/CodeHub.Core/ViewModels/Accounts/OAuthLoginViewModel.cs @@ -0,0 +1,54 @@ +using System; +using ReactiveUI; +using CodeHub.Core.Messages; +using CodeHub.Core.Services; +using Splat; +using System.Reactive; + +namespace CodeHub.Core.ViewModels.Accounts +{ + public class OAuthLoginViewModel : ReactiveObject + { + public static readonly string RedirectUri = "http://dillonbuchanan.com/"; + private readonly ILoginService _loginService; + private readonly IAlertDialogService _alertDialogService; + + public string LoginUrl + { + get + { + var web = WebDomain.TrimEnd('/'); + return string.Format( + web + "/login/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}", + Secrets.GithubOAuthId, + Uri.EscapeDataString(OAuthLoginViewModel.RedirectUri), + Uri.EscapeDataString("user:follow,repo,notifications,gist,read:org")); + } + } + + public string WebDomain { get; set; } = GitHubSharp.Client.AccessTokenUri; + + public ReactiveCommand LoginCommand { get; } + + public OAuthLoginViewModel( + ILoginService loginService = null, + IAlertDialogService alertDialogService = null) + { + _loginService = loginService ?? Locator.Current.GetService(); + _alertDialogService = alertDialogService ?? Locator.Current.GetService(); + + LoginCommand = ReactiveCommand.CreateFromTask(async code => + { + await _loginService.LoginWithToken( + Secrets.GithubOAuthId, Secrets.GithubOAuthSecret, + code, RedirectUri, WebDomain, GitHubSharp.Client.DefaultApi); + MessageBus.Current.SendMessage(new LogoutMessage()); + }); + + LoginCommand + .ThrownExceptions + .Subscribe(err => _alertDialogService.Alert("Error!", err.Message).ToBackground()); + } + } +} + diff --git a/CodeHub.Core/ViewModels/App/BaseDefaultStartupViewModel.cs b/CodeHub.Core/ViewModels/App/BaseDefaultStartupViewModel.cs deleted file mode 100644 index e9894415..00000000 --- a/CodeHub.Core/ViewModels/App/BaseDefaultStartupViewModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Linq; -using CodeHub.Core.Utils; -using CodeHub.Core.Services; -using System; -using MvvmCross.Core.ViewModels; - -namespace CodeHub.Core.ViewModels.App -{ - public abstract class BaseDefaultStartupViewModel : BaseViewModel - { - private readonly IAccountsService _accountsService; - private readonly Type _menuViewModelType; - - private readonly CollectionViewModel _startupViews = new CollectionViewModel(); - public CollectionViewModel StartupViews - { - get { return _startupViews; } - } - - private string _selectedStartupView; - public string SelectedStartupView - { - get { return _selectedStartupView; } - set - { - _selectedStartupView = value; - RaisePropertyChanged(() => SelectedStartupView); - } - } - - protected BaseDefaultStartupViewModel(IAccountsService accountsService, Type menuViewModelType) - { - _accountsService = accountsService; - _menuViewModelType = menuViewModelType; - } - - public void Init() - { - var props = from p in _menuViewModelType.GetProperties() - let attr = p.GetCustomAttributes(typeof(PotentialStartupViewAttribute), true) - where attr.Length == 1 - select attr[0] as PotentialStartupViewAttribute; - - SelectedStartupView = _accountsService.ActiveAccount.DefaultStartupView; - StartupViews.Items.Reset(props.Select(x => x.Name)); - - this.Bind(x => SelectedStartupView).Subscribe(x => - { - _accountsService.ActiveAccount.DefaultStartupView = x; - _accountsService.Update(_accountsService.ActiveAccount); - ChangePresentation(new MvxClosePresentationHint(this)); - }); - } - } -} - diff --git a/CodeHub.Core/ViewModels/App/BaseMenuViewModel.cs b/CodeHub.Core/ViewModels/App/BaseMenuViewModel.cs deleted file mode 100644 index 3dd6d113..00000000 --- a/CodeHub.Core/ViewModels/App/BaseMenuViewModel.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using System.Windows.Input; -using CodeHub.Core.Utils; -using System.Collections.Generic; -using CodeHub.Core.Services; -using MvvmCross.Core.ViewModels; -using MvvmCross.Platform; - -namespace CodeHub.Core.ViewModels.App -{ - public abstract class BaseMenuViewModel : BaseViewModel - { - private static readonly IDictionary Presentation = new Dictionary {{PresentationValues.SlideoutRootPresentation, string.Empty}}; - - private static IAccountsService Accounts - { - get { return Mvx.Resolve(); } - } - - public ICommand GoToDefaultTopView - { - get - { - var startupViewName = Accounts.ActiveAccount.DefaultStartupView; - if (!string.IsNullOrEmpty(startupViewName)) - { - var props = from p in GetType().GetProperties() - let attr = p.GetCustomAttributes(typeof(PotentialStartupViewAttribute), true) - where attr.Length == 1 - select new { Property = p, Attribute = attr[0] as PotentialStartupViewAttribute}; - - foreach (var p in props) - { - if (string.Equals(startupViewName, p.Attribute.Name)) - return p.Property.GetValue(this) as ICommand; - } - } - - //Oh no... Look for the last resort DefaultStartupViewAttribute - var deprop = (from p in GetType().GetProperties() - let attr = p.GetCustomAttributes(typeof(DefaultStartupViewAttribute), true) - where attr.Length == 1 - select new { Property = p, Attribute = attr[0] as DefaultStartupViewAttribute }).FirstOrDefault(); - - //That shouldn't happen... - if (deprop == null) - return null; - var val = deprop.Property.GetValue(this); - return val as ICommand; - } - } - - public ICommand DeletePinnedRepositoryCommand - { - get - { - return new MvxCommand(x => Accounts.ActiveAccount.PinnnedRepositories.RemovePinnedRepository(x.Id), x => x != null); - } - } - - protected bool ShowMenuViewModel(object data) where T : IMvxViewModel - { - return this.ShowViewModel(data, new MvxBundle(Presentation)); - } - - public IEnumerable PinnedRepositories - { - get { return Accounts.ActiveAccount.PinnnedRepositories; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/App/DefaultStartupViewModel.cs b/CodeHub.Core/ViewModels/App/DefaultStartupViewModel.cs deleted file mode 100644 index 84600614..00000000 --- a/CodeHub.Core/ViewModels/App/DefaultStartupViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CodeHub.Core.ViewModels.App; -using CodeHub.Core.Services; - -namespace CodeHub.Core.ViewModels.App -{ - public class DefaultStartupViewModel : BaseDefaultStartupViewModel - { - public DefaultStartupViewModel(IAccountsService accountsService) - : base(accountsService, typeof(MenuViewModel)) - { - } - } -} - diff --git a/CodeHub.Core/ViewModels/App/FeedbackComposerViewModel.cs b/CodeHub.Core/ViewModels/App/FeedbackComposerViewModel.cs new file mode 100644 index 00000000..07e54656 --- /dev/null +++ b/CodeHub.Core/ViewModels/App/FeedbackComposerViewModel.cs @@ -0,0 +1,72 @@ +using ReactiveUI; +using System.Reactive; +using CodeHub.Core.Services; +using System; +using System.Linq; +using System.Reactive.Linq; +using Humanizer; +using Splat; + +namespace CodeHub.Core.ViewModels.App +{ + public class FeedbackComposerViewModel : ReactiveObject + { + private const string CodeHubOwner = "codehubapp"; + private const string CodeHubName = "codehub"; + + private string _subject; + public string Subject + { + get { return _subject; } + set { this.RaiseAndSetIfChanged(ref _subject, value); } + } + + private string _description; + public string Description + { + get { return _description; } + set { this.RaiseAndSetIfChanged(ref _description, value); } + } + + public string Title => "Open Issue"; + + public ReactiveCommand SubmitCommand { get; private set; } + + public ReactiveCommand DismissCommand { get; private set; } + + public FeedbackComposerViewModel( + IApplicationService applicationService = null, + IAlertDialogService alertDialogService = null) + { + applicationService = applicationService ?? Locator.Current.GetService(); + alertDialogService = alertDialogService ?? Locator.Current.GetService(); + + SubmitCommand = ReactiveCommand.CreateFromTask(async _ => + { + if (string.IsNullOrEmpty(Subject)) + throw new ArgumentException("You must provide a title for this issue!"); + + var createIssueRequest = new Octokit.NewIssue(Subject) { Body = Description }; + + await applicationService.GitHubClient.Issue.Create(CodeHubOwner, CodeHubName, createIssueRequest); + }, this.WhenAnyValue(x => x.Subject).Select(x => !string.IsNullOrEmpty(x))); + + SubmitCommand + .ThrownExceptions + .Select(ex => new UserError("There was a problem trying to post your feedback.", ex)) + .SelectMany(Interactions.Errors.Handle) + .Subscribe(); + + DismissCommand = ReactiveCommand.CreateFromTask(async t => + { + if (string.IsNullOrEmpty(Description) && string.IsNullOrEmpty(Subject)) + return true; + + return await alertDialogService.PromptYesNo( + "Discard Issue?", + "Are you sure you want to discard this issue?"); + }); + } + } +} + diff --git a/CodeHub.Core/ViewModels/App/FeedbackItemViewModel.cs b/CodeHub.Core/ViewModels/App/FeedbackItemViewModel.cs new file mode 100644 index 00000000..10894c47 --- /dev/null +++ b/CodeHub.Core/ViewModels/App/FeedbackItemViewModel.cs @@ -0,0 +1,38 @@ +using System; +using ReactiveUI; +using Humanizer; +using System.Reactive; + +namespace CodeHub.Core.ViewModels.App +{ + public class FeedbackItemViewModel : ReactiveObject, ICanGoToViewModel + { + public string Title { get; } + + public string ImageUrl { get; } + + public ReactiveCommand GoToCommand { get; } = ReactiveCommand.Create(() => { }); + + public DateTimeOffset Created { get; } + + public string CreatedString { get; } + + public string RepositoryName { get; } + + public string RepositoryOwner { get; } + + public int IssueId { get; } + + internal FeedbackItemViewModel(string repositoryOwner, string repositoryName, Octokit.Issue issue) + { + RepositoryOwner = repositoryOwner; + RepositoryName = repositoryName; + IssueId = issue.Number; + Title = issue.Title; + ImageUrl = issue.User.AvatarUrl; + Created = issue.CreatedAt; + CreatedString = Created.Humanize(); + } + } +} + diff --git a/CodeHub.Core/ViewModels/App/FeedbackViewModel.cs b/CodeHub.Core/ViewModels/App/FeedbackViewModel.cs new file mode 100644 index 00000000..4b04d482 --- /dev/null +++ b/CodeHub.Core/ViewModels/App/FeedbackViewModel.cs @@ -0,0 +1,67 @@ +using System; +using ReactiveUI; +using CodeHub.Core.Services; +using System.Reactive; +using Splat; +using Octokit; + +namespace CodeHub.Core.ViewModels.App +{ + public class FeedbackViewModel : ReactiveObject, IProvidesSearchKeyword + { + private const string CodeHubOwner = "codehubapp"; + private const string CodeHubName = "codehub"; + + public IReadOnlyReactiveList Items { get; } + + private string _searchKeyword; + public string SearchKeyword + { + get { return _searchKeyword; } + set { this.RaiseAndSetIfChanged(ref _searchKeyword, value); } + } + + private bool? _isEmpty; + public bool? IsEmpty + { + get { return _isEmpty; } + set { this.RaiseAndSetIfChanged(ref _isEmpty, value); } + } + + public string Title => "Feedback"; + + public ReactiveCommand LoadCommand { get; private set; } + + public FeedbackViewModel(IApplicationService applicationService = null) + { + applicationService = applicationService ?? Locator.Current.GetService(); + + var items = new ReactiveList(resetChangeThreshold: 1); + var feedbackItems = items.CreateDerivedCollection( + x => new FeedbackItemViewModel(CodeHubOwner, CodeHubName, x)); + + Items = feedbackItems.CreateDerivedCollection( + x => x, + x => x.Title.ContainsKeyword(SearchKeyword), + signalReset: this.WhenAnyValue(x => x.SearchKeyword)); + + LoadCommand = ReactiveCommand.CreateFromTask(async _ => + { + items.Clear(); + + var list = applicationService.GitHubClient.RetrieveList( + ApiUrls.Issues(CodeHubOwner, CodeHubName)); + + var issues = await list.Next(); + IsEmpty = issues?.Count > 0; + + while (issues != null) + { + items.AddRange(issues); + issues = await list.Next(); + } + }); + } + } +} + diff --git a/CodeHub.Core/ViewModels/App/MenuViewModel.cs b/CodeHub.Core/ViewModels/App/MenuViewModel.cs index 4cadb68e..a900d3c4 100644 --- a/CodeHub.Core/ViewModels/App/MenuViewModel.cs +++ b/CodeHub.Core/ViewModels/App/MenuViewModel.cs @@ -3,30 +3,24 @@ using CodeHub.Core.Data; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Events; -using CodeHub.Core.ViewModels.Gists; using CodeHub.Core.ViewModels.Issues; -using CodeHub.Core.ViewModels.Repositories; using CodeHub.Core.ViewModels.User; using System.Linq; -using CodeHub.Core.Utils; -using CodeHub.Core.ViewModels.App; using CodeHub.Core.Messages; using CodeHub.Core.ViewModels.Notifications; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; using MvvmCross.Core.ViewModels; -using ReactiveUI; +using System; namespace CodeHub.Core.ViewModels.App { - public class MenuViewModel : BaseMenuViewModel + public class MenuViewModel : BaseViewModel { - private readonly IApplicationService _application; + private readonly IApplicationService _applicationService; private readonly IFeaturesService _featuresService; private int _notifications; private List _organizations; - private readonly MvxSubscriptionToken _notificationCountToken; + private readonly IDisposable _notificationCountToken; public int Notifications { @@ -40,9 +34,9 @@ public List Organizations set { _organizations = value; RaisePropertyChanged(); } } - public GitHubAccount Account + public Account Account { - get { return _application.Account; } + get { return _applicationService.Account; } } public bool ShouldShowUpgrades @@ -50,11 +44,15 @@ public bool ShouldShowUpgrades get { return !_featuresService.IsProEnabled; } } - public MenuViewModel(IApplicationService application, IFeaturesService featuresService) + public MenuViewModel(IApplicationService application = null, + IFeaturesService featuresService = null, + IMessageService messageService = null) { - _application = application; - _featuresService = featuresService; - _notificationCountToken = Messenger.SubscribeOnMainThread(OnNotificationCountMessage); + _applicationService = application ?? GetService(); + _featuresService = featuresService ?? GetService(); + messageService = messageService ?? GetService(); + + _notificationCountToken = messageService.Listen(OnNotificationCountMessage); } private void OnNotificationCountMessage(NotificationCountMessage msg) @@ -62,74 +60,26 @@ private void OnNotificationCountMessage(NotificationCountMessage msg) Notifications = msg.Count; } - public IReactiveCommand GoToAccountsCommand { get; } = ReactiveCommand.Create(); - - [PotentialStartupViewAttribute("Profile")] public ICommand GoToProfileCommand { - get { return new MvxCommand(() => ShowMenuViewModel(new UserViewModel.NavObject { Username = _application.Account.Username })); } + get { return new MvxCommand(() => ShowMenuViewModel(new UserViewModel.NavObject { Username = _applicationService.Account.Username })); } } - [PotentialStartupViewAttribute("Notifications")] public ICommand GoToNotificationsCommand { get { return new MvxCommand(() => ShowMenuViewModel(null)); } } - [PotentialStartupViewAttribute("My Issues")] public ICommand GoToMyIssuesCommand { get { return new MvxCommand(() => ShowMenuViewModel(null)); } } - [PotentialStartupViewAttribute("My Events")] public ICommand GoToMyEvents { get { return new MvxCommand(() => ShowMenuViewModel(new UserEventsViewModel.NavObject { Username = Account.Username })); } } - [PotentialStartupViewAttribute("My Gists")] - public ICommand GoToMyGistsCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(new UserGistsViewModel.NavObject { Username = Account.Username}));} - } - - [PotentialStartupViewAttribute("Starred Gists")] - public ICommand GoToStarredGistsCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null)); } - } - - [PotentialStartupViewAttribute("Public Gists")] - public ICommand GoToPublicGistsCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null)); } - } - - [PotentialStartupViewAttribute("Starred Repositories")] - public ICommand GoToStarredRepositoriesCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null));} - } - - [PotentialStartupViewAttribute("Owned Repositories")] - public ICommand GoToOwnedRepositoriesCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(new UserRepositoriesViewModel.NavObject { Username = Account.Username }));} - } - - [PotentialStartupViewAttribute("Explore Repositories")] - public ICommand GoToExploreRepositoriesCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null));} - } - - [PotentialStartupViewAttribute("Trending Repositories")] - public ICommand GoToTrendingRepositoriesCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null));} - } - public ICommand GoToOrganizationEventsCommand { get { return new MvxCommand(x => ShowMenuViewModel(new Events.UserEventsViewModel.NavObject { Username = x }));} @@ -140,39 +90,16 @@ public ICommand GoToOrganizationCommand get { return new MvxCommand(x => ShowMenuViewModel(new Organizations.OrganizationViewModel.NavObject { Name = x }));} } - [PotentialStartupViewAttribute("Organizations")] public ICommand GoToOrganizationsCommand { get { return new MvxCommand(() => ShowMenuViewModel(new Organizations.OrganizationsViewModel.NavObject { Username = Account.Username }));} } - [DefaultStartupViewAttribute] - [PotentialStartupViewAttribute("News")] public ICommand GoToNewsCommand { get { return new MvxCommand(() => ShowMenuViewModel(null));} } - public ICommand GoToSettingsCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null));} - } - - public ICommand GoToSupport - { - get { return new MvxCommand(() => ShowMenuViewModel(new WebBrowserViewModel.NavObject { Url = "https://codehub.uservoice.com/" })); } - } - - public ICommand GoToRepositoryCommand - { - get { return new MvxCommand(x => ShowMenuViewModel(new RepositoryViewModel.NavObject { Username = x.Owner, Repository = x.Name }));} - } - - public ICommand GoToUpgradesCommand - { - get { return new MvxCommand(() => ShowMenuViewModel(null)); } - } - public ICommand LoadCommand { get { return new MvxCommand(Load);} @@ -189,37 +116,61 @@ private void Load() .ToBackground(x => Organizations = x.Data.ToList()); } -// -// private async Task PromptForPushNotifications() -// { -// // Push notifications are not enabled for enterprise -// if (Account.IsEnterprise) -// return; -// -// try -// { -// var features = Mvx.Resolve(); -// var alertDialog = Mvx.Resolve(); -// var push = Mvx.Resolve(); -// var -// // Check for push notifications -// if (Account.IsPushNotificationsEnabled == null && features.IsPushNotificationsActivated) -// { -// var result = await alertDialog.PromptYesNo("Push Notifications", "Would you like to enable push notifications for this account?"); -// if (result) -// Task.Run(() => push.Register()).FireAndForget(); -// Account.IsPushNotificationsEnabled = result; -// Accounts.Update(Account); -// } -// else if (Account.IsPushNotificationsEnabled.HasValue && Account.IsPushNotificationsEnabled.Value) -// { -// Task.Run(() => push.Register()).FireAndForget(); -// } -// } -// catch (Exception e) -// { -// _alertDialogService.Alert("Error", e.Message); -// } -// } + // + // private async Task PromptForPushNotifications() + // { + // // Push notifications are not enabled for enterprise + // if (Account.IsEnterprise) + // return; + // + // try + // { + // var features = Mvx.Resolve(); + // var alertDialog = Mvx.Resolve(); + // var push = Mvx.Resolve(); + // var + // // Check for push notifications + // if (Account.IsPushNotificationsEnabled == null && features.IsPushNotificationsActivated) + // { + // var result = await alertDialog.PromptYesNo("Push Notifications", "Would you like to enable push notifications for this account?"); + // if (result) + // Task.Run(() => push.Register()).FireAndForget(); + // Account.IsPushNotificationsEnabled = result; + // Accounts.Update(Account); + // } + // else if (Account.IsPushNotificationsEnabled.HasValue && Account.IsPushNotificationsEnabled.Value) + // { + // Task.Run(() => push.Register()).FireAndForget(); + // } + // } + // catch (Exception e) + // { + // _alertDialogService.Alert("Error", e.Message); + // } + // } + + private static readonly IDictionary Presentation = new Dictionary { { PresentationValues.SlideoutRootPresentation, string.Empty } }; + + public ICommand DeletePinnedRepositoryCommand + { + get + { + return new MvxCommand(x => + { + Account.PinnedRepositories.Remove(x); + _applicationService.UpdateActiveAccount().ToBackground(); + }, x => x != null); + } + } + + protected bool ShowMenuViewModel(object data) where T : IMvxViewModel + { + return this.ShowViewModel(data, new MvxBundle(Presentation)); + } + + public IEnumerable PinnedRepositories + { + get { return Account.PinnedRepositories; } + } } } diff --git a/CodeHub.Core/ViewModels/App/SettingsViewModel.cs b/CodeHub.Core/ViewModels/App/SettingsViewModel.cs deleted file mode 100644 index 7b844733..00000000 --- a/CodeHub.Core/ViewModels/App/SettingsViewModel.cs +++ /dev/null @@ -1,110 +0,0 @@ -using CodeHub.Core.ViewModels; -using System.Windows.Input; -using CodeHub.Core.Services; -using System; -using System.Threading.Tasks; -using CodeHub.Core.ViewModels.Repositories; -using MvvmCross.Core.ViewModels; - -namespace CodeHub.Core.ViewModels.App -{ - public class SettingsViewModel : BaseViewModel - { - private readonly IFeaturesService _featuresService; - - public SettingsViewModel(IFeaturesService featuresService) - { - _featuresService = featuresService; - } - - public string DefaultStartupViewName - { - get { return this.GetApplication().Account.DefaultStartupView; } - } - - public bool ShouldShowUpgrades - { - get { return _featuresService.IsProEnabled; } - } - - public ICommand GoToDefaultStartupViewCommand - { - get { return new MvxCommand(() => ShowViewModel()); } - } - - public ICommand GoToSourceCodeCommand - { - get { return new MvxCommand(() => ShowViewModel(new RepositoryViewModel.NavObject { Repository = "codehub", Username = "thedillonb" })); } - } - - public ICommand GoToUpgradesCommand - { - get { return new MvxCommand(() => ShowViewModel()); } - } - - private bool _isSaving; - public bool IsSaving - { - get { return _isSaving; } - private set { this.RaiseAndSetIfChanged(ref _isSaving, value); } - } - - public bool PushNotificationsEnabled - { - get - { - return this.GetApplication().Account.IsPushNotificationsEnabled.HasValue && this.GetApplication().Account.IsPushNotificationsEnabled.Value; - } - set - { - if (_featuresService.IsProEnabled) - { - RegisterPushNotifications(value).ToBackground(); - } - else - { - GetService() - .PromptYesNo("Requires Activation", "Push notifications require activation. Would you like to go there now to activate push notifications?") - .ContinueWith(t => - { - if (t.Status == TaskStatus.RanToCompletion && t.Result) - ShowViewModel(); - }); - RaisePropertyChanged(() => PushNotificationsEnabled); - } - } - } - - private async Task RegisterPushNotifications(bool enabled) - { - var notificationService = GetService(); - - try - { - IsSaving = true; - if (enabled) - { - await notificationService.Register(); - } - else - { - await notificationService.Deregister(); - } - - this.GetApplication().Account.IsPushNotificationsEnabled = enabled; - this.GetApplication().Accounts.Update(this.GetApplication().Account); - } - catch (Exception e) - { - GetService() - .Alert("Unable to register for push notifications!", e.Message) - .ToBackground(); - } - finally - { - RaisePropertyChanged(() => PushNotificationsEnabled); - IsSaving = false; - } - } - } -} diff --git a/CodeHub.Core/ViewModels/App/StartupViewModel.cs b/CodeHub.Core/ViewModels/App/StartupViewModel.cs index 8419b9b4..4af1d7ef 100644 --- a/CodeHub.Core/ViewModels/App/StartupViewModel.cs +++ b/CodeHub.Core/ViewModels/App/StartupViewModel.cs @@ -1,15 +1,12 @@ using System; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Data; using CodeHub.Core.Services; using System.Linq; -using CodeHub.Core.Factories; using System.Windows.Input; using Dumb = MvvmCross.Core.ViewModels; using System.Threading.Tasks; using ReactiveUI; -using CodeHub.Core.ViewModels.Accounts; using System.Reactive.Threading.Tasks; +using System.Reactive; namespace CodeHub.Core.ViewModels.App { @@ -18,9 +15,8 @@ public class StartupViewModel : BaseViewModel private bool _isLoggingIn; private string _status; private Uri _imageUrl; - private readonly ILoginFactory _loginFactory; private readonly IApplicationService _applicationService; - private readonly IDefaultValueService _defaultValueService; + private readonly IAccountsService _accountsService; public bool IsLoggingIn { @@ -45,44 +41,39 @@ public ICommand StartupCommand get { return new Dumb.MvxAsyncCommand(Startup); } } - public ReactiveCommand GoToMenu { get; } = ReactiveCommand.Create(); + public Data.Account Account => _applicationService.Account; - public ReactiveCommand GoToAccounts { get; } = ReactiveCommand.Create(); + public ReactiveCommand GoToMenu { get; } = ReactiveCommand.Create(() => { }); - public ReactiveCommand GoToNewAccount { get; } = ReactiveCommand.Create(); + public ReactiveCommand GoToAccounts { get; } = ReactiveCommand.Create(() => { }); + + public ReactiveCommand GoToNewAccount { get; } = ReactiveCommand.Create(() => { }); public StartupViewModel( - ILoginFactory loginFactory, - IApplicationService applicationService, - IDefaultValueService defaultValueService) + IApplicationService applicationService = null, + IAccountsService accountsService = null) { - _loginFactory = loginFactory; - _applicationService = applicationService; - _defaultValueService = defaultValueService; + _applicationService = applicationService ?? GetService(); + _accountsService = accountsService ?? GetService(); } protected async Task Startup() { - if (!_applicationService.Accounts.Any()) + var accounts = (await _accountsService.GetAccounts()).ToList(); + if (!accounts.Any()) { - GoToNewAccount.Execute(null); + GoToNewAccount.ExecuteNow(); return; } - var accounts = GetService(); - var account = accounts.GetDefault(); + var account = await _accountsService.GetActiveAccount(); if (account == null) { - GoToAccounts.Execute(null); + GoToAccounts.ExecuteNow(); return; } var isEnterprise = account.IsEnterprise || !string.IsNullOrEmpty(account.Password); - if (account.DontRemember) - { - GoToAccounts.Execute(null); - return; - } //Lets login! try @@ -96,54 +87,55 @@ protected async Task Startup() ImageUrl = accountAvatarUri; Status = "Logging in as " + account.Username; - var client = await _loginFactory.LoginAccount(account); - _applicationService.ActivateUser(account, client); + await _applicationService.LoginAccount(account); if (!isEnterprise) StarOrWatch(); - GoToMenu.Execute(typeof(MenuViewModel)); + GoToMenu.ExecuteNow(); } - catch (GitHubSharp.UnauthorizedException e) + catch (Octokit.AuthorizationException e) { - DisplayAlertAsync("The credentials for the selected account are incorrect. " + e.Message) + DisplayAlertAsync("The credentials for the selected account are not valid. " + e.Message) .ToObservable() .BindCommand(GoToAccounts); } catch (Exception e) { DisplayAlert(e.Message); - GoToAccounts.Execute(null); + GoToAccounts.ExecuteNow(); } finally { IsLoggingIn = false; } - } private void StarOrWatch() { - try + if (Settings.ShouldStar) { - bool shouldStar; - if (_defaultValueService.TryGet("SHOULD_STAR_CODEHUB", out shouldStar) && shouldStar) - { - _defaultValueService.Clear("SHOULD_STAR_CODEHUB"); - var starRequest = _applicationService.Client.Users["thedillonb"].Repositories["codehub"].Star(); - _applicationService.Client.ExecuteAsync(starRequest).ToBackground(); - } + Settings.ShouldStar = false; - bool shouldWatch; - if (_defaultValueService.TryGet("SHOULD_WATCH_CODEHUB", out shouldWatch) && shouldWatch) - { - _defaultValueService.Clear("SHOULD_WATCH_CODEHUB"); - var watchRequest = _applicationService.Client.Users["thedillonb"].Repositories["codehub"].Watch(); - _applicationService.Client.ExecuteAsync(watchRequest).ToBackground(); - } + _applicationService + .GitHubClient.Activity.Starring + .StarRepo("codehubapp", "codehub") + .ToBackground(); } - catch + + if (Settings.ShouldWatch) { + Settings.ShouldWatch = false; + + var subscription = new Octokit.NewSubscription + { + Subscribed = true + }; + + _applicationService + .GitHubClient.Activity.Watching + .WatchRepo("codehubapp", "codehub", subscription) + .ToBackground(); } } } diff --git a/CodeHub.Core/ViewModels/App/SupportViewModel.cs b/CodeHub.Core/ViewModels/App/SupportViewModel.cs new file mode 100644 index 00000000..5ee47838 --- /dev/null +++ b/CodeHub.Core/ViewModels/App/SupportViewModel.cs @@ -0,0 +1,54 @@ +using System; +using ReactiveUI; +using CodeHub.Core.Services; +using System.Reactive.Linq; +using System.Reactive; +using Splat; + +namespace CodeHub.Core.ViewModels.App +{ + public class SupportViewModel : ReactiveObject, ILoadableViewModel + { + public readonly static string CodeHubOwner = "codehubapp"; + public readonly static string CodeHubName = "codehub"; + + private int? _contributors; + public int? Contributors + { + get { return _contributors; } + private set { this.RaiseAndSetIfChanged(ref _contributors, value); } + } + + public string Title => "Feedback & Support"; + + private readonly ObservableAsPropertyHelper _lastCommit; + public DateTimeOffset? LastCommit => _lastCommit.Value; + + private Octokit.Repository _repository; + public Octokit.Repository Repository + { + get { return _repository; } + private set { this.RaiseAndSetIfChanged(ref _repository, value); } + } + + public ReactiveCommand LoadCommand { get; } + + public SupportViewModel(IApplicationService applicationService = null) + { + applicationService = applicationService ?? Locator.Current.GetService(); + + _lastCommit = this + .WhenAnyValue(x => x.Repository).Where(x => x != null) + .Select(x => x.PushedAt).ToProperty(this, x => x.LastCommit); + + LoadCommand = ReactiveCommand.CreateFromTask(async _ => + { + applicationService.GitHubClient.Repository.GetAllContributors(CodeHubOwner, CodeHubName) + .ToBackground(x => Contributors = x.Count); + Repository = await applicationService.GitHubClient.Repository.Get(CodeHubOwner, CodeHubName); + }); + } + + } +} + diff --git a/CodeHub.Core/ViewModels/App/UpgradeViewModel.cs b/CodeHub.Core/ViewModels/App/UpgradeViewModel.cs deleted file mode 100644 index 831450d0..00000000 --- a/CodeHub.Core/ViewModels/App/UpgradeViewModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CodeHub.Core.ViewModels.App -{ - public class UpgradeViewModel : BaseViewModel - { - } -} - diff --git a/CodeHub.Core/ViewModels/BaseViewModel.cs b/CodeHub.Core/ViewModels/BaseViewModel.cs index c29550c8..a91b946a 100755 --- a/CodeHub.Core/ViewModels/BaseViewModel.cs +++ b/CodeHub.Core/ViewModels/BaseViewModel.cs @@ -1,8 +1,6 @@ using CodeHub.Core.Services; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using MvvmCross.Plugins.Messenger; using MvvmCross.Platform; using ReactiveUI; using System; @@ -102,15 +100,6 @@ protected IViewModelTxService TxSevice get { return GetService(); } } - /// - /// Gets the messenger service - /// - /// The messenger. - protected IMvxMessenger Messenger - { - get { return GetService(); } - } - /// /// Gets the alert service /// diff --git a/CodeHub.Core/ViewModels/Changesets/ChangesetViewModel.cs b/CodeHub.Core/ViewModels/Changesets/ChangesetViewModel.cs index e12928d0..a800181d 100644 --- a/CodeHub.Core/ViewModels/Changesets/ChangesetViewModel.cs +++ b/CodeHub.Core/ViewModels/Changesets/ChangesetViewModel.cs @@ -1,7 +1,5 @@ -using System; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Repositories; using GitHubSharp.Models; @@ -10,6 +8,7 @@ using MvvmCross.Platform; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; +using System.Reactive; namespace CodeHub.Core.ViewModels.Changesets { @@ -46,26 +45,6 @@ public ICommand GoToRepositoryCommand get { return new MvxCommand(() => ShowViewModel(new RepositoryViewModel.NavObject { Username = User, Repository = Repository })); } } - public ICommand GoToFileCommand - { - get - { - return new MvxCommand(x => - { - if (x.Patch == null) - { - ShowViewModel(new SourceViewModel.NavObject { GitUrl = x.ContentsUrl, HtmlUrl = x.BlobUrl, Name = x.Filename, Path = x.Filename, ForceBinary = true }); - } - else - { - Mvx.Resolve().Add(x); - ShowViewModel(new ChangesetDiffViewModel.NavObject { Username = User, Repository = Repository, Branch = _commitModel.Sha, Filename = x.Filename }); - } - - }); - } - } - public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => ShowViewModel(new WebBrowserViewModel.NavObject { Url = _commitModel.Url }), () => _commitModel != null); } @@ -76,15 +55,16 @@ public CollectionViewModel Comments get { return _comments; } } - public ReactiveUI.ReactiveCommand GoToOwner { get; } + public ReactiveUI.ReactiveCommand GoToOwner { get; } public ChangesetViewModel(IApplicationService application, IFeaturesService featuresService) { _applicationService = application; _featuresService = featuresService; - GoToOwner = ReactiveUI.ReactiveCommand.Create(this.Bind(x => x.Changeset, true).Select(x => x?.Author?.Login != null)); - GoToOwner.Subscribe(_ => ShowViewModel(new UserViewModel.NavObject { Username = Changeset?.Author?.Login })); + GoToOwner = ReactiveUI.ReactiveCommand.Create( + () => ShowViewModel(new UserViewModel.NavObject { Username = Changeset?.Author?.Login }), + this.Bind(x => x.Changeset, true).Select(x => x?.Author?.Login != null)); } public void Init(NavObject navObject) @@ -108,7 +88,7 @@ protected override Task Load() } var t1 = this.RequestModel(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Get(), response => Changeset = response.Data); - Comments.SimpleCollectionLoad(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Comments.GetAll()).FireAndForget(); + Comments.SimpleCollectionLoad(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Comments.GetAll()).ToBackground(); return t1; } diff --git a/CodeHub.Core/ViewModels/Changesets/ChangesetsViewModel.cs b/CodeHub.Core/ViewModels/Changesets/ChangesetsViewModel.cs index 426dc9c0..b06b0fb7 100755 --- a/CodeHub.Core/ViewModels/Changesets/ChangesetsViewModel.cs +++ b/CodeHub.Core/ViewModels/Changesets/ChangesetsViewModel.cs @@ -9,7 +9,9 @@ public class ChangesetsViewModel : CommitsViewModel { public string Branch { get; private set; } - public ChangesetsViewModel(IApplicationService applicationService,IFeaturesService featuresService) + public ChangesetsViewModel( + IApplicationService applicationService = null, + IFeaturesService featuresService = null) : base(applicationService, featuresService) { } diff --git a/CodeHub.Core/ViewModels/Changesets/CommitsViewModel.cs b/CodeHub.Core/ViewModels/Changesets/CommitsViewModel.cs index ebc01b2f..5cf4e29a 100644 --- a/CodeHub.Core/ViewModels/Changesets/CommitsViewModel.cs +++ b/CodeHub.Core/ViewModels/Changesets/CommitsViewModel.cs @@ -1,5 +1,3 @@ -using System; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; using System.Windows.Input; using System.Threading.Tasks; @@ -7,6 +5,7 @@ using System.Collections.Generic; using MvvmCross.Core.ViewModels; using CodeHub.Core.Services; +using Splat; namespace CodeHub.Core.ViewModels.Changesets { @@ -45,10 +44,12 @@ public CollectionViewModel Commits get { return _commits; } } - protected CommitsViewModel(IApplicationService applicationService, IFeaturesService featuresService) + protected CommitsViewModel( + IApplicationService applicationService = null, + IFeaturesService featuresService = null) { - _applicationService = applicationService; - _featuresService = featuresService; + _applicationService = applicationService ?? Locator.Current.GetService(); + _featuresService = featuresService ?? Locator.Current.GetService(); } public void Init(NavObject navObject) diff --git a/CodeHub.Core/ViewModels/Events/BaseEventsViewModel.cs b/CodeHub.Core/ViewModels/Events/BaseEventsViewModel.cs index 3e68f3ea..a45771cc 100644 --- a/CodeHub.Core/ViewModels/Events/BaseEventsViewModel.cs +++ b/CodeHub.Core/ViewModels/Events/BaseEventsViewModel.cs @@ -4,18 +4,17 @@ using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using CodeHub.Core.ViewModels.Gists; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.ViewModels.PullRequests; using CodeHub.Core.ViewModels.Repositories; -using CodeHub.Core.ViewModels.Source; using CodeHub.Core.ViewModels.User; using GitHubSharp; using GitHubSharp.Models; using CodeHub.Core.Utils; using CodeHub.Core.ViewModels.Changesets; -using System.Dynamic; +using System.Reactive; +using System.Reactive.Subjects; namespace CodeHub.Core.ViewModels.Events { @@ -23,16 +22,13 @@ public abstract class BaseEventsViewModel : LoadableViewModel { private readonly CollectionViewModel> _events = new CollectionViewModel>(); - public CollectionViewModel> Events - { - get { return _events; } - } + public readonly ISubject> GoToTagCommand = new Subject>(); - public bool ReportRepository - { - get; - private set; - } + public readonly ISubject> GoToBranchCommand = new Subject>(); + + public CollectionViewModel> Events => _events; + + public bool ReportRepository { get; private set; } protected BaseEventsViewModel() { @@ -69,6 +65,9 @@ private IEnumerable> CreateDataFromLoad(List(new ChangesetsViewModel.NavObject { Username = repoId?.Owner, @@ -85,6 +84,9 @@ public ICommand GoToRepositoryCommand private void GoToRepository(EventModel.RepoModel eventModel) { var repoId = RepositoryIdentifier.FromFullName(eventModel.Name); + if (repoId == null) + return; + ShowViewModel(new RepositoryViewModel.NavObject { Username = repoId?.Owner, @@ -99,25 +101,21 @@ private void GoToUser(string username) ShowViewModel(new UserViewModel.NavObject {Username = username}); } - private void GoToBranches(RepositoryIdentifier repoId) + private void GoToBranch(RepositoryIdentifier repoId, string branchName) { - ShowViewModel(new BranchesAndTagsViewModel.NavObject - { - Username = repoId.Owner, - Repository = repoId.Name, - IsShowingBranches = true - }); + if (repoId == null) + return; + + GoToBranchCommand.OnNext(Tuple.Create(repoId, branchName)); } - private void GoToTags(EventModel.RepoModel eventModel) + private void GoToTag(EventModel.RepoModel eventModel, string tagName) { var repoId = RepositoryIdentifier.FromFullName(eventModel.Name); - ShowViewModel(new BranchesAndTagsViewModel.NavObject - { - Username = repoId?.Owner, - Repository = repoId?.Name, - IsShowingBranches = false - }); + if (repoId == null) + return; + + GoToTagCommand.OnNext(Tuple.Create(repoId, tagName)); } public ICommand GoToGistCommand @@ -129,6 +127,7 @@ private void GoToIssue(RepositoryIdentifier repo, long id) { if (repo == null || string.IsNullOrEmpty(repo.Name) || string.IsNullOrEmpty(repo.Owner)) return; + ShowViewModel(new IssueViewModel.NavObject { Username = repo.Owner, @@ -216,7 +215,7 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) } else if (createEvent.RefType.Equals("branch")) { - eventBlock.Tapped = () => GoToBranches(repoId); + eventBlock.Tapped = () => GoToBranch(repoId, createEvent.Ref); eventBlock.Header.Add(new TextBlock(" created branch ")); eventBlock.Header.Add(new AnchorBlock(createEvent.Ref, eventBlock.Tapped)); @@ -228,7 +227,7 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) } else if (createEvent.RefType.Equals("tag")) { - eventBlock.Tapped = () => GoToTags(eventModel.Repo); + eventBlock.Tapped = () => GoToTag(eventModel.Repo, createEvent.Ref); eventBlock.Header.Add(new TextBlock(" created tag ")); eventBlock.Header.Add(new AnchorBlock(createEvent.Ref, eventBlock.Tapped)); @@ -246,12 +245,12 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) { if (deleteEvent.RefType.Equals("branch")) { - eventBlock.Tapped = () => GoToBranches(repoId); + eventBlock.Tapped = () => GoToRepository(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" deleted branch ")); } else if (deleteEvent.RefType.Equals("tag")) { - eventBlock.Tapped = () => GoToTags(eventModel.Repo); + eventBlock.Tapped = () => GoToRepository(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" deleted tag ")); } else @@ -303,7 +302,7 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) eventBlock.Header.Add(new TextBlock(" applied fork to ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Header.Add(new TextBlock(" on branch ")); - eventBlock.Header.Add(new AnchorBlock(forkEvent.Head, () => GoToBranches(repoId))); + eventBlock.Header.Add(new AnchorBlock(forkEvent.Head, () => GoToBranch(repoId, forkEvent.Head))); } /* * GIST EVENT @@ -397,9 +396,17 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); if (memberEvent.Action.Equals("added")) - eventBlock.Header.Add(new TextBlock(" added as a collaborator")); + eventBlock.Header.Add(new TextBlock(" added ")); else if (memberEvent.Action.Equals("removed")) - eventBlock.Header.Add(new TextBlock(" removed as a collaborator")); + eventBlock.Header.Add(new TextBlock(" removed ")); + + var memberName = memberEvent.Member?.Login; + if (memberName != null) + eventBlock.Header.Add(new AnchorBlock(memberName, () => GoToUser(memberName))); + else + eventBlock.Header.Add(new TextBlock("")); + + eventBlock.Header.Add(new TextBlock(" as a collaborator")); if (ReportRepository) { @@ -479,7 +486,7 @@ private EventBlock CreateEventTextBlocks(EventModel eventModel) eventBlock.Header.Add(new TextBlock(" pushed to ")); if (branchRef != null) - eventBlock.Header.Add(new AnchorBlock(branchRef, () => GoToBranches(repoId))); + eventBlock.Header.Add(new AnchorBlock(branchRef, () => GoToBranch(repoId, branchRef))); if (ReportRepository) { diff --git a/CodeHub.Core/ViewModels/FileSourceViewModel.cs b/CodeHub.Core/ViewModels/FileSourceViewModel.cs deleted file mode 100644 index 1ae04f63..00000000 --- a/CodeHub.Core/ViewModels/FileSourceViewModel.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Linq; -using System.Windows.Input; -using CodeHub.Core.ViewModels; -using MvvmCross.Core.ViewModels; - -namespace CodeHub.Core.ViewModels -{ - public abstract class FileSourceViewModel : LoadableViewModel - { - private static readonly string[] BinaryMIMEs = new string[] - { - "image/", "video/", "audio/", "model/", "application/pdf", "application/zip", "application/gzip" - }; - - private string _filePath; - public string FilePath - { - get { return _filePath; } - protected set { this.RaiseAndSetIfChanged(ref _filePath, value); } - } - - public bool IsMarkdown { get; protected set; } - - private string _contentPath; - public string ContentPath - { - get { return _contentPath; } - protected set { this.RaiseAndSetIfChanged(ref _contentPath, value); } - } - - public string HtmlUrl - { - get; - protected set; - } - - public ICommand GoToHtmlUrlCommand - { - get { return new MvxCommand(() => ShowViewModel(new WebBrowserViewModel.NavObject { Url = HtmlUrl }), () => !string.IsNullOrEmpty(HtmlUrl)); } - } - - protected static string CreatePlainContentFile(string data, string filename) - { - var filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), filename); - System.IO.File.WriteAllText(filepath, data, System.Text.Encoding.UTF8); - return filepath; - } - - protected static bool IsBinary(string mime) - { - var lowerMime = mime.ToLower(); - return BinaryMIMEs.Any(lowerMime.StartsWith); - } - } -} - diff --git a/CodeHub.Core/ViewModels/FilterableCollectionViewModel.cs b/CodeHub.Core/ViewModels/FilterableCollectionViewModel.cs old mode 100755 new mode 100644 index 90b40140..086bf45d --- a/CodeHub.Core/ViewModels/FilterableCollectionViewModel.cs +++ b/CodeHub.Core/ViewModels/FilterableCollectionViewModel.cs @@ -1,39 +1,43 @@ -using CodeHub.Core.Services; -using MvvmCross.Platform; - -namespace CodeHub.Core.ViewModels -{ - public class FilterableCollectionViewModel : CollectionViewModel, IFilterableViewModel where TF : FilterModel, new() - { - protected TF _filter; - private readonly string _filterKey; - - public TF Filter - { - get { return _filter; } - set - { - _filter = value; - RaisePropertyChanged(() => Filter); - } - } - - public FilterableCollectionViewModel(string filterKey) - { - _filterKey = filterKey; - var accounts = Mvx.Resolve(); - _filter = accounts.ActiveAccount.Filters.GetFilter(_filterKey); - } - - public void ApplyFilter(TF filter, bool saveAsDefault = false) - { - Filter = filter; - if (saveAsDefault) - { - var accounts = Mvx.Resolve(); - accounts.ActiveAccount.Filters.AddFilter(_filterKey, filter); - } - } - } -} - +using CodeHub.Core.Data; +using CodeHub.Core.Services; +using MvvmCross.Platform; + +namespace CodeHub.Core.ViewModels +{ + public class FilterableCollectionViewModel : CollectionViewModel, IFilterableViewModel where TF : FilterModel, new() + { + protected TF _filter; + private readonly string _filterKey; + + public TF Filter + { + get { return _filter; } + set + { + _filter = value; + RaisePropertyChanged(() => Filter); + } + } + + public FilterableCollectionViewModel(string filterKey) + { + _filterKey = filterKey; + var application = Mvx.Resolve(); + var accounts = Mvx.Resolve(); + _filter = application.Account.GetFilter(_filterKey) ?? new TF(); + accounts.Save(application.Account).ToBackground(); + } + + public void ApplyFilter(TF filter, bool saveAsDefault = false) + { + Filter = filter; + if (saveAsDefault) + { + var application = Mvx.Resolve(); + application.Account.SetFilter(_filterKey, _filter); + application.UpdateActiveAccount().ToBackground(); + } + } + } +} + diff --git a/CodeHub.Core/ViewModels/Gists/GistCreateViewModel.cs b/CodeHub.Core/ViewModels/Gists/GistCreateViewModel.cs index f2518ac8..c633f484 100644 --- a/CodeHub.Core/ViewModels/Gists/GistCreateViewModel.cs +++ b/CodeHub.Core/ViewModels/Gists/GistCreateViewModel.cs @@ -1,16 +1,17 @@ using System; -using CodeHub.Core.ViewModels; using System.Collections.Generic; using System.Threading.Tasks; -using GitHubSharp.Models; -using System.Linq; using CodeHub.Core.Messages; using ReactiveUI; +using System.Reactive; +using CodeHub.Core.Services; +using Octokit; namespace CodeHub.Core.ViewModels.Gists { public class GistCreateViewModel : BaseViewModel { + private readonly IMessageService _messageService; private string _description; private bool _public; private IDictionary _files = new Dictionary(); @@ -40,36 +41,40 @@ public IDictionary Files set { this.RaiseAndSetIfChanged(ref _files, value); } } - public IReactiveCommand SaveCommand { get; } + public ReactiveCommand SaveCommand { get; } - public IReactiveCommand CancelCommand { get; } + public ReactiveCommand CancelCommand { get; } - public GistCreateViewModel() + public GistCreateViewModel(IMessageService messageService = null) { - CancelCommand = ReactiveCommand.Create(); - SaveCommand = ReactiveCommand.CreateAsyncTask(_ => Save()); + _messageService = messageService ?? GetService(); + + CancelCommand = ReactiveCommand.Create(() => { }); + SaveCommand = ReactiveCommand.CreateFromTask(Save); SaveCommand.ThrownExceptions.Subscribe(x => DisplayAlert(x.Message)); } - private async Task Save() + private async Task Save() { if (_files.Count == 0) throw new Exception("You cannot create a Gist without atleast one file! Please correct and try again."); try { - var createGist = new GistCreateModel + var newGist = new NewGist() { Description = Description ?? string.Empty, - Public = Public, - Files = Files.ToDictionary(x => x.Key, x => new GistCreateModel.File { Content = x.Value }) + Public = Public }; + foreach (var kv in Files) + newGist.Files.Add(kv.Key, kv.Value); + IsSaving = true; - var newGist = (await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.AuthenticatedUser.Gists.CreateGist(createGist))).Data; - Messenger.Publish(new GistAddMessage(this, newGist)); - return newGist; + var gist = await this.GetApplication().GitHubClient.Gist.Create(newGist); + _messageService.Send(new GistAddMessage(gist)); + return gist; } finally { diff --git a/CodeHub.Core/ViewModels/Gists/GistFileViewModel.cs b/CodeHub.Core/ViewModels/Gists/GistFileViewModel.cs deleted file mode 100644 index 004d6584..00000000 --- a/CodeHub.Core/ViewModels/Gists/GistFileViewModel.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading.Tasks; -using GitHubSharp.Models; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Services; -using MvvmCross.Core.ViewModels; - -namespace CodeHub.Core.ViewModels.Gists -{ - public class GistFileViewModel : FileSourceViewModel - { - private string _id; - private string _filename; - - public string FileName { get; private set; } - - private GistFileModel _gist; - public GistFileModel Gist - { - get { return _gist; } - private set { this.RaiseAndSetIfChanged(ref _gist, value); } - } - - public void Init(NavObject navObject) - { - //Create the filename - var fileName = System.IO.Path.GetFileName(navObject.Filename); - if (fileName == null) - fileName = navObject.Filename.Substring(navObject.Filename.LastIndexOf('/') + 1); - - //Create the temp file path - Title = fileName; - FileName = fileName; - - _id = navObject.GistId; - _filename = navObject.Filename; - - //Grab the data - Gist = GetService().Get() as GistFileModel; - } - - protected override async Task Load() - { - - if (Gist == null) - { - var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Gists[_id].Get()); - Gist = data.Data.Files[_filename]; - } - - if (Gist == null || Gist.Content == null) - throw new Exception("Unable to retreive gist!"); - - IsMarkdown = string.Equals(Gist?.Language, "Markdown"); - Gist = Gist; - - var filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), FileName); - System.IO.File.WriteAllText(filepath, Gist.Content, System.Text.Encoding.UTF8); - ContentPath = FilePath = filepath; - } - - public class NavObject - { - public string GistId { get; set; } - public string Filename { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Gists/GistItemViewModel.cs b/CodeHub.Core/ViewModels/Gists/GistItemViewModel.cs new file mode 100644 index 00000000..9752977b --- /dev/null +++ b/CodeHub.Core/ViewModels/Gists/GistItemViewModel.cs @@ -0,0 +1,50 @@ +using System; +using ReactiveUI; +using CodeHub.Core.Utilities; +using Humanizer; +using System.Reactive; +using Octokit; +using System.Linq; + +namespace CodeHub.Core.ViewModels.Gists +{ + public class GistItemViewModel : ReactiveObject, ICanGoToViewModel + { + public GitHubAvatar Avatar { get; } + + public string Title { get; } + + public string Description { get; } + + public DateTimeOffset UpdatedAt { get; } + + public string UpdatedString { get; } + + public ReactiveCommand GoToCommand { get; } + + public string Id { get; } + + public Gist Gist { get; } + + private static string GetGistTitle(Gist gist) + { + var title = (gist.Owner == null) ? "Anonymous" : gist.Owner.Login; + if (gist.Files.Count > 0) + title = gist.Files.First().Key; + return title; + } + + public GistItemViewModel(Gist gist, Action gotoAction) + { + Gist = gist; + Id = gist.Id; + Title = GetGistTitle(gist); + Description = string.IsNullOrEmpty(gist.Description) ? "Gist " + gist.Id : gist.Description; + Avatar = new GitHubAvatar(gist.Owner?.AvatarUrl); + UpdatedAt = gist.UpdatedAt; + UpdatedString = UpdatedAt.Humanize(); + GoToCommand = ReactiveCommand.Create(() => gotoAction(this)); + } + } +} + diff --git a/CodeHub.Core/ViewModels/Gists/GistViewModel.cs b/CodeHub.Core/ViewModels/Gists/GistViewModel.cs index f4fce4d7..c0325b01 100644 --- a/CodeHub.Core/ViewModels/Gists/GistViewModel.cs +++ b/CodeHub.Core/ViewModels/Gists/GistViewModel.cs @@ -2,16 +2,15 @@ using System.Threading.Tasks; using System.Windows.Input; using CodeHub.Core.ViewModels.User; -using GitHubSharp.Models; -using CodeHub.Core.ViewModels; using MvvmCross.Core.ViewModels; +using Octokit; namespace CodeHub.Core.ViewModels.Gists { public class GistViewModel : LoadableViewModel { - private readonly CollectionViewModel _comments = new CollectionViewModel(); - private GistModel _gist; + private readonly CollectionViewModel _comments = new CollectionViewModel(); + private Gist _gist; private bool _starred; public string Id @@ -20,7 +19,7 @@ public string Id private set; } - public GistModel Gist + public Gist Gist { get { return _gist; } set { this.RaiseAndSetIfChanged(ref _gist, value); } @@ -32,7 +31,7 @@ public bool IsStarred private set { this.RaiseAndSetIfChanged(ref _starred, value); } } - public CollectionViewModel Comments + public CollectionViewModel Comments { get { return _comments; } } @@ -42,16 +41,6 @@ public ICommand GoToUserCommand get { return new MvxCommand(() => ShowViewModel(new UserViewModel.NavObject { Username = Gist.Owner.Login }), () => Gist != null && Gist.Owner != null); } } - public ICommand GoToFileSourceCommand - { - get { - return new MvxCommand(x => { - GetService().Add(x); - ShowViewModel(new GistFileViewModel.NavObject { GistId = Id, Filename = x.Filename }); - }); - } - } - public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => GoToUrlCommand.Execute(_gist.HtmlUrl), () => _gist != null); } @@ -73,6 +62,15 @@ public ICommand ToggleStarCommand } } + public static GistViewModel FromGist(Gist gist) + { + return new GistViewModel + { + Gist = gist, + Id = gist.Id + }; + } + public void Init(NavObject navObject) { Id = navObject.Id; @@ -99,18 +97,22 @@ public async Task ForkGist() ShowViewModel(new GistViewModel.NavObject { Id = forkedGist.Id }); } - protected override Task Load() + protected override async Task Load() { - var t1 = this.RequestModel(this.GetApplication().Client.Gists[Id].Get(), response => Gist = response.Data); - this.RequestModel(this.GetApplication().Client.Gists[Id].IsGistStarred(), response => IsStarred = response.Data).FireAndForget(); - Comments.SimpleCollectionLoad(this.GetApplication().Client.Gists[Id].GetComments()).FireAndForget(); - return t1; + Comments.Items.Clear(); + + this.GetApplication().GitHubClient.Gist.IsStarred(Id) + .ToBackground(x => IsStarred = x); + + this.GetApplication().GitHubClient.Gist.Comment.GetAllForGist(Id) + .ToBackground(Comments.Items.AddRange); + + Gist = await this.GetApplication().GitHubClient.Gist.Get(Id); } - public async Task Edit(GistEditModel editModel) + public async Task Edit(GistUpdate editModel) { - var response = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Gists[Id].EditGist(editModel)); - Gist = response.Data; + Gist = await this.GetApplication().GitHubClient.Gist.Edit(Id, editModel); } public class NavObject diff --git a/CodeHub.Core/ViewModels/Gists/GistsViewModel.cs b/CodeHub.Core/ViewModels/Gists/GistsViewModel.cs index 87b07c7a..4063ad67 100755 --- a/CodeHub.Core/ViewModels/Gists/GistsViewModel.cs +++ b/CodeHub.Core/ViewModels/Gists/GistsViewModel.cs @@ -1,30 +1,156 @@ using System.Collections.Generic; using System.Threading.Tasks; -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp; -using GitHubSharp.Models; +using ReactiveUI; +using CodeHub.Core.Services; +using System.Reactive; +using System; +using Octokit; +using System.Reactive.Linq; +using Splat; +using CodeHub.Core.Messages; namespace CodeHub.Core.ViewModels.Gists { - public abstract class GistsViewModel : LoadableViewModel + public class CurrentUserGistsViewModel : GistsViewModel { - private readonly CollectionViewModel _gists = new CollectionViewModel(); + private readonly IDisposable _addToken; - public CollectionViewModel Gists { get { return _gists; } } + public CurrentUserGistsViewModel(string username, IMessageService messageService = null) + : base(ApiUrls.UsersGists(username)) + { + messageService = messageService ?? Locator.Current.GetService(); + _addToken = messageService.Listen(msg => Gists.Insert(0, msg.Gist)); + } + } + + public class GistsViewModel : ReactiveObject + { + private readonly IApplicationService _applicationService; + private readonly IAlertDialogService _dialogService; + private readonly ReactiveList _internalItems + = new ReactiveList(resetChangeThreshold: double.MaxValue); + + public ReactiveCommand LoadCommand { get; } + + public ReactiveCommand LoadMoreCommand { get; } + + public IReadOnlyReactiveList Items { get; private set; } + + protected ReactiveList Gists => _internalItems; + + public ReactiveCommand ItemSelected { get; } + + private ObservableAsPropertyHelper _hasMore; + public bool HasMore => _hasMore.Value; + + private Uri _nextPage; + private Uri NextPage + { + get { return _nextPage; } + set { this.RaiseAndSetIfChanged(ref _nextPage, value); } + } + + private string _searchText; + public string SearchText + { + get { return _searchText; } + set { this.RaiseAndSetIfChanged(ref _searchText, value); } + } + + private readonly ObservableAsPropertyHelper _isEmpty; + public bool IsEmpty => _isEmpty.Value; + + public static GistsViewModel CreatePublicGistsViewModel() + => new GistsViewModel(ApiUrls.PublicGists()); - public ICommand GoToGistCommand + public static GistsViewModel CreateStarredGistsViewModel() + => new GistsViewModel(ApiUrls.StarredGists()); + + public static GistsViewModel CreateUserGistsViewModel(string username) + => new GistsViewModel(ApiUrls.UsersGists(username)); + + public GistsViewModel( + Uri uri, + IApplicationService applicationService = null, + IAlertDialogService dialogService = null) { - get { return new MvxCommand(x => ShowViewModel(new GistViewModel.NavObject { Id = x.Id }));} + _applicationService = applicationService ?? Locator.Current.GetService(); + _dialogService = dialogService ?? Locator.Current.GetService(); + + NextPage = uri; + + var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; + + ItemSelected = ReactiveCommand.Create(x => x); + + var gistItems = _internalItems.CreateDerivedCollection( + x => new GistItemViewModel(x, GoToItem)); + + var searchUpdated = this.WhenAnyValue(x => x.SearchText) + .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); + + Items = gistItems.CreateDerivedCollection( + x => x, + x => x.Title.ContainsKeyword(SearchText) || x.Description.ContainsKeyword(SearchText), + signalReset: searchUpdated); + + LoadCommand = ReactiveCommand.CreateFromTask(async t => + { + _internalItems.Clear(); + var parameters = new Dictionary { ["per_page"] = 100.ToString() }; + var items = await RetrieveItems(uri, parameters); + _internalItems.AddRange(items); + return items.Count > 0; + }); + + var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); + LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => + { + var items = await RetrieveItems(NextPage); + _internalItems.AddRange(items); + return items.Count > 0; + }, canLoadMore); + + LoadCommand.Select(_ => _internalItems.Count == 0) + .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); + + LoadCommand.ThrownExceptions.Subscribe(LoadingError); + + LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); + + _hasMore = this.WhenAnyValue(x => x.NextPage) + .Select(x => x != null) + .ToProperty(this, x => x.HasMore); } - protected override Task Load() + private void LoadingError(Exception err) { - return Gists.SimpleCollectionLoad(CreateRequest()); + _dialogService.Alert("Error Loading", err.Message).ToBackground(); } - protected abstract GitHubRequest> CreateRequest(); + private void GoToItem(GistItemViewModel item) + { + ItemSelected.ExecuteNow(item); + } + + private async Task> RetrieveItems( + Uri repositoriesUri, IDictionary parameters = null) + { + try + { + var connection = _applicationService.GitHubClient.Connection; + var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); + NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") + ? ret.HttpResponse.ApiInfo.Links["next"] + : null; + return ret.Body; + } + catch + { + NextPage = null; + throw; + } + } } } diff --git a/CodeHub.Core/ViewModels/Gists/PublicGistsViewModel.cs b/CodeHub.Core/ViewModels/Gists/PublicGistsViewModel.cs deleted file mode 100755 index 37d22c74..00000000 --- a/CodeHub.Core/ViewModels/Gists/PublicGistsViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using GitHubSharp; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Gists -{ - public class PublicGistsViewModel : GistsViewModel - { - protected override GitHubRequest> CreateRequest() - { - return this.GetApplication().Client.Gists.GetPublicGists(); - } - } -} diff --git a/CodeHub.Core/ViewModels/Gists/StarredGistsViewModel.cs b/CodeHub.Core/ViewModels/Gists/StarredGistsViewModel.cs deleted file mode 100755 index 2aef0b25..00000000 --- a/CodeHub.Core/ViewModels/Gists/StarredGistsViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using GitHubSharp; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Gists -{ - public class StarredGistsViewModel : GistsViewModel - { - protected override GitHubRequest> CreateRequest() - { - return this.GetApplication().Client.Gists.GetStarredGists(); - } - } -} diff --git a/CodeHub.Core/ViewModels/Gists/UserGistsViewModel.cs b/CodeHub.Core/ViewModels/Gists/UserGistsViewModel.cs deleted file mode 100755 index 9915675d..00000000 --- a/CodeHub.Core/ViewModels/Gists/UserGistsViewModel.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using GitHubSharp; -using GitHubSharp.Models; -using CodeHub.Core.Messages; -using MvvmCross.Plugins.Messenger; - -namespace CodeHub.Core.ViewModels.Gists -{ - public class UserGistsViewModel : GistsViewModel - { - private readonly MvxSubscriptionToken _addToken; - - public string Username - { - get; - private set; - } - - public bool IsMine - { - get { return this.GetApplication().Account.Username.Equals(Username); } - } - - public UserGistsViewModel() - { - _addToken = Messenger.SubscribeOnMainThread(x => Gists.Items.Insert(0, x.Gist)); - } - - public void Init(NavObject navObject) - { - Username = navObject.Username ?? this.GetApplication().Account.Username; - - //Assign some sort of title - if (Username != null) - { - if (IsMine) - Title = "My Gists"; - else - { - if (Username.EndsWith("s", System.StringComparison.Ordinal)) - Title = Username + "' Gists"; - else - Title = Username + "'s Gists"; - } - } - else - { - Title = "Gists"; - } - } - - protected override GitHubRequest> CreateRequest() - { - return this.GetApplication().Client.Users[Username].Gists.GetGists(); - } - - public class NavObject - { - public string Username { get; set; } - } - } - -} diff --git a/CodeHub.Core/ViewModels/ICanGoToViewModel.cs b/CodeHub.Core/ViewModels/ICanGoToViewModel.cs new file mode 100644 index 00000000..7cf590c3 --- /dev/null +++ b/CodeHub.Core/ViewModels/ICanGoToViewModel.cs @@ -0,0 +1,11 @@ +using System.Reactive; +using ReactiveUI; + +namespace CodeHub.Core.ViewModels +{ + public interface ICanGoToViewModel + { + ReactiveCommand GoToCommand { get; } + } +} + diff --git a/CodeHub.Core/ViewModels/IListViewModel.cs b/CodeHub.Core/ViewModels/IListViewModel.cs new file mode 100644 index 00000000..66bd8d22 --- /dev/null +++ b/CodeHub.Core/ViewModels/IListViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Reactive; +using ReactiveUI; + +namespace CodeHub.Core.ViewModels +{ + public interface IListViewModel + { + ReactiveCommand LoadCommand { get; } + + ReactiveCommand LoadMoreCommand { get; } + + IReadOnlyReactiveList Items { get; } + + bool HasMore { get; } + + string SearchText { get; } + } +} diff --git a/CodeHub.Core/ViewModels/ILoadableViewModel.cs b/CodeHub.Core/ViewModels/ILoadableViewModel.cs new file mode 100644 index 00000000..c26501c9 --- /dev/null +++ b/CodeHub.Core/ViewModels/ILoadableViewModel.cs @@ -0,0 +1,11 @@ +using ReactiveUI; +using System.Reactive; + +namespace CodeHub.Core.ViewModels +{ + public interface ILoadableViewModel + { + ReactiveCommand LoadCommand { get; } + } +} + diff --git a/CodeHub.Core/ViewModels/IProvidesSearchKeyword.cs b/CodeHub.Core/ViewModels/IProvidesSearchKeyword.cs new file mode 100644 index 00000000..83e96f7c --- /dev/null +++ b/CodeHub.Core/ViewModels/IProvidesSearchKeyword.cs @@ -0,0 +1,7 @@ +namespace CodeHub.Core.ViewModels +{ + public interface IProvidesSearchKeyword + { + string SearchKeyword { get; set; } + } +} diff --git a/CodeHub.Core/ViewModels/Issues/IssueAddViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueAddViewModel.cs index e2440659..27d08590 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueAddViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueAddViewModel.cs @@ -3,11 +3,20 @@ using System.Threading.Tasks; using CodeHub.Core.Messages; using System.Linq; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueAddViewModel : IssueModifyViewModel { + private readonly IMessageService _messageService; + + public IssueAddViewModel(IMessageService messageService) + : base(messageService) + { + _messageService = messageService; + } + protected override async Task Save() { if (string.IsNullOrEmpty(IssueTitle)) @@ -27,7 +36,7 @@ protected override async Task Save() IsSaving = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues.Create(IssueTitle, content, assignedTo, milestone, labels)); - Messenger.Publish(new IssueAddMessage(this) { Issue = data.Data }); + _messageService.Send(new IssueAddMessage(data.Data)); ChangePresentation(new MvxClosePresentationHint(this)); } catch diff --git a/CodeHub.Core/ViewModels/Issues/IssueAssignedToViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueAssignedToViewModel.cs index cd7d078c..21be80b7 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueAssignedToViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueAssignedToViewModel.cs @@ -1,15 +1,16 @@ using System; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; using System.Threading.Tasks; using CodeHub.Core.Messages; -using System.Linq; using MvvmCross.Core.ViewModels; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueAssignedToViewModel : LoadableViewModel { + private readonly IMessageService _messageService; + private BasicUserModel _selectedUser; public BasicUserModel SelectedUser { @@ -48,6 +49,11 @@ public CollectionViewModel Users public bool SaveOnSelect { get; private set; } + public IssueAssignedToViewModel(IMessageService messageService) + { + _messageService = messageService; + } + public void Init(NavObject navObject) { Username = navObject.Username; @@ -69,7 +75,7 @@ private async Task SelectUser(BasicUserModel x) var assignee = x != null ? x.Login : null; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateAssignee(assignee); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); - Messenger.Publish(new IssueEditMessage(this) { Issue = newIssue.Data }); + _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch @@ -83,7 +89,7 @@ private async Task SelectUser(BasicUserModel x) } else { - Messenger.Publish(new SelectedAssignedToMessage(this) { User = x }); + _messageService.Send(new SelectedAssignedToMessage(x)); } ChangePresentation(new MvxClosePresentationHint(this)); diff --git a/CodeHub.Core/ViewModels/Issues/IssueEditViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueEditViewModel.cs index 19818fef..a7010165 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueEditViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueEditViewModel.cs @@ -4,11 +4,13 @@ using System; using CodeHub.Core.Messages; using System.Linq; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueEditViewModel : IssueModifyViewModel { + private readonly IMessageService _messageService; private IssueModel _issue; private bool _open; @@ -26,6 +28,12 @@ public IssueModel Issue public long Id { get; private set; } + public IssueEditViewModel(IMessageService messageService) + : base(messageService) + { + _messageService = messageService; + } + protected override async Task Save() { try @@ -50,7 +58,7 @@ protected override async Task Save() try { var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Issue.Number].Update(IssueTitle, content, state, assignedTo, milestone, labels)); - Messenger.Publish(new IssueEditMessage(this) { Issue = data.Data }); + _messageService.Send(new IssueEditMessage(data.Data)); } catch (GitHubSharp.InternalServerException) { diff --git a/CodeHub.Core/ViewModels/Issues/IssueLabelsViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueLabelsViewModel.cs index 9979143c..4e8ba5a2 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueLabelsViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueLabelsViewModel.cs @@ -1,5 +1,3 @@ -using System; -using CodeHub.Core.ViewModels; using System.Threading.Tasks; using GitHubSharp.Models; using System.Collections.Generic; @@ -7,11 +5,13 @@ using System.Linq; using System.Windows.Input; using MvvmCross.Core.ViewModels; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueLabelsViewModel : LoadableViewModel { + private readonly IMessageService _messageService; private IEnumerable _originalLables; private bool _isSaving; @@ -60,6 +60,11 @@ public ICommand SaveLabelChoices get { return new MvxCommand(() => SelectLabels(SelectedLabels)); } } + public IssueLabelsViewModel(IMessageService messageService) + { + _messageService = messageService; + } + private async Task SelectLabels(IEnumerable x) { //If nothing has changed, dont do anything... @@ -77,7 +82,7 @@ private async Task SelectLabels(IEnumerable x) var labels = x != null ? x.Select(y => y.Name).ToArray() : null; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateLabels(labels); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); - Messenger.Publish(new IssueEditMessage(this) { Issue = newIssue.Data }); + _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch { @@ -90,7 +95,7 @@ private async Task SelectLabels(IEnumerable x) } else { - Messenger.Publish(new SelectIssueLabelsMessage(this) { Labels = SelectedLabels.Items.ToArray() }); + _messageService.Send(new SelectIssueLabelsMessage(SelectedLabels.Items)); } ChangePresentation(new MvxClosePresentationHint(this)); diff --git a/CodeHub.Core/ViewModels/Issues/IssueMilestonesViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueMilestonesViewModel.cs index f904f658..c6ed1f3a 100755 --- a/CodeHub.Core/ViewModels/Issues/IssueMilestonesViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueMilestonesViewModel.cs @@ -1,14 +1,15 @@ using System.Threading.Tasks; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.Messages; -using System.Linq; using System; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueMilestonesViewModel : LoadableViewModel { + private readonly IMessageService _messageService; + private MilestoneModel _selectedMilestone; public MilestoneModel SelectedMilestone { @@ -47,6 +48,11 @@ public CollectionViewModel Milestones public bool SaveOnSelect { get; private set; } + public IssueMilestonesViewModel(IMessageService messageService) + { + _messageService = messageService; + } + public void Init(NavObject navObject) { Username = navObject.Username; @@ -69,7 +75,7 @@ private async Task SelectMilestone(MilestoneModel x) if (x != null) milestone = x.Number; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateMilestone(milestone); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); - Messenger.Publish(new IssueEditMessage(this) { Issue = newIssue.Data }); + _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch { @@ -82,7 +88,7 @@ private async Task SelectMilestone(MilestoneModel x) } else { - Messenger.Publish(new SelectedMilestoneMessage(this) { Milestone = x }); + _messageService.Send(new SelectedMilestoneMessage(x)); } ChangePresentation(new MvvmCross.Core.ViewModels.MvxClosePresentationHint(this)); diff --git a/CodeHub.Core/ViewModels/Issues/IssueModifyViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueModifyViewModel.cs index 66586f70..b901c8cf 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueModifyViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueModifyViewModel.cs @@ -1,22 +1,22 @@ using System; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; using System.Windows.Input; using MvvmCross.Core.ViewModels; using System.Threading.Tasks; using CodeHub.Core.Messages; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public abstract class IssueModifyViewModel : BaseViewModel { + private readonly IMessageService _messageService; private string _title; private string _content; private BasicUserModel _assignedTo; private readonly CollectionViewModel _labels = new CollectionViewModel(); private MilestoneModel _milestone; - private MvxSubscriptionToken _labelsToken, _milestoneToken, _assignedToken; + private IDisposable _labelsToken, _milestoneToken, _assignedToken; private bool _isSaving; public string IssueTitle @@ -96,15 +96,19 @@ public ICommand SaveCommand get { return new MvxCommand(() => Save()); } } + public IssueModifyViewModel(IMessageService messageService) + { + _messageService = messageService; + } + protected void Init(string username, string repository) { Username = username; Repository = repository; - var messenger = GetService(); - _labelsToken = messenger.SubscribeOnMainThread(x => Labels.Items.Reset(x.Labels)); - _milestoneToken = messenger.SubscribeOnMainThread(x => Milestone = x.Milestone); - _assignedToken = messenger.SubscribeOnMainThread(x => AssignedTo = x.User); + _labelsToken = _messageService.Listen(x => Labels.Items.Reset(x.Labels)); + _milestoneToken = _messageService.Listen(x => Milestone = x.Milestone); + _assignedToken = _messageService.Listen(x => AssignedTo = x.User); } protected abstract Task Save(); diff --git a/CodeHub.Core/ViewModels/Issues/IssueViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssueViewModel.cs index bc26fc6e..8c093d9e 100644 --- a/CodeHub.Core/ViewModels/Issues/IssueViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssueViewModel.cs @@ -1,22 +1,27 @@ using System.Threading.Tasks; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; using System.Windows.Input; using CodeHub.Core.Messages; using CodeHub.Core.Services; using System; -using MvvmCross.Plugins.Messenger; using MvvmCross.Core.ViewModels; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; +using System.Reactive; +using Splat; +using System.Reactive.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; namespace CodeHub.Core.ViewModels.Issues { public class IssueViewModel : LoadableViewModel { - private MvxSubscriptionToken _editToken; + private IDisposable _editToken; private readonly IFeaturesService _featuresService; private readonly IApplicationService _applicationService; + private readonly IMessageService _messageService; + private readonly IMarkdownService _markdownService; public long Id { @@ -36,14 +41,11 @@ public string Repository private set; } + private string _markdownDescription; public string MarkdownDescription { - get - { - if (Issue == null) - return string.Empty; - return (GetService().Convert(Issue.Body)); - } + get { return _markdownDescription; } + private set { this.RaiseAndSetIfChanged(ref _markdownDescription, value); } } private bool? _isClosed; @@ -81,7 +83,28 @@ public bool IsModifying set { this.RaiseAndSetIfChanged(ref _isModifying, value); } } - public ReactiveUI.ReactiveCommand GoToOwner { get; } + private int? _participants; + public int? Participants + { + get { return _participants; } + set { this.RaiseAndSetIfChanged(ref _participants, value); } + } + + private IReadOnlyList _comments; + public IReadOnlyList Comments + { + get { return _comments ?? new List(); } + private set { this.RaiseAndSetIfChanged(ref _comments, value); } + } + + private IReadOnlyList _events; + public IReadOnlyList Events + { + get { return _events ?? new List(); } + private set { this.RaiseAndSetIfChanged(ref _events, value); } + } + + public ReactiveUI.ReactiveCommand GoToOwner { get; } public ICommand GoToAssigneeCommand { @@ -135,49 +158,59 @@ public ICommand ToggleStateCommand } } - private readonly CollectionViewModel _comments = new CollectionViewModel(); - public CollectionViewModel Comments - { - get { return _comments; } - } - - private readonly CollectionViewModel _events = new CollectionViewModel(); - public CollectionViewModel Events - { - get { return _events; } - } - protected override Task Load() { if (_featuresService.IsProEnabled) ShouldShowPro = false; else { - var request = _applicationService.Client.Users[Username].Repositories[Repository].Get(); - _applicationService.Client.ExecuteAsync(request) - .ToBackground(x => ShouldShowPro = x.Data.Private && !_featuresService.IsProEnabled); + _applicationService + .GitHubClient.Repository.Get(Username, Repository) + .ToBackground(x => ShouldShowPro = x.Private && !_featuresService.IsProEnabled); } - var t1 = this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data); - Comments.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].GetComments()).FireAndForget(); - Events.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].GetEvents()).FireAndForget(); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].IsCollaborator(this.GetApplication().Account.Username), response => IsCollaborator = response.Data).FireAndForget(); - return t1; - } + _applicationService + .GitHubClient.Issue.Comment.GetAllForIssue(Username, Repository, (int)Id) + .ToBackground(x => Comments = x); - public string ConvertToMarkdown(string str) - { - return (GetService().Convert(str)); + _applicationService + .GitHubClient.Issue.Events.GetAllForIssue(Username, Repository, (int)Id) + .ToBackground(events => + { + Events = events; + Participants = events.Select(x => x.Actor?.Login).Distinct().Count(); + }); + + _applicationService + .GitHubClient.Repository.Collaborator.IsCollaborator(Username, Repository, _applicationService.Account.Username) + .ToBackground(x => IsCollaborator = x); + + return this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data); } - public IssueViewModel(IApplicationService applicationService, IFeaturesService featuresService) + public IssueViewModel( + IApplicationService applicationService = null, + IFeaturesService featuresService = null, + IMessageService messageService = null, + IMarkdownService markdownService = null) { - _applicationService = applicationService; - _featuresService = featuresService; - this.Bind(x => x.Issue, true).Where(x => x != null).Select(x => string.Equals(x.State, "closed")).Subscribe(x => IsClosed = x); + _applicationService = applicationService ?? Locator.Current.GetService(); + _featuresService = featuresService ?? Locator.Current.GetService(); + _messageService = messageService ?? Locator.Current.GetService(); + _markdownService = markdownService ?? Locator.Current.GetService(); + + this.Bind(x => x.Issue, true) + .Where(x => x != null) + .Select(x => string.Equals(x.State, "closed")) + .Subscribe(x => IsClosed = x); + + this.Bind(x => x.Issue, true) + .SelectMany(issue => _markdownService.Convert(issue?.Body).ToObservable()) + .Subscribe(x => MarkdownDescription = x); - GoToOwner = ReactiveUI.ReactiveCommand.Create(this.Bind(x => x.Issue, true).Select(x => x != null)); - GoToOwner.Subscribe(_ => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login })); + GoToOwner = ReactiveUI.ReactiveCommand.Create( + () => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login }), + this.Bind(x => x.Issue, true).Select(x => x != null)); } public void Init(NavObject navObject) @@ -186,7 +219,7 @@ public void Init(NavObject navObject) Repository = navObject.Repository; Id = navObject.Id; - _editToken = Messenger.SubscribeOnMainThread(x => + _editToken = _messageService.Listen(x => { if (x.Issue == null || x.Issue.Number != Issue.Number) return; @@ -198,8 +231,9 @@ public async Task AddComment(string text) { try { - var comment = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].CreateComment(text)); - Comments.Items.Add(comment.Data); + var comment = await _applicationService.GitHubClient.Issue.Comment.Create(Username, Repository, (int)Id, text); + var newCommentList = new List(Comments) { comment }; + Comments = newCommentList; return true; } catch (Exception e) @@ -215,7 +249,7 @@ private async Task ToggleState(bool closed) { IsModifying = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Issue.Number].UpdateState(closed ? "closed" : "open")); - Messenger.Publish(new IssueEditMessage(this) { Issue = data.Data }); + _messageService.Send(new IssueEditMessage(data.Data)); } catch (Exception e) { diff --git a/CodeHub.Core/ViewModels/Issues/IssuesViewModel.cs b/CodeHub.Core/ViewModels/Issues/IssuesViewModel.cs index f39acbcd..bb3c5fe2 100755 --- a/CodeHub.Core/ViewModels/Issues/IssuesViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/IssuesViewModel.cs @@ -1,19 +1,19 @@ using System; using System.Threading.Tasks; -using CodeHub.Core.ViewModels; using CodeHub.Core.Filters; using GitHubSharp.Models; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.Messages; using System.Linq; -using MvvmCross.Plugins.Messenger; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssuesViewModel : BaseIssuesViewModel { - private MvxSubscriptionToken _addToken, _editToken; + private readonly IMessageService _messageService; + private IDisposable _addToken, _editToken; public string Username { get; private set; } @@ -24,6 +24,11 @@ public ICommand GoToNewIssueCommand get { return new MvxCommand(() => ShowViewModel(new IssueAddViewModel.NavObject { Username = Username, Repository = Repository })); } } + public IssuesViewModel(IMessageService messageService) + { + _messageService = messageService; + } + public void Init(NavObject nav) { Username = nav.Username; @@ -32,14 +37,14 @@ public void Init(NavObject nav) _issues.GroupingFunction = Group; _issues.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(true)); - _addToken = Messenger.SubscribeOnMainThread(x => + _addToken = _messageService.Listen(x => { if (x.Issue == null || !DoesIssueBelong(x.Issue)) return; Issues.Items.Insert(0, x.Issue); }); - _editToken = Messenger.SubscribeOnMainThread(x => + _editToken = _messageService.Listen(x => { if (x.Issue == null || !DoesIssueBelong(x.Issue)) return; diff --git a/CodeHub.Core/ViewModels/Issues/MyIssuesViewModel.cs b/CodeHub.Core/ViewModels/Issues/MyIssuesViewModel.cs index ef4b9b0c..1bc6afc1 100755 --- a/CodeHub.Core/ViewModels/Issues/MyIssuesViewModel.cs +++ b/CodeHub.Core/ViewModels/Issues/MyIssuesViewModel.cs @@ -1,18 +1,17 @@ using System.Threading.Tasks; -using CodeHub.Core.ViewModels; using CodeHub.Core.Filters; using GitHubSharp.Models; using System.Collections.Generic; using System.Linq; using System; using CodeHub.Core.Messages; -using MvvmCross.Plugins.Messenger; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class MyIssuesViewModel : BaseIssuesViewModel { - private MvxSubscriptionToken _editToken; + private IDisposable _editToken; private int _selectedFilter; public int SelectedFilter @@ -25,8 +24,10 @@ public int SelectedFilter } } - public MyIssuesViewModel() + public MyIssuesViewModel(IMessageService messageService = null) { + messageService = messageService ?? GetService(); + _issues = new FilterableCollectionViewModel("MyIssues"); _issues.GroupingFunction = Group; _issues.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(false)); @@ -39,7 +40,7 @@ public MyIssuesViewModel() _issues.Filter = MyIssuesFilterModel.CreateClosedFilter(); }); - _editToken = Messenger.SubscribeOnMainThread(x => + _editToken = messageService.Listen(x => { if (x.Issue == null) return; diff --git a/CodeHub.Core/ViewModels/MarkdownAccessoryViewModel.cs b/CodeHub.Core/ViewModels/MarkdownAccessoryViewModel.cs new file mode 100644 index 00000000..f6189bbf --- /dev/null +++ b/CodeHub.Core/ViewModels/MarkdownAccessoryViewModel.cs @@ -0,0 +1,62 @@ +using System; +using ReactiveUI; +using CodeHub.Core.Services; +using System.IO; +using System.Threading.Tasks; +using System.Reactive.Linq; +using System.Reactive; +using Splat; +using Plugin.Media.Abstractions; + +namespace CodeHub.Core.ViewModels +{ + public class MarkdownAccessoryViewModel : ReactiveObject + { + private const string IMGUR_UPLOAD_WARN = "IMGUR_UPLOAD_WARN"; + private const string IMGUR_UPLOAD_WARN_MESSAGE = + "Because GitHub's image upload API is not public images you upload here are hosted by Imgur. " + + "Please be aware of this when posting confidential information"; + + public ReactiveCommand PostToImgurCommand { get; private set; } + + public MarkdownAccessoryViewModel( + IImgurService imgurService = null, + IMedia mediaPicker = null, + IAlertDialogService alertDialog = null) + { + imgurService = imgurService ?? Locator.Current.GetService(); + mediaPicker = mediaPicker ?? Plugin.Media.CrossMedia.Current; + alertDialog = alertDialog ?? Locator.Current.GetService(); + + PostToImgurCommand = ReactiveCommand.CreateFromTask(async _ => { + + if (!Settings.HasSeenImgurUploadWarn) + { + Settings.HasSeenImgurUploadWarn = true; + await alertDialog.Alert("Please Read!", IMGUR_UPLOAD_WARN_MESSAGE); + } + + var photo = await mediaPicker.PickPhotoAsync(new PickMediaOptions + { + CompressionQuality = 80 + }); + + var memoryStream = new MemoryStream(); + await photo.GetStream().CopyToAsync(memoryStream); + + using (alertDialog.Activate("Uploading...")) + { + var model = await imgurService.SendImage(memoryStream.ToArray()); + if (model == null || model.Data == null || model.Data.Link == null) + throw new InvalidOperationException("Unable to upload to Imgur. Please try again later."); + return model.Data.Link; + } + }); + + PostToImgurCommand.ThrownExceptions + .Where(x => !(x is TaskCanceledException)) + .Subscribe(x => alertDialog.Alert("Upload Error", x.Message)); + } + } +} + diff --git a/CodeHub.Core/ViewModels/Notifications/NotificationsViewModel.cs b/CodeHub.Core/ViewModels/Notifications/NotificationsViewModel.cs index 695d3d71..63f9b3f5 100755 --- a/CodeHub.Core/ViewModels/Notifications/NotificationsViewModel.cs +++ b/CodeHub.Core/ViewModels/Notifications/NotificationsViewModel.cs @@ -1,32 +1,30 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using CodeHub.Core.Filters; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.ViewModels.PullRequests; -using GitHubSharp.Models; using CodeHub.Core.Messages; -using System.Collections.Generic; -using CodeHub.Core.ViewModels.Source; using CodeHub.Core.ViewModels.Changesets; using CodeHub.Core.Utils; +using CodeHub.Core.Services; +using CodeHub.Core.ViewModels.Repositories; namespace CodeHub.Core.ViewModels.Notifications { public class NotificationsViewModel : LoadableViewModel { - private readonly FilterableCollectionViewModel _notifications; + private readonly IApplicationService _applicationService; + private readonly IMessageService _messageService; + private readonly FilterableCollectionViewModel _notifications; private ICommand _readAllCommand; - private ICommand _readCommand; private ICommand _readReposCommand; private int _shownIndex; private bool _isMarking; - public FilterableCollectionViewModel Notifications + public FilterableCollectionViewModel Notifications { get { return _notifications; } } @@ -43,57 +41,56 @@ public bool IsMarking set { this.RaiseAndSetIfChanged(ref _isMarking, value); } } - public ICommand ReadCommand - { - get { return _readCommand ?? (_readCommand = new MvxCommand(x => Read(x)));} - } - public ICommand ReadRepositoriesCommand { - get { return _readReposCommand ?? (_readReposCommand = new MvxCommand(x => MarkRepoAsRead(x))); } + get { return _readReposCommand ?? (_readReposCommand = new MvxAsyncCommand(x => MarkRepoAsRead(x))); } } public ICommand ReadAllCommand { - get { return _readAllCommand ?? (_readAllCommand = new MvxCommand(() => MarkAllAsRead(), () => ShownIndex != 2 && !IsLoading && !IsMarking && Notifications.Any())); } + get { return _readAllCommand ?? (_readAllCommand = new MvxAsyncCommand(() => MarkAllAsRead(), () => ShownIndex != 2 && !IsLoading && !IsMarking && Notifications.Any())); } } public ICommand GoToNotificationCommand { - get { return new MvxCommand(GoToNotification); } + get { return new MvxCommand(GoToNotification); } } - private void GoToNotification(NotificationModel x) + private void GoToNotification(Octokit.Notification x) { var subject = x.Subject.Type.ToLower(); if (subject.Equals("issue")) { - ReadCommand.Execute(x); + Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new IssueViewModel.NavObject { Username = x.Repository.Owner.Login,Repository = x.Repository.Name, Id = long.Parse(node) }); } else if (subject.Equals("pullrequest")) { - ReadCommand.Execute(x); + Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new PullRequestViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name, Id = long.Parse(node) }); } else if (subject.Equals("commit")) { - ReadCommand.Execute(x); + Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new ChangesetViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name, Node = node }); } else if (subject.Equals("release")) { - ReadCommand.Execute(x); - ShowViewModel(new BranchesAndTagsViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name, IsShowingBranches = false }); + Read(x).ToBackground(); + ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name }); } } - public NotificationsViewModel() + public NotificationsViewModel( + IMessageService messageService = null, + IApplicationService applicationService = null) { - _notifications = new FilterableCollectionViewModel("Notifications"); + _messageService = messageService ?? GetService(); + _applicationService = applicationService ?? GetService(); + _notifications = new FilterableCollectionViewModel("Notifications"); _notifications.GroupingFunction = (n) => n.GroupBy(x => x.Repository.FullName); _notifications.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(false)); @@ -114,15 +111,20 @@ public NotificationsViewModel() } - protected override Task Load() + protected override async Task Load() { - return this.RequestModel(this.GetApplication().Client.Notifications.GetAll(all: Notifications.Filter.All, participating: Notifications.Filter.Participating), response => { - Notifications.Items.Reset(response.Data); - UpdateAccountNotificationsCount(); - }); + var req = new Octokit.NotificationsRequest + { + All = Notifications.Filter.All, + Participating = Notifications.Filter.Participating + }; + + var notifications = await _applicationService.GitHubClient.Activity.Notifications.GetAllForCurrent(req); + Notifications.Items.Reset(notifications); + UpdateAccountNotificationsCount(); } - private async Task Read(NotificationModel model) + private async Task Read(Octokit.Notification model) { // If its already read, ignore it if (!model.Unread) @@ -130,16 +132,15 @@ private async Task Read(NotificationModel model) try { - var response = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Notifications[model.Id].MarkAsRead()); - if (response.Data) - { - //We just read it - model.Unread = false; - - //Update the notifications count on the account + if (!int.TryParse(model.Id, out int id)) + return; + + await _applicationService.GitHubClient.Activity.Notifications.MarkAsRead(id); + + if (_shownIndex != 2) Notifications.Items.Remove(model); - UpdateAccountNotificationsCount(); - } + + UpdateAccountNotificationsCount(); } catch { @@ -195,7 +196,10 @@ private void UpdateAccountNotificationsCount() { // Only update if we're looking at if (!Notifications.Filter.All && !Notifications.Filter.Participating) - Messenger.Publish(new NotificationCountMessage(this) { Count = Notifications.Items.Sum(x => x.Unread ? 1 : 0) }); + { + var count = Notifications.Items.Sum(x => x.Unread ? 1 : 0); + _messageService.Send(new NotificationCountMessage(count)); + } } } } diff --git a/CodeHub.Core/ViewModels/Organizations/OrganizationViewModel.cs b/CodeHub.Core/ViewModels/Organizations/OrganizationViewModel.cs index c8b99e95..e872e90e 100755 --- a/CodeHub.Core/ViewModels/Organizations/OrganizationViewModel.cs +++ b/CodeHub.Core/ViewModels/Organizations/OrganizationViewModel.cs @@ -2,11 +2,7 @@ using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.ViewModels.Events; -using CodeHub.Core.ViewModels.Gists; -using CodeHub.Core.ViewModels.Repositories; -using CodeHub.Core.ViewModels.User; using GitHubSharp.Models; -using CodeHub.Core.ViewModels; namespace CodeHub.Core.ViewModels.Organizations { @@ -16,7 +12,7 @@ public class OrganizationViewModel : LoadableViewModel public string Name { get; private set; } - public void Init(NavObject navObject) + public void Init(NavObject navObject) { Name = navObject.Name; } @@ -27,36 +23,16 @@ public UserModel Organization private set { this.RaiseAndSetIfChanged(ref _userModel, value); } } - public ICommand GoToMembersCommand - { - get { return new MvxCommand(() => ShowViewModel(new OrganizationMembersViewModel.NavObject { Name = Name }));} - } - public ICommand GoToTeamsCommand { get { return new MvxCommand(() => ShowViewModel(new TeamsViewModel.NavObject { Name = Name })); } } - public ICommand GoToFollowersCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserFollowersViewModel.NavObject { Username = Name })); } - } - public ICommand GoToEventsCommand { get { return new MvxCommand(() => ShowViewModel(new UserEventsViewModel.NavObject { Username = Name })); } } - public ICommand GoToGistsCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserGistsViewModel.NavObject { Username = Name })); } - } - - public ICommand GoToRepositoriesCommand - { - get { return new MvxCommand(() => ShowViewModel(new OrganizationRepositoriesViewModel.NavObject { Name = Name })); } - } - protected override Task Load() { return this.RequestModel(this.GetApplication().Client.Organizations[Name].Get(), response => Organization = response.Data); diff --git a/CodeHub.Core/ViewModels/Organizations/TeamsViewModel.cs b/CodeHub.Core/ViewModels/Organizations/TeamsViewModel.cs index afb5ec04..47decc3f 100755 --- a/CodeHub.Core/ViewModels/Organizations/TeamsViewModel.cs +++ b/CodeHub.Core/ViewModels/Organizations/TeamsViewModel.cs @@ -1,7 +1,3 @@ -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using CodeHub.Core.ViewModels.User; using GitHubSharp.Models; using System.Threading.Tasks; @@ -13,11 +9,6 @@ public class TeamsViewModel : LoadableViewModel public string OrganizationName { get; private set; } - public ICommand GoToTeamCommand - { - get { return new MvxCommand(x => ShowViewModel(new TeamMembersViewModel.NavObject { Id = x.Id })); } - } - public TeamsViewModel() { Title = "Teams"; diff --git a/CodeHub.Core/ViewModels/PullRequests/PullRequestFilesViewModel.cs b/CodeHub.Core/ViewModels/PullRequests/PullRequestFilesViewModel.cs index 44a68fa4..b77119e2 100755 --- a/CodeHub.Core/ViewModels/PullRequests/PullRequestFilesViewModel.cs +++ b/CodeHub.Core/ViewModels/PullRequests/PullRequestFilesViewModel.cs @@ -1,10 +1,11 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; +using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Source; +using GitHubSharp.Models; using MvvmCross.Core.ViewModels; +using MvvmCross.Platform; namespace CodeHub.Core.ViewModels.PullRequests { @@ -23,23 +24,14 @@ public CollectionViewModel Files public string Repository { get; private set; } - public ICommand GoToSourceCommand - { - get - { - return new MvxCommand(x => - { - var name = x.Filename.Substring(x.Filename.LastIndexOf("/", System.StringComparison.Ordinal) + 1); - ShowViewModel(new SourceViewModel.NavObject { Name = name, Path = x.Filename, GitUrl = x.ContentsUrl, ForceBinary = x.Patch == null }); - }); - } - } + public string Sha { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; PullRequestId = navObject.PullRequestId; + Sha = navObject.Sha; _files.GroupingFunction = (x) => x.GroupBy(y => { var filename = "/" + y.Filename; @@ -56,6 +48,7 @@ public class NavObject { public string Username { get; set; } public string Repository { get; set; } + public string Sha { get; set; } public long PullRequestId { get; set; } } } diff --git a/CodeHub.Core/ViewModels/PullRequests/PullRequestViewModel.cs b/CodeHub.Core/ViewModels/PullRequests/PullRequestViewModel.cs index 219ce415..be146343 100644 --- a/CodeHub.Core/ViewModels/PullRequests/PullRequestViewModel.cs +++ b/CodeHub.Core/ViewModels/PullRequests/PullRequestViewModel.cs @@ -4,20 +4,24 @@ using MvvmCross.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.Services; -using MvvmCross.Plugins.Messenger; using CodeHub.Core.ViewModels.Issues; -using CodeHub.Core.ViewModels; using CodeHub.Core.Messages; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; +using System.Reactive; +using System.Reactive.Threading.Tasks; +using System.Collections.Generic; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestViewModel : LoadableViewModel { - private MvxSubscriptionToken _issueEditSubscription; - private MvxSubscriptionToken _pullRequestEditSubscription; + private readonly IApplicationService _applicationService; + private readonly IMessageService _messageService; + private IDisposable _issueEditSubscription; + private IDisposable _pullRequestEditSubscription; private readonly IFeaturesService _featuresService; + private readonly IMarkdownService _markdownService; public long Id { @@ -37,9 +41,11 @@ public string Repository private set; } + private string _markdownDescription; public string MarkdownDescription { - get { return PullRequest == null ? string.Empty : (GetService().Convert(PullRequest.Body)); } + get { return _markdownDescription; } + private set { this.RaiseAndSetIfChanged(ref _markdownDescription, value); } } private bool _canPush; @@ -91,6 +97,20 @@ public bool? IsClosed private set { this.RaiseAndSetIfChanged(ref _isClosed, value); } } + private IReadOnlyList _comments; + public IReadOnlyList Comments + { + get { return _comments ?? new List(); } + private set { this.RaiseAndSetIfChanged(ref _comments, value); } + } + + private IReadOnlyList _events; + public IReadOnlyList Events + { + get { return _events ?? new List(); } + private set { this.RaiseAndSetIfChanged(ref _events, value); } + } + private ICommand _goToAssigneeCommand; public ICommand GoToAssigneeCommand @@ -174,7 +194,7 @@ public ICommand ToggleStateCommand get { return new MvxCommand(() => ToggleState(PullRequest.State == "open")); } } - public ReactiveUI.ReactiveCommand GoToOwner { get; } + public ReactiveUI.ReactiveCommand GoToOwner { get; } public ICommand GoToCommitsCommand { @@ -183,35 +203,42 @@ public ICommand GoToCommitsCommand public ICommand GoToFilesCommand { - get { return new MvxCommand(() => ShowViewModel(new PullRequestFilesViewModel.NavObject { Username = Username, Repository = Repository, PullRequestId = Id })); } - } - - private readonly CollectionViewModel _comments = new CollectionViewModel(); - - public CollectionViewModel Comments - { - get { return _comments; } - } - - private readonly CollectionViewModel _events = new CollectionViewModel(); - - public CollectionViewModel Events - { - get { return _events; } - } - - public string ConvertToMarkdown(string str) - { - return (GetService().Convert(str)); + get { + return new MvxCommand(() => + { + ShowViewModel(new PullRequestFilesViewModel.NavObject { + Username = Username, + Repository = Repository, + PullRequestId = Id, + Sha = PullRequest.Head?.Sha + }); + }); + } } - public PullRequestViewModel(IFeaturesService featuresService) + public PullRequestViewModel( + IApplicationService applicationService, + IFeaturesService featuresService, + IMessageService messageService, + IMarkdownService markdownService) { + _applicationService = applicationService; _featuresService = featuresService; + _messageService = messageService; + _markdownService = markdownService; + + this.Bind(x => x.PullRequest, true) + .Where(x => x != null) + .Select(x => string.Equals(x.State, "closed")) + .Subscribe(x => IsClosed = x); - this.Bind(x => x.PullRequest, true).IsNotNull().Select(x => string.Equals(x.State, "closed")).Subscribe(x => IsClosed = x); - GoToOwner = ReactiveUI.ReactiveCommand.Create(this.Bind(x => x.Issue, true).Select(x => x != null)); - GoToOwner.Subscribe(_ => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login })); + this.Bind(x => x.Issue, true) + .SelectMany(issue => _markdownService.Convert(issue?.Body).ToObservable()) + .Subscribe(x => MarkdownDescription = x); + + GoToOwner = ReactiveUI.ReactiveCommand.Create( + () => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login }), + this.Bind(x => x.Issue, true).Select(x => x != null)); } public void Init(NavObject navObject) @@ -220,14 +247,14 @@ public void Init(NavObject navObject) Repository = navObject.Repository; Id = navObject.Id; - _issueEditSubscription = Messenger.SubscribeOnMainThread(x => + _issueEditSubscription = _messageService.Listen(x => { if (x.Issue == null || x.Issue.Number != Id) return; Issue = x.Issue; }); - _pullRequestEditSubscription = Messenger.SubscribeOnMainThread(x => + _pullRequestEditSubscription = _messageService.Listen(x => { if (x.PullRequest == null || x.PullRequest.Number != Id) return; @@ -239,8 +266,9 @@ public async Task AddComment(string text) { try { - var comment = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].CreateComment(text)); - Comments.Items.Add(comment.Data); + var comment = await _applicationService.GitHubClient.Issue.Comment.Create(Username, Repository, (int)Id, text); + var newCommentList = new List(Comments) { comment }; + Comments = newCommentList; return true; } catch (Exception e) @@ -256,7 +284,7 @@ private async Task ToggleState(bool closed) { IsModifying = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[Id].UpdateState(closed ? "closed" : "open")); - Messenger.Publish(new PullRequestEditMessage(this) { PullRequest = data.Data }); + _messageService.Send(new PullRequestEditMessage(data.Data)); } catch (Exception e) { @@ -279,17 +307,30 @@ protected override Task Load() { ShouldShowPro = false; + _applicationService + .GitHubClient.Issue.Comment.GetAllForIssue(Username, Repository, (int)Id) + .ToBackground(x => Comments = x); + + _applicationService + .GitHubClient.Issue.Events.GetAllForIssue(Username, Repository, (int)Id) + .ToBackground(x => Events = x); + var pullRequest = this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[Id].Get(); var t1 = this.RequestModel(pullRequest, response => PullRequest = response.Data); - Events.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].GetEvents()).FireAndForget(); - Comments.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].GetComments()).FireAndForget(); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data).FireAndForget(); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Get(), response => { - CanPush = response.Data.Permissions.Push; - ShouldShowPro = response.Data.Private && !_featuresService.IsProEnabled; - }).FireAndForget(); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].IsCollaborator(this.GetApplication().Account.Username), - response => IsCollaborator = response.Data).FireAndForget(); + this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data).ToBackground(); + + _applicationService + .GitHubClient.Repository.Get(Username, Repository) + .ToBackground(x => + { + CanPush = x.Permissions.Push; + ShouldShowPro = x.Private && !_featuresService.IsProEnabled; + }); + + _applicationService + .GitHubClient.Repository.Collaborator.IsCollaborator(Username, Repository, _applicationService.Account.Username) + .ToBackground(x => IsCollaborator = x); + return t1; } @@ -307,7 +348,7 @@ public async Task Merge() this.AlertService.Alert("Unable to Merge!", e.Message).ToBackground(); } - await Load().FireAndForget(); + await Load(); } public ICommand MergeCommand diff --git a/CodeHub.Core/ViewModels/PullRequests/PullRequestsViewModel.cs b/CodeHub.Core/ViewModels/PullRequests/PullRequestsViewModel.cs index 61c5ba2a..df67ac3e 100755 --- a/CodeHub.Core/ViewModels/PullRequests/PullRequestsViewModel.cs +++ b/CodeHub.Core/ViewModels/PullRequests/PullRequestsViewModel.cs @@ -2,17 +2,17 @@ using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; -using MvvmCross.Plugins.Messenger; using CodeHub.Core.Messages; using System.Linq; +using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestsViewModel : LoadableViewModel { - private MvxSubscriptionToken _pullRequestEditSubscription; + private readonly IMessageService _messageService; + private IDisposable _pullRequestEditSubscription; private readonly CollectionViewModel _pullrequests = new CollectionViewModel(); public CollectionViewModel PullRequests @@ -40,8 +40,9 @@ public ICommand GoToPullRequestCommand get { return new MvxCommand(x => ShowViewModel(new PullRequestViewModel.NavObject { Username = Username, Repository = Repository, Id = x.Number })); } } - public PullRequestsViewModel() + public PullRequestsViewModel(IMessageService messageService) { + _messageService = messageService; this.Bind(x => x.SelectedFilter).Subscribe(_ => LoadCommand.Execute(null)); } @@ -55,7 +56,7 @@ public void Init(NavObject navObject) return x.Where(y => y.State == state); }; - _pullRequestEditSubscription = Messenger.SubscribeOnMainThread(x => + _pullRequestEditSubscription = _messageService.Listen(x => { if (x.PullRequest == null) return; diff --git a/CodeHub.Core/ViewModels/Repositories/OrganizationRepositoriesViewModel.cs b/CodeHub.Core/ViewModels/Repositories/OrganizationRepositoriesViewModel.cs deleted file mode 100755 index d434819e..00000000 --- a/CodeHub.Core/ViewModels/Repositories/OrganizationRepositoriesViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class OrganizationRepositoriesViewModel : RepositoriesViewModel - { - public string Name - { - get; - private set; - } - - public OrganizationRepositoriesViewModel() - { - ShowRepositoryOwner = false; - } - - public void Init(NavObject navObject) - { - Name = navObject.Name; - } - - protected override Task Load() - { - return Repositories.SimpleCollectionLoad(this.GetApplication().Client.Organizations[Name].Repositories.GetAll()); - } - - public class NavObject - { - public string Name { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/ReadmeViewModel.cs b/CodeHub.Core/ViewModels/Repositories/ReadmeViewModel.cs deleted file mode 100644 index a8908a32..00000000 --- a/CodeHub.Core/ViewModels/Repositories/ReadmeViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Input; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Services; -using MvvmCross.Core.ViewModels; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class ReadmeViewModel : LoadableViewModel - { - private readonly IMarkdownService _markdownService; - - public string RepositoryOwner { get; private set; } - - public string RepositoryName { get; private set; } - - private string _contentText; - public string ContentText - { - get { return _contentText; } - private set { this.RaiseAndSetIfChanged(ref _contentText, value); } - } - - private ContentModel _contentModel; - public ContentModel ContentModel - { - get { return _contentModel; } - set { this.RaiseAndSetIfChanged(ref _contentModel, value); } - } - - public string HtmlUrl - { - get { return _contentModel?.HtmlUrl; } - } - - public ICommand GoToGitHubCommand - { - get { return new MvxCommand(() => GoToUrlCommand.Execute(HtmlUrl), () => _contentModel != null); } - } - - public ICommand GoToLinkCommand - { - get { return GoToUrlCommand; } - } - - public ReadmeViewModel(IMarkdownService markdownService) - { - Title = "Readme"; - _markdownService = markdownService; - } - - protected override async Task Load() - { - var cmd = this.GetApplication().Client.Users[RepositoryOwner].Repositories[RepositoryName].GetReadme(); - var result = await this.GetApplication().Client.ExecuteAsync(cmd); - ContentModel = result.Data; - ContentText = _markdownService.Convert(Encoding.UTF8.GetString(Convert.FromBase64String(result.Data.Content))); - } - - public void Init(NavObject navObject) - { - RepositoryOwner = navObject.Username; - RepositoryName = navObject.Repository; - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - } - } -} diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesExploreViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesExploreViewModel.cs deleted file mode 100755 index cc4c5a85..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesExploreViewModel.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; -using System.Threading.Tasks; -using CodeHub.Core.ViewModels.Repositories; -using System; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoriesExploreViewModel : BaseViewModel - { - private readonly CollectionViewModel _repositories = new CollectionViewModel(); - private string _searchText; - - public bool ShowRepositoryDescription - { - get { return this.GetApplication().Account.ShowRepositoryDescriptionInList; } - } - - public CollectionViewModel Repositories - { - get { return _repositories; } - } - - public string SearchText - { - get { return _searchText; } - set { this.RaiseAndSetIfChanged(ref _searchText, value); } - } - - private bool _isSearching; - public bool IsSearching - { - get { return _isSearching; } - private set { this.RaiseAndSetIfChanged(ref _isSearching, value); } - } - - public ICommand GoToRepositoryCommand - { - get { return new MvxCommand(x => ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Owner.Login, Repository = x.Name })); } - } - - public ICommand SearchCommand - { - get { return new MvxCommand(() => Search(), () => !string.IsNullOrEmpty(SearchText)); } - } - - private async Task Search() - { - try - { - IsSearching = true; - - var request = this.GetApplication().Client.Repositories.SearchRepositories(new [] { SearchText }, new string[] { }); - var response = await this.GetApplication().Client.ExecuteAsync(request); - Repositories.Items.Reset(response.Data.Items); - } - catch - { - DisplayAlert("Unable to search for repositories. Please try again."); - } - finally - { - IsSearching = false; - } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesForkedViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesForkedViewModel.cs deleted file mode 100644 index 82995be7..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesForkedViewModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoriesForkedViewModel : RepositoriesViewModel - { - public RepositoriesForkedViewModel() - { - ShowRepositoryOwner = true; - } - - protected override Task Load() - { - return Repositories.SimpleCollectionLoad(this.GetApplication().Client.Users[User].Repositories[Repository].GetForks()); - } - - public string User - { - get; - private set; - } - - public string Repository - { - get; - private set; - } - - public void Init(NavObject navObject) - { - User = navObject.User; - Repository = navObject.Repository; - } - - public class NavObject - { - public string User { get; set; } - public string Repository { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesStarredViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesStarredViewModel.cs deleted file mode 100755 index 32443365..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesStarredViewModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoriesStarredViewModel : RepositoriesViewModel - { - public RepositoriesStarredViewModel() - { - ShowRepositoryOwner = true; - } - - protected override Task Load() - { - return Repositories.SimpleCollectionLoad(this.GetApplication().Client.AuthenticatedUser.Repositories.GetStarred()); - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesTrendingViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesTrendingViewModel.cs deleted file mode 100644 index 8808cc84..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesTrendingViewModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using CodeHub.Core.ViewModels; -using System.Windows.Input; -using System.Threading.Tasks; -using MvvmCross.Core.ViewModels; -using System.Collections.Generic; -using CodeHub.Core.Data; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoriesTrendingViewModel : LoadableViewModel - { - private readonly Language _defaultLanguage = new Language("All Languages", null); - - private IList>> _repos; - public IList>> Repositories - { - get { return _repos; } - private set { this.RaiseAndSetIfChanged(ref _repos, value); } - } - - private Language _selectedLanguage; - public Language SelectedLanguage - { - get { return _selectedLanguage; } - set { this.RaiseAndSetIfChanged(ref _selectedLanguage, value); } - } - - public bool ShowRepositoryDescription - { - get { return this.GetApplication().Account.ShowRepositoryDescriptionInList; } - } - - public RepositoriesTrendingViewModel() - { - SelectedLanguage = _defaultLanguage; - } - - public void Init() - { - this.Bind(x => x.SelectedLanguage).Subscribe(_ => LoadCommand.Execute(null)); - } - - public ICommand GoToRepositoryCommand - { - get { return new MvxCommand(x => ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Owner?.Login, Repository = x.Name })); } - } - - protected override async Task Load() - { - var trendingRepo = new TrendingRepository(); - var repos = new List>>(); - var times = new [] - { - Tuple.Create("Daily", "daily"), - Tuple.Create("Weekly", "weekly"), - Tuple.Create("Monthly", "monthly"), - }; - - foreach (var t in times) - { - var repo = await trendingRepo.GetTrendingRepositories(t.Item2, SelectedLanguage.Slug); - repos.Add(Tuple.Create(t.Item1, repo)); - } - - Repositories = repos; - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesViewModel.cs index bc89775d..7bdb7db0 100755 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesViewModel.cs +++ b/CodeHub.Core/ViewModels/Repositories/RepositoriesViewModel.cs @@ -1,29 +1,171 @@ -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; - +using ReactiveUI; +using System.Reactive; +using CodeHub.Core.Services; +using Octokit; +using Splat; +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Linq; + namespace CodeHub.Core.ViewModels.Repositories { - public abstract class RepositoriesViewModel : LoadableViewModel + public class RepositoriesViewModel : ReactiveObject { - public bool ShowRepositoryDescription - { - get { return this.GetApplication().Account.ShowRepositoryDescriptionInList; } + private readonly IApplicationService _applicationService; + private readonly IAlertDialogService _dialogService; + private readonly ReactiveList _internalItems + = new ReactiveList(resetChangeThreshold: double.MaxValue); + + public ReactiveCommand LoadCommand { get; } + + public ReactiveCommand LoadMoreCommand { get; } + + public IReadOnlyReactiveList Items { get; private set; } + + public ReactiveCommand RepositoryItemSelected { get; } + + private ObservableAsPropertyHelper _hasMore; + public bool HasMore => _hasMore.Value; + + private Uri _nextPage; + private Uri NextPage + { + get { return _nextPage; } + set { this.RaiseAndSetIfChanged(ref _nextPage, value); } + } + + private string _searchText; + public string SearchText + { + get { return _searchText; } + set { this.RaiseAndSetIfChanged(ref _searchText, value); } + } + + private readonly ObservableAsPropertyHelper _isEmpty; + public bool IsEmpty => _isEmpty.Value; + + public static RepositoriesViewModel CreateWatchedViewModel() + => new RepositoriesViewModel(ApiUrls.Watched()); + + public static RepositoriesViewModel CreateStarredViewModel() + => new RepositoriesViewModel(ApiUrls.Starred()); + + public static RepositoriesViewModel CreateForkedViewModel(string username, string repository) + => new RepositoriesViewModel(ApiUrls.RepositoryForks(username, repository)); + + public static RepositoriesViewModel CreateOrganizationViewModel(string org) + => new RepositoriesViewModel(ApiUrls.OrganizationRepositories(org)); + + public static RepositoriesViewModel CreateTeamViewModel(int id) + => new RepositoriesViewModel(ApiUrls.TeamRepositories(id)); + + public static RepositoriesViewModel CreateMineViewModel() + => new RepositoriesViewModel(ApiUrls.Repositories(), false, "owner,collaborator"); + + public static RepositoriesViewModel CreateUsersViewModel(string username) + { + var applicationService = Locator.Current.GetService(); + var isCurrent = string.Equals(applicationService.Account.Username, username, StringComparison.OrdinalIgnoreCase); + + return isCurrent + ? CreateMineViewModel() + : new RepositoriesViewModel(ApiUrls.Repositories(username)); + } + + public RepositoriesViewModel( + Uri repositoriesUri, + bool showOwner = true, + string affiliation = null, + IApplicationService applicationService = null, + IAlertDialogService dialogService = null) + { + _applicationService = applicationService ?? Locator.Current.GetService(); + _dialogService = dialogService ?? Locator.Current.GetService(); + + NextPage = repositoriesUri; + + var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; + + RepositoryItemSelected = ReactiveCommand.Create(x => x); + + var repositoryItems = _internalItems.CreateDerivedCollection( + x => new RepositoryItemViewModel(x, showOwner, showDescription, GoToRepository)); + + var searchUpdated = this.WhenAnyValue(x => x.SearchText) + .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); + + Items = repositoryItems + .CreateDerivedCollection( + x => x, + x => x.Name.ContainsKeyword(SearchText), + signalReset: searchUpdated); + + LoadCommand = ReactiveCommand.CreateFromTask(async t => + { + _internalItems.Clear(); + var parameters = new Dictionary + { + ["per_page"] = 75.ToString() + }; + + if (affiliation != null) + { + parameters["affiliation"] = affiliation; + } + + var items = await RetrieveRepositories(repositoriesUri, parameters); + _internalItems.AddRange(items); + return items.Count > 0; + }); + + var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); + LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => + { + var items = await RetrieveRepositories(NextPage); + _internalItems.AddRange(items); + return items.Count > 0; + }, canLoadMore); + + LoadCommand.Select(_ => _internalItems.Count == 0) + .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); + + LoadCommand.ThrownExceptions.Subscribe(LoadingError); + + LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); + + _hasMore = this.WhenAnyValue(x => x.NextPage) + .Select(x => x != null) + .ToProperty(this, x => x.HasMore); + } + + private void LoadingError(Exception err) + { + var message = err.Message; + var baseException = err.GetInnerException(); + if (baseException is System.Net.Sockets.SocketException) + { + message = "Unable to communicate with GitHub. " + baseException.Message; + } + + _dialogService.Alert("Error Loading", message).ToBackground(); + } + + private void GoToRepository(RepositoryItemViewModel item) + { + RepositoryItemSelected.ExecuteNow(item); } - public CollectionViewModel Repositories { get; } - - public bool ShowRepositoryOwner { get; protected set; } - - public ICommand GoToRepositoryCommand - { - get { return new MvxCommand(x => this.ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Owner.Login, Repository = x.Name })); } - } - - protected RepositoriesViewModel() + private async Task> RetrieveRepositories( + Uri repositoriesUri, IDictionary parameters = null) { - Repositories = new CollectionViewModel(); + var connection = _applicationService.GitHubClient.Connection; + var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); + NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") + ? ret.HttpResponse.ApiInfo.Links["next"] + : null; + return ret.Body; } } } \ No newline at end of file diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoriesWatchedViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoriesWatchedViewModel.cs deleted file mode 100755 index e97617b7..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoriesWatchedViewModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoriesWatchedViewModel : RepositoriesViewModel - { - public RepositoriesWatchedViewModel() - { - ShowRepositoryOwner = true; - } - - protected override Task Load() - { - return Repositories.SimpleCollectionLoad(this.GetApplication().Client.AuthenticatedUser.Repositories.GetWatching()); - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoryCollaboratorsViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoryCollaboratorsViewModel.cs deleted file mode 100644 index 9e0a1750..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoryCollaboratorsViewModel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Threading.Tasks; -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using CodeHub.Core.ViewModels.User; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoryCollaboratorsViewModel : LoadableViewModel - { - private readonly CollectionViewModel _collaborators = new CollectionViewModel(); - - public CollectionViewModel Collaborators - { - get { return _collaborators; } - } - - public string Username { get; private set; } - - public string Repository { get; private set; } - - public ICommand GoToUserCommand - { - get { return new MvxCommand(x => this.ShowViewModel(new UserViewModel.NavObject { Username = x.Login })); } - } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - Repository = navObject.Repository; - } - - protected override Task Load() - { - return Collaborators.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].GetCollaborators()); - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoryItemViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoryItemViewModel.cs new file mode 100644 index 00000000..6a50a029 --- /dev/null +++ b/CodeHub.Core/ViewModels/Repositories/RepositoryItemViewModel.cs @@ -0,0 +1,50 @@ +using ReactiveUI; +using System; +using CodeHub.Core.Utilities; +using System.Diagnostics; +using System.Reactive; + +namespace CodeHub.Core.ViewModels.Repositories +{ + [DebuggerDisplay("{Owner}/{Name}")] + public class RepositoryItemViewModel : ReactiveObject, ICanGoToViewModel + { + public string Name => Repository.Name; + + public string Owner => Repository.Owner?.Login ?? string.Empty; + + public GitHubAvatar Avatar => new GitHubAvatar(Repository.Owner?.AvatarUrl); + + public string Description { get; } + + public string Stars => Repository.StargazersCount.ToString(); + + public string Forks => Repository.ForksCount.ToString(); + + public bool ShowOwner { get; } + + public Octokit.Repository Repository { get; } + + public ReactiveCommand GoToCommand { get; } + + public RepositoryItemViewModel(Octokit.Repository repository, bool showOwner, bool showDescription, Action gotoCommand) + { + if (showDescription) + { + if (!string.IsNullOrEmpty(repository.Description) && repository.Description.IndexOf(':') >= 0) + Description = Emojis.FindAndReplace(repository.Description); + else + Description = repository.Description; + } + else + { + Description = null; + } + + Repository = repository; + ShowOwner = showOwner; + GoToCommand = ReactiveCommand.Create(() => gotoCommand?.Invoke(this)); + } + } +} + diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoryStargazersViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoryStargazersViewModel.cs deleted file mode 100755 index 0dafd024..00000000 --- a/CodeHub.Core/ViewModels/Repositories/RepositoryStargazersViewModel.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Threading.Tasks; -using System.Windows.Input; -using CodeHub.Core.ViewModels; -using CodeHub.Core.ViewModels.User; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class RepositoryStargazersViewModel : BaseUserCollectionViewModel - { - public string User - { - get; - private set; - } - - public string Repository - { - get; - private set; - } - - public void Init(NavObject navObject) - { - User = navObject.User; - Repository = navObject.Repository; - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Users[User].Repositories[Repository].GetStargazers()); - } - - public class NavObject - { - public string User { get; set; } - public string Repository { get; set; } - } - } - - public class RepositoryWatchersViewModel : BaseUserCollectionViewModel - { - public string User - { - get; - private set; - } - - public string Repository - { - get; - private set; - } - - public void Init(NavObject navObject) - { - User = navObject.User; - Repository = navObject.Repository; - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Users[User].Repositories[Repository].GetWatchers()); - } - - public class NavObject - { - public string User { get; set; } - public string Repository { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Repositories/RepositoryViewModel.cs b/CodeHub.Core/ViewModels/Repositories/RepositoryViewModel.cs index dac890b5..9d80e22b 100644 --- a/CodeHub.Core/ViewModels/Repositories/RepositoryViewModel.cs +++ b/CodeHub.Core/ViewModels/Repositories/RepositoryViewModel.cs @@ -1,18 +1,23 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.ViewModels.User; using CodeHub.Core.ViewModels.Events; using CodeHub.Core.ViewModels.Changesets; +using System.Linq; +using System; +using CodeHub.Core.Services; +using Splat; +using CodeHub.Core.ViewModels.Source; namespace CodeHub.Core.ViewModels.Repositories { public class RepositoryViewModel : LoadableViewModel { + private readonly IApplicationService _applicationService; + public string Username { get; private set; } public string RepositoryName { get; private set; } @@ -33,56 +38,36 @@ public bool? IsWatched private set { this.RaiseAndSetIfChanged(ref _watched, value); } } - private RepositoryModel _repository; - public RepositoryModel Repository + private Octokit.Repository _repository; + public Octokit.Repository Repository { get { return _repository; } - private set { this.RaiseAndSetIfChanged(ref _repository, value); } + set { this.RaiseAndSetIfChanged(ref _repository, value); } } - private ContentModel _readme; - public ContentModel Readme + private Octokit.Readme _readme; + public Octokit.Readme Readme { get { return _readme; } private set { this.RaiseAndSetIfChanged(ref _readme, value); } } - private List _branches; - public List Branches + private IReadOnlyList _branches; + public IReadOnlyList Branches { get { return _branches; } private set { this.RaiseAndSetIfChanged(ref _branches, value); } } - public void Init(NavObject navObject) - { - Username = navObject.Username; - Title = RepositoryName = navObject.Repository; - } - - public ICommand GoToOwnerCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserViewModel.NavObject { Username = Username })); } - } - - public ICommand GoToForkParentCommand + public RepositoryViewModel(IApplicationService applicationService = null) { - get { return new MvxCommand(x => ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Owner.Login, Repository = x.Name })); } + _applicationService = applicationService ?? Locator.Current.GetService(); } - public ICommand GoToStargazersCommand - { - get { return new MvxCommand(() => ShowViewModel(new RepositoryStargazersViewModel.NavObject { User = Username, Repository = RepositoryName })); } - } - - public ICommand GoToWatchersCommand - { - get { return new MvxCommand(() => ShowViewModel(new RepositoryWatchersViewModel.NavObject { User = Username, Repository = RepositoryName })); } - } - - public ICommand GoToForkedCommand + public void Init(NavObject navObject) { - get { return new MvxCommand(() => ShowViewModel(new RepositoriesForkedViewModel.NavObject { User = Username, Repository = RepositoryName })); } + Username = navObject.Username; + Title = RepositoryName = navObject.Repository; } public ICommand GoToEventsCommand @@ -95,39 +80,16 @@ public ICommand GoToIssuesCommand get { return new MvxCommand(() => ShowViewModel(new Issues.IssuesViewModel.NavObject { Username = Username, Repository = RepositoryName })); } } - public ICommand GoToReadmeCommand - { - get { return new MvxCommand(() => ShowViewModel(new ReadmeViewModel.NavObject { Username = Username, Repository = RepositoryName })); } - } - - public ICommand GoToCommitsCommand - { - get { return new MvxCommand(ShowCommits);} - } - public ICommand GoToPullRequestsCommand { get { return new MvxCommand(() => ShowViewModel(new PullRequests.PullRequestsViewModel.NavObject { Username = Username, Repository = RepositoryName })); } } - public ICommand GoToSourceCommand - { - get { return new MvxCommand(() => ShowViewModel(new Source.BranchesAndTagsViewModel.NavObject { Username = Username, Repository = RepositoryName })); } - } - public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => ShowViewModel(new WebBrowserViewModel.NavObject { Url = Repository.HtmlUrl }), () => Repository != null); } } - private void ShowCommits() - { - if (Branches != null && Branches.Count == 1) - ShowViewModel(new ChangesetsViewModel.NavObject {Username = Username, Repository = RepositoryName}); - else - ShowViewModel(new Source.ChangesetBranchesViewModel.NavObject {Username = Username, Repository = RepositoryName}); - } - public ICommand PinCommand { get { return new MvxCommand(PinRepository, () => Repository != null); } @@ -137,33 +99,54 @@ private void PinRepository() { var repoOwner = Repository.Owner.Login; var repoName = Repository.Name; + var account = this.GetApplication().Account; + var pinnedRepository = + account.PinnedRepositories + .FirstOrDefault(x => string.Equals(repoName, x.Name, StringComparison.OrdinalIgnoreCase) && + string.Equals(repoOwner, x.Owner, StringComparison.OrdinalIgnoreCase)); //Is it pinned already or not? - var pinnedRepo = this.GetApplication().Account.PinnnedRepositories.GetPinnedRepository(repoOwner, repoName); - if (pinnedRepo == null) - this.GetApplication().Account.PinnnedRepositories.AddPinnedRepository(repoOwner, repoName, repoName, ImageUrl); + if (pinnedRepository == null) + { + account.PinnedRepositories.Add(new Data.PinnedRepository + { + Name = repoName, + Owner = repoOwner, + ImageUri = ImageUrl, + Slug = repoName + }); + + _applicationService.UpdateActiveAccount().ToBackground(); + + } else - this.GetApplication().Account.PinnnedRepositories.RemovePinnedRepository(pinnedRepo.Id); + { + account.PinnedRepositories.Remove(pinnedRepository); + _applicationService.UpdateActiveAccount().ToBackground(); + } } - protected override Task Load() + protected override async Task Load() { - var t1 = this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].Get(), response => Repository = response.Data); + _applicationService.GitHubClient.Repository.Content + .GetReadme(Username, RepositoryName).ToBackground(x => Readme = x); + + _applicationService.GitHubClient.Repository.Branch + .GetAll(Username, RepositoryName).ToBackground(x => Branches = x); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].GetReadme(), - response => Readme = response.Data).FireAndForget(); + _applicationService.GitHubClient.Activity.Starring + .CheckStarred(Username, RepositoryName).ToBackground(x => IsStarred = x); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].GetBranches(), - response => Branches = response.Data).FireAndForget(); + _applicationService.GitHubClient.Activity.Watching + .CheckWatched(Username, RepositoryName).ToBackground(x => IsWatched = x); - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].IsWatching(), - response => IsWatched = response.Data).FireAndForget(); - - this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].IsStarred(), - response => IsStarred = response.Data).FireAndForget(); + var retrieveRepository = _applicationService.GitHubClient.Repository.Get(Username, RepositoryName); - return t1; + if (Repository == null) + Repository = await retrieveRepository; + else + retrieveRepository.ToBackground(repo => Repository = repo); } public ICommand ToggleWatchCommand @@ -197,7 +180,12 @@ public ICommand ToggleStarCommand public bool IsPinned { - get { return this.GetApplication().Account.PinnnedRepositories.GetPinnedRepository(Username, RepositoryName) != null; } + get + { + var repos = this.GetApplication().Account.PinnedRepositories; + return repos.Any(x => string.Equals(x.Owner, Username, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.Slug, RepositoryName, StringComparison.OrdinalIgnoreCase)); + } } private async Task ToggleStar() diff --git a/CodeHub.Core/ViewModels/Repositories/TrendingRepositoriesViewModel.cs b/CodeHub.Core/ViewModels/Repositories/TrendingRepositoriesViewModel.cs new file mode 100644 index 00000000..392cb47a --- /dev/null +++ b/CodeHub.Core/ViewModels/Repositories/TrendingRepositoriesViewModel.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using CodeHub.Core.Data; +using ReactiveUI; +using System.Reactive; +using System.Reactive.Linq; +using System.Linq; +using CodeHub.Core.Services; +using Splat; + +namespace CodeHub.Core.ViewModels.Repositories +{ + public class RepositoriesTrendingViewModel : ReactiveObject + { + private readonly Language _defaultLanguage = new Language("All Languages", null); + private readonly ITrendingRepository _trendingRepository; + private readonly IAlertDialogService _dialogService; + + private static readonly Tuple[] Times = { + Tuple.Create("Daily", "daily"), + Tuple.Create("Weekly", "weekly"), + Tuple.Create("Monthly", "monthly"), + }; + + private IList>> _items; + public IList>> Items + { + get { return _items ?? new List>>(); } + private set { this.RaiseAndSetIfChanged(ref _items, value); } + } + + private Language _selectedLanguage; + public Language SelectedLanguage + { + get { return _selectedLanguage; } + set { this.RaiseAndSetIfChanged(ref _selectedLanguage, value); } + } + + public ReactiveCommand LoadCommand { get; } + + public ReactiveCommand RepositoryItemSelected { get; } + + public RepositoriesTrendingViewModel( + ITrendingRepository trendingRepository = null, + IAlertDialogService dialogService = null) + { + _trendingRepository = trendingRepository ?? new TrendingRepository(); + _dialogService = dialogService ?? Locator.Current.GetService(); + + RepositoryItemSelected = ReactiveCommand.Create(x => x); + + LoadCommand = ReactiveCommand.CreateFromTask(Load); + LoadCommand.ThrownExceptions.Subscribe(LoadingError); + + SelectedLanguage = _defaultLanguage; + + this.WhenAnyValue(x => x.SelectedLanguage) + .Skip(1) + .Select(_ => Unit.Default) + .Do(_ => Items = null) + .InvokeReactiveCommand(LoadCommand); + } + + private void LoadingError(Exception err) + { + var message = err.Message; + var baseException = err.GetInnerException(); + if (baseException is System.Net.Sockets.SocketException) + { + message = "Unable to communicate with GitHub. " + baseException.Message; + } + + _dialogService.Alert("Error Loading", message).ToBackground(); + } + + private async Task Load() + { + var items = new List>>(); + + foreach (var t in Times) + { + var repos = await _trendingRepository.GetTrendingRepositories(t.Item2, SelectedLanguage.Slug); + var viewModels = repos + .Select(x => new RepositoryItemViewModel(x, true, true, vm => RepositoryItemSelected.ExecuteNow(vm))) + .ToList(); + + items.Add(Tuple.Create(t.Item1, viewModels as IList)); + } + + Items = items; + } + } +} + diff --git a/CodeHub.Core/ViewModels/Repositories/UserRepositoriesViewModel.cs b/CodeHub.Core/ViewModels/Repositories/UserRepositoriesViewModel.cs deleted file mode 100755 index 00aa3d89..00000000 --- a/CodeHub.Core/ViewModels/Repositories/UserRepositoriesViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using CodeHub.Core.ViewModels.Repositories; -using GitHubSharp; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.Repositories -{ - public class UserRepositoriesViewModel : RepositoriesViewModel - { - public string Username { get; private set; } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - } - - protected override Task Load() - { - GitHubRequest> request; - if (string.Equals(this.GetApplication().Account.Username, Username, StringComparison.OrdinalIgnoreCase)) - request = this.GetApplication().Client.AuthenticatedUser.Repositories.GetAll(); - else - request = this.GetApplication().Client.Users[Username].Repositories.GetAll(); - return Repositories.SimpleCollectionLoad(request); - } - - public class NavObject - { - public string Username { get; set; } - } - } -} diff --git a/CodeHub.Core/ViewModels/Search/ExploreViewModel.cs b/CodeHub.Core/ViewModels/Search/ExploreViewModel.cs new file mode 100644 index 00000000..4822faa9 --- /dev/null +++ b/CodeHub.Core/ViewModels/Search/ExploreViewModel.cs @@ -0,0 +1,25 @@ +using ReactiveUI; + +namespace CodeHub.Core.ViewModels.Search +{ + public class ExploreViewModel : ReactiveObject + { + public RepositoryExploreViewModel Repositories { get; } = new RepositoryExploreViewModel(); + + public UserExploreViewModel Users { get; } = new UserExploreViewModel(); + + private SearchType _searchFilter = SearchType.Repositories; + public SearchType SearchFilter + { + get { return _searchFilter; } + set { this.RaiseAndSetIfChanged(ref _searchFilter, value); } + } + + public enum SearchType + { + Repositories = 0, + Users + } + } +} + diff --git a/CodeHub.Core/ViewModels/Search/RepositoryExploreViewModel.cs b/CodeHub.Core/ViewModels/Search/RepositoryExploreViewModel.cs new file mode 100755 index 00000000..5c72bc5e --- /dev/null +++ b/CodeHub.Core/ViewModels/Search/RepositoryExploreViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Reactive; +using CodeHub.Core.Services; +using ReactiveUI; +using Octokit; +using System.Reactive.Linq; +using Splat; +using CodeHub.Core.ViewModels.Repositories; + +namespace CodeHub.Core.ViewModels.Search +{ + public class RepositoryExploreViewModel : ReactiveObject + { + private readonly IApplicationService _applicationService; + + public ReactiveCommand SearchCommand { get; } + + public IReadOnlyReactiveList Items { get; private set; } + + public ReactiveCommand RepositoryItemSelected { get; } + + private string _searchText; + public string SearchText + { + get { return _searchText; } + set { this.RaiseAndSetIfChanged(ref _searchText, value); } + } + + public RepositoryExploreViewModel( + IApplicationService applicationService = null, + IAlertDialogService dialogService = null) + { + _applicationService = applicationService ?? Locator.Current.GetService(); + dialogService = dialogService ?? Locator.Current.GetService(); + + RepositoryItemSelected = ReactiveCommand.Create(x => x); + var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; + + var internalItems = new ReactiveList(resetChangeThreshold: double.MaxValue); + + Items = internalItems.CreateDerivedCollection(x => + new RepositoryItemViewModel(x, true, showDescription, y => RepositoryItemSelected.ExecuteNow(y))); + + var canSearch = this.WhenAnyValue(x => x.SearchText).Select(x => !string.IsNullOrEmpty(x)); + SearchCommand = ReactiveCommand.CreateFromTask(async t => + { + try + { + internalItems.Clear(); + var request = new SearchRepositoriesRequest(SearchText); + var response = await _applicationService.GitHubClient.Search.SearchRepo(request); + internalItems.Reset(response.Items); + } + catch (Exception e) + { + var msg = string.Format("Unable to search for {0}. Please try again.", SearchText); + throw new Exception(msg, e); + } + }, canSearch); + + SearchCommand + .ThrownExceptions + .Subscribe(err => dialogService.Alert("Error Searching", err.Message).ToBackground()); + } + } +} + diff --git a/CodeHub.Core/ViewModels/Search/UserExploreViewModel.cs b/CodeHub.Core/ViewModels/Search/UserExploreViewModel.cs new file mode 100644 index 00000000..3dc101a2 --- /dev/null +++ b/CodeHub.Core/ViewModels/Search/UserExploreViewModel.cs @@ -0,0 +1,58 @@ +using System; +using CodeHub.Core.Services; +using ReactiveUI; +using Octokit; +using CodeHub.Core.ViewModels.Users; +using System.Reactive; +using System.Reactive.Linq; +using Humanizer; +using Splat; + +namespace CodeHub.Core.ViewModels.Search +{ + public class UserExploreViewModel : ReactiveObject + { + public ReactiveCommand SearchCommand { get; } + + public IReadOnlyReactiveList Items { get; private set; } + + public ReactiveCommand RepositoryItemSelected { get; } + + private string _searchText; + public string SearchText + { + get { return _searchText; } + set { this.RaiseAndSetIfChanged(ref _searchText, value); } + } + + public UserExploreViewModel(IApplicationService applicationService = null) + { + applicationService = applicationService ?? Locator.Current.GetService(); + + var items = new ReactiveList(); + var itemSelected = ReactiveCommand.Create(x => x); + + Items = items.CreateDerivedCollection( + x => new UserItemViewModel(x, y => itemSelected.ExecuteNow(y))); + + RepositoryItemSelected = itemSelected; + + var canSearch = this.WhenAnyValue(x => x.SearchText).Select(x => !string.IsNullOrEmpty(x)); + SearchCommand = ReactiveCommand.CreateFromTask(async t => { + try + { + items.Clear(); + var request = new SearchUsersRequest(SearchText); + var response = await applicationService.GitHubClient.Search.SearchUsers(request); + items.Reset(response.Items); + } + catch (Exception e) + { + var msg = string.Format("Unable to search for {0}. Please try again.", SearchText.Humanize()); + throw new Exception(msg, e); + } + }, canSearch); + } + } +} + diff --git a/CodeHub.Core/ViewModels/Source/BranchesAndTagsViewModel.cs b/CodeHub.Core/ViewModels/Source/BranchesAndTagsViewModel.cs deleted file mode 100644 index 247b6985..00000000 --- a/CodeHub.Core/ViewModels/Source/BranchesAndTagsViewModel.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Threading.Tasks; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; -using System.Linq; - -namespace CodeHub.Core.ViewModels.Source -{ - public class BranchesAndTagsViewModel : LoadableViewModel - { - private int _selectedFilter; - public int SelectedFilter - { - get { return _selectedFilter; } - set { this.RaiseAndSetIfChanged(ref _selectedFilter, value); } - } - - public string Username { get; private set; } - - public string Repository { get; private set; } - - public CollectionViewModel Items { get; } - - public IMvxCommand GoToSourceCommand - { - get { return new MvxCommand(GoToSource); } - } - - private void GoToSource(ViewObject obj) - { - if (obj.Object is BranchModel) - { - var x = obj.Object as BranchModel; - ShowViewModel(new SourceTreeViewModel.NavObject { Username = Username, Repository = Repository, Branch = x.Name, TrueBranch = true }); - } - else if (obj.Object is TagModel) - { - var x = obj.Object as TagModel; - ShowViewModel(new SourceTreeViewModel.NavObject { Username = Username, Repository = Repository, Branch = x.Commit.Sha }); - } - } - - public BranchesAndTagsViewModel() - { - Items = new CollectionViewModel(); - this.Bind(x => x.SelectedFilter).Subscribe(x => LoadCommand.Execute(false)); - } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - Repository = navObject.Repository; - _selectedFilter = navObject.IsShowingBranches ? 0 : 1; - } - - protected override Task Load() - { - if (SelectedFilter == 0) - { - var request = this.GetApplication().Client.Users[Username].Repositories[Repository].GetBranches(); - return this.RequestModel(request, response => - { - this.CreateMore(response, m => Items.MoreItems = m, d => Items.Items.AddRange(d.Where(x => x != null).Select(x => new ViewObject { Name = x.Name, Object = x }))); - Items.Items.Reset(response.Data.Where(x => x != null).Select(x => new ViewObject { Name = x.Name, Object = x })); - }); - } - else - { - var request = this.GetApplication().Client.Users[Username].Repositories[Repository].GetTags(); - return this.RequestModel(request, response => - { - this.CreateMore(response, m => Items.MoreItems = m, d => Items.Items.AddRange(d.Where(x => x != null).Select(x => new ViewObject { Name = x.Name, Object = x }))); - Items.Items.Reset(response.Data.Where(x => x != null).Select(x => new ViewObject { Name = x.Name, Object = x })); - }); - } - } - - public class ViewObject - { - public string Name { get; set; } - public object Object { get; set; } - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - public bool IsShowingBranches { get; set; } - - public NavObject() - { - IsShowingBranches = true; - } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Source/ChangesetBranchesViewModel.cs b/CodeHub.Core/ViewModels/Source/ChangesetBranchesViewModel.cs deleted file mode 100755 index c982a7d5..00000000 --- a/CodeHub.Core/ViewModels/Source/ChangesetBranchesViewModel.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Threading.Tasks; -using System.Windows.Input; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; -using CodeHub.Core.ViewModels.Changesets; -using MvvmCross.Core.ViewModels; - -namespace CodeHub.Core.ViewModels.Source -{ - public class ChangesetBranchesViewModel : LoadableViewModel - { - private readonly CollectionViewModel _items = new CollectionViewModel(); - - public string Username - { - get; - private set; - } - - public string Repository - { - get; - private set; - } - - public CollectionViewModel Branches - { - get { return _items; } - } - - public ICommand GoToBranchCommand - { - get { return new MvxCommand(x => ShowViewModel(new ChangesetsViewModel.NavObject { Username = Username, Repository = Repository, Branch = x.Name })); } - } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - Repository = navObject.Repository; - } - - protected override Task Load() - { - return Branches.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].GetBranches()); - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Source/ChangesetDiffViewModel.cs b/CodeHub.Core/ViewModels/Source/ChangesetDiffViewModel.cs deleted file mode 100644 index 59cef694..00000000 --- a/CodeHub.Core/ViewModels/Source/ChangesetDiffViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using CodeHub.Core.ViewModels; -using System.Threading.Tasks; -using GitHubSharp.Models; -using CodeHub.Core.Services; -using System.Collections.Generic; -using System.Linq; -using MvvmCross.Platform; - -namespace CodeHub.Core.ViewModels.Source -{ - public class ChangesetDiffViewModel : FileSourceViewModel - { - private readonly CollectionViewModel _comments = new CollectionViewModel(); - private CommitModel.CommitFileModel _commitFileModel; - private string _actualFilename; - - public string Username { get; private set; } - - public string Repository { get; private set; } - - public string Branch { get; private set; } - - public string Filename { get; private set; } - - public CollectionViewModel Comments - { - get { return _comments; } - } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - Repository = navObject.Repository; - Branch = navObject.Branch; - Filename = navObject.Filename; - - _actualFilename = System.IO.Path.GetFileName(Filename); - if (_actualFilename == null) - _actualFilename = Filename.Substring(Filename.LastIndexOf('/') + 1); - - Title = _actualFilename; - - _commitFileModel = Mvx.Resolve().Get() as CommitModel.CommitFileModel; - } - - protected override async Task Load() - { - //Make sure we have this information. If not, go get it - if (_commitFileModel == null) - { - var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Commits[Branch].Get()); - _commitFileModel = data.Data.Files.First(x => string.Equals(x.Filename, Filename)); - } - - FilePath = CreatePlainContentFile(_commitFileModel.Patch, _actualFilename); - await Comments.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Commits[Branch].Comments.GetAll()); - } - - public async Task PostComment(string comment, int line) - { - var c = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Commits[Branch].Comments.Create(comment, Filename, line)); - Comments.Items.Add(c.Data); - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - public string Branch { get; set; } - public string Filename { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Source/EditSourceViewModel.cs b/CodeHub.Core/ViewModels/Source/EditSourceViewModel.cs index 0a64f446..b12d3dd3 100644 --- a/CodeHub.Core/ViewModels/Source/EditSourceViewModel.cs +++ b/CodeHub.Core/ViewModels/Source/EditSourceViewModel.cs @@ -1,12 +1,15 @@ using System; using System.Threading.Tasks; using CodeHub.Core.Messages; +using CodeHub.Core.Services; using MvvmCross.Core.ViewModels; namespace CodeHub.Core.ViewModels.Source { public class EditSourceViewModel : LoadableViewModel { + private readonly IMessageService _messageService; + private string _text; public string Text { @@ -24,6 +27,11 @@ public string Text public string Branch { get; private set; } + public EditSourceViewModel(IMessageService messageService = null) + { + _messageService = messageService ?? GetService(); + } + public void Init(NavObject navObject) { Username = navObject.Username; @@ -47,7 +55,7 @@ public async Task Commit(string data, string message) { var request = this.GetApplication().Client.Users[Username].Repositories[Repository].UpdateContentFile(Path, message, data, BlobSha, Branch); var response = await this.GetApplication().Client.ExecuteAsync(request); - Messenger.Publish(new SourceEditMessage(this) { OldSha = BlobSha, Data = data, Update = response.Data }); + _messageService.Send(new SourceEditMessage { OldSha = BlobSha, Data = data, Update = response.Data }); } public class NavObject diff --git a/CodeHub.Core/ViewModels/Source/SourceTreeViewModel.cs b/CodeHub.Core/ViewModels/Source/SourceTreeViewModel.cs deleted file mode 100755 index 2da5816e..00000000 --- a/CodeHub.Core/ViewModels/Source/SourceTreeViewModel.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Threading.Tasks; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; -using CodeHub.Core.Utils; -using CodeHub.Core.Services; -using System.Reactive.Linq; - -namespace CodeHub.Core.ViewModels.Source -{ - public class SourceTreeViewModel : LoadableViewModel - { - private readonly IFeaturesService _featuresService; - private readonly IApplicationService _applicationService; - - public CollectionViewModel Content { get; } = new CollectionViewModel(); - - public string Username { get; private set; } - - public string Path { get; private set; } - - public string Branch { get; private set; } - - public bool TrueBranch { get; private set; } - - public string Repository { get; private set; } - - private bool _shouldShowPro; - public bool ShouldShowPro - { - get { return _shouldShowPro; } - private set { this.RaiseAndSetIfChanged(ref _shouldShowPro, value); } - } - - public ReactiveUI.IReactiveCommand GoToItemCommand { get; } - - public SourceTreeViewModel(IApplicationService applicationService, IFeaturesService featuresService) - { - _applicationService = applicationService; - _featuresService = featuresService; - - GoToItemCommand = ReactiveUI.ReactiveCommand.Create(); - GoToItemCommand.OfType().Subscribe(x => { - if (x.Type.Equals("dir", StringComparison.OrdinalIgnoreCase)) - { - ShowViewModel(new NavObject { Username = Username, Branch = Branch, - Repository = Repository, Path = x.Path, TrueBranch = TrueBranch }); - } - if (x.Type.Equals("file", StringComparison.OrdinalIgnoreCase)) - { - if (x.DownloadUrl == null) - { - var nameAndSlug = x.GitUrl.Substring(x.GitUrl.IndexOf("/repos/", StringComparison.Ordinal) + 7); - var indexOfGit = nameAndSlug.LastIndexOf("/git", StringComparison.Ordinal); - indexOfGit = indexOfGit < 0 ? 0 : indexOfGit; - var repoId = RepositoryIdentifier.FromFullName(nameAndSlug.Substring(0, indexOfGit)); - var sha = x.GitUrl.Substring(x.GitUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); - ShowViewModel(new NavObject {Username = repoId?.Owner, Repository = repoId?.Name, Branch = sha}); - } - else - { - ShowViewModel(new SourceViewModel.NavObject { - Name = x.Name, Username = Username, Repository = Repository, Branch = Branch, - Path = x.Path, HtmlUrl = x.HtmlUrl, GitUrl = x.GitUrl, TrueBranch = TrueBranch }); - } - } - }); - } - - public void Init(NavObject navObject) - { - Username = navObject.Username; - Repository = navObject.Repository; - Branch = navObject.Branch ?? "master"; - Path = navObject.Path ?? ""; - TrueBranch = navObject.TrueBranch; - } - - protected override Task Load() - { - if (_featuresService.IsProEnabled) - ShouldShowPro = false; - else - { - var request = _applicationService.Client.Users[Username].Repositories[Repository].Get(); - _applicationService.Client.ExecuteAsync(request) - .ToBackground(x => ShouldShowPro = x.Data.Private && !_featuresService.IsProEnabled); - } - - return Content.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].GetContent(Path, Branch)); - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - public string Branch { get; set; } - public string Path { get; set; } - - // Whether the branch is a real branch and not a node - public bool TrueBranch { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/Source/SourceViewModel.cs b/CodeHub.Core/ViewModels/Source/SourceViewModel.cs deleted file mode 100755 index 80b06cab..00000000 --- a/CodeHub.Core/ViewModels/Source/SourceViewModel.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Threading.Tasks; -using System; -using CodeHub.Core.ViewModels; -using CodeHub.Core.Messages; -using MvvmCross.Plugins.Messenger; -using System.Linq; - -namespace CodeHub.Core.ViewModels.Source -{ - public class SourceViewModel : FileSourceViewModel - { - private readonly MvxSubscriptionToken _editToken; - private static readonly string[] MarkdownExtensions = { ".markdown", ".mdown", ".mkdn", ".md", ".mkd", ".mdwn", ".mdtxt", ".mdtext", ".text" }; - - private string _name; - private string _gitUrl; - private bool _forceBinary; - - public string Username { get; private set; } - - public string Repository { get; private set; } - - public string Branch { get; private set; } - - public bool TrueBranch { get; private set; } - - public string Path { get; private set; } - - protected override async Task Load() - { - var fileName = System.IO.Path.GetFileName(_name); - var filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), fileName); - string mime = string.Empty; - - using (var stream = new System.IO.FileStream(filepath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) - { - mime = await this.GetApplication().Client.DownloadRawResource2(_gitUrl, stream) ?? string.Empty; - } - - FilePath = filepath; - - // We can force a binary representation if it was passed during init. In which case we don't care to figure out via the mime. - if (_forceBinary) - return; - - var isText = mime.Contains("charset"); - if (isText) - { - ContentPath = FilePath; - } - } - - public bool CanEdit - { - get { return ContentPath != null && TrueBranch; } - } - - public SourceViewModel() - { - _editToken = Messenger.SubscribeOnMainThread(x => - { - if (x.OldSha == null || x.Update == null) - return; - _gitUrl = x.Update.Content.GitUrl; - if (LoadCommand.CanExecute(null)) - LoadCommand.Execute(true); - }); - } - - public void Init(NavObject navObject) - { - Path = navObject.Path; - HtmlUrl = navObject.HtmlUrl; - _name = navObject.Name; - _gitUrl = navObject.GitUrl; - _forceBinary = navObject.ForceBinary; - Username = navObject.Username; - Repository = navObject.Repository; - Branch = navObject.Branch; - TrueBranch = navObject.TrueBranch; - - //Create the filename - var fileName = System.IO.Path.GetFileName(Path); - if (fileName == null) - fileName = Path.Substring(Path.LastIndexOf('/') + 1); - - //Create the temp file path - Title = fileName; - - var extension = System.IO.Path.GetExtension(Path); - IsMarkdown = MarkdownExtensions.Contains(extension); - } - - public class NavObject - { - public string Username { get; set; } - public string Repository { get; set; } - public string Branch { get; set; } - public string Path { get; set; } - public string HtmlUrl { get; set; } - public string Name { get; set; } - public string GitUrl { get; set; } - public bool ForceBinary { get; set; } - public bool TrueBranch { get; set; } - } - } -} \ No newline at end of file diff --git a/CodeHub.Core/ViewModels/User/BaseUserCollectionViewModel.cs b/CodeHub.Core/ViewModels/User/BaseUserCollectionViewModel.cs deleted file mode 100644 index ebf7a1bd..00000000 --- a/CodeHub.Core/ViewModels/User/BaseUserCollectionViewModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Windows.Input; -using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; -using GitHubSharp.Models; - -namespace CodeHub.Core.ViewModels.User -{ - public abstract class BaseUserCollectionViewModel : LoadableViewModel - { - private readonly CollectionViewModel _users = new CollectionViewModel(); - - public CollectionViewModel Users - { - get { return _users; } - } - - public ICommand GoToUserCommand - { - get { return new MvxCommand(x => this.ShowViewModel(new UserViewModel.NavObject { Username = x.Login })); } - } - } -} diff --git a/CodeHub.Core/ViewModels/User/OrganizationMembersViewModel.cs b/CodeHub.Core/ViewModels/User/OrganizationMembersViewModel.cs deleted file mode 100755 index da206612..00000000 --- a/CodeHub.Core/ViewModels/User/OrganizationMembersViewModel.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.User -{ - public class OrganizationMembersViewModel : BaseUserCollectionViewModel - { - public string OrganizationName - { - get; - private set; - } - - public void Init(NavObject navObject) - { - OrganizationName = navObject.Name; - } - - public class NavObject - { - public string Name { get; set; } - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Organizations[OrganizationName].GetMembers()); - } - } -} - diff --git a/CodeHub.Core/ViewModels/User/TeamMembersViewModel.cs b/CodeHub.Core/ViewModels/User/TeamMembersViewModel.cs deleted file mode 100755 index 764b9c27..00000000 --- a/CodeHub.Core/ViewModels/User/TeamMembersViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.User -{ - public class TeamMembersViewModel : BaseUserCollectionViewModel - { - public long Id { get; private set; } - - public void Init(NavObject navObject) - { - Id = navObject.Id; - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Teams[Id].GetMembers()); - } - - public class NavObject - { - public long Id { get; set; } - } - } -} \ No newline at end of file diff --git a/CodeHub.Core/ViewModels/User/UserFollowersViewModel.cs b/CodeHub.Core/ViewModels/User/UserFollowersViewModel.cs deleted file mode 100755 index e035bc34..00000000 --- a/CodeHub.Core/ViewModels/User/UserFollowersViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.User -{ - public class UserFollowersViewModel : BaseUserCollectionViewModel - { - public string Name { get; private set; } - - public void Init(NavObject navObject) - { - Name = navObject.Username; - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Users[Name].GetFollowers()); - } - - public class NavObject - { - public string Username { get; set; } - } - } -} - diff --git a/CodeHub.Core/ViewModels/User/UserFollowingsViewModel.cs b/CodeHub.Core/ViewModels/User/UserFollowingsViewModel.cs deleted file mode 100755 index f9914915..00000000 --- a/CodeHub.Core/ViewModels/User/UserFollowingsViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; - -namespace CodeHub.Core.ViewModels.User -{ - public class UserFollowingsViewModel : BaseUserCollectionViewModel - { - public string Name { get; private set; } - - public void Init(NavObject navObject) - { - Name = navObject.Name; - } - - protected override Task Load() - { - return Users.SimpleCollectionLoad(this.GetApplication().Client.Users[Name].GetFollowing()); - } - - public class NavObject - { - public string Name { get; set; } - } - } -} \ No newline at end of file diff --git a/CodeHub.Core/ViewModels/Users/UserItemViewModel.cs b/CodeHub.Core/ViewModels/Users/UserItemViewModel.cs new file mode 100644 index 00000000..c25d54b1 --- /dev/null +++ b/CodeHub.Core/ViewModels/Users/UserItemViewModel.cs @@ -0,0 +1,27 @@ +using ReactiveUI; +using System; +using CodeHub.Core.Utilities; +using System.Reactive; + +namespace CodeHub.Core.ViewModels.Users +{ + public class UserItemViewModel : ReactiveObject, ICanGoToViewModel + { + public string Login => User.Login; + + public string Name => User.Name; + + public GitHubAvatar Avatar => new GitHubAvatar(User.AvatarUrl); + + public ReactiveCommand GoToCommand { get; } + + public Octokit.User User { get; } + + internal UserItemViewModel(Octokit.User user, Action gotoAction) + { + User = user; + GoToCommand = ReactiveCommand.Create(() => gotoAction?.Invoke(this)); + } + } +} + diff --git a/CodeHub.Core/ViewModels/User/UserViewModel.cs b/CodeHub.Core/ViewModels/Users/UserViewModel.cs similarity index 51% rename from CodeHub.Core/ViewModels/User/UserViewModel.cs rename to CodeHub.Core/ViewModels/Users/UserViewModel.cs index 6c8facc6..ae3932f6 100755 --- a/CodeHub.Core/ViewModels/User/UserViewModel.cs +++ b/CodeHub.Core/ViewModels/Users/UserViewModel.cs @@ -1,24 +1,24 @@ using System.Threading.Tasks; using System.Windows.Input; +using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Events; -using CodeHub.Core.ViewModels.Gists; using CodeHub.Core.ViewModels.Organizations; -using GitHubSharp.Models; -using CodeHub.Core.ViewModels; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels.Repositories; +using Splat; namespace CodeHub.Core.ViewModels.User { public class UserViewModel : LoadableViewModel { + private readonly IApplicationService _applicationService = Locator.Current.GetService(); + public string Username { get; private set; } - private UserModel _user; - public UserModel User + private Octokit.User _user; + public Octokit.User User { get { return _user; } - private set { this.RaiseAndSetIfChanged(ref _user, value); } + set { this.RaiseAndSetIfChanged(ref _user, value); } } private bool _isFollowing; @@ -36,16 +36,6 @@ public bool IsLoggedInUser } } - public ICommand GoToFollowersCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserFollowersViewModel.NavObject { Username = Username })); } - } - - public ICommand GoToFollowingCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserFollowingsViewModel.NavObject { Name = Username })); } - } - public ICommand GoToEventsCommand { get { return new MvxCommand(() => ShowViewModel(new UserEventsViewModel.NavObject { Username = Username })); } @@ -56,19 +46,9 @@ public ICommand GoToOrganizationsCommand get { return new MvxCommand(() => ShowViewModel(new OrganizationsViewModel.NavObject { Username = Username })); } } - public ICommand GoToRepositoriesCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserRepositoriesViewModel.NavObject { Username = Username })); } - } - - public ICommand GoToGistsCommand - { - get { return new MvxCommand(() => ShowViewModel(new UserGistsViewModel.NavObject { Username = Username })); } - } - public ICommand ToggleFollowingCommand { - get { return new MvxCommand(() => ToggleFollowing()); } + get { return new MvxCommand(() => ToggleFollowing().ToBackground()); } } private async Task ToggleFollowing() @@ -76,9 +56,9 @@ private async Task ToggleFollowing() try { if (IsFollowing) - await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.AuthenticatedUser.Unfollow(Username)); + await _applicationService.GitHubClient.User.Followers.Unfollow(Username); else - await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.AuthenticatedUser.Follow(Username)); + await _applicationService.GitHubClient.User.Followers.Follow(Username); IsFollowing = !IsFollowing; } catch @@ -92,10 +72,15 @@ public void Init(NavObject navObject) Title = Username = navObject.Username; } - protected override Task Load() + protected override async Task Load() { - this.RequestModel(this.GetApplication().Client.AuthenticatedUser.IsFollowing(Username), x => IsFollowing = x.Data).FireAndForget(); - return this.RequestModel(this.GetApplication().Client.Users[Username].Get(), response => User = response.Data); + _applicationService.GitHubClient.User.Followers + .IsFollowingForCurrent(Username).ToBackground(x => IsFollowing = x); + + if (User != null) + _applicationService.GitHubClient.User.Get(Username).ToBackground(x => User = x); + else + User = await _applicationService.GitHubClient.User.Get(Username); } public class NavObject diff --git a/CodeHub.Core/ViewModels/Users/UsersViewModel.cs b/CodeHub.Core/ViewModels/Users/UsersViewModel.cs new file mode 100644 index 00000000..0e9a12f1 --- /dev/null +++ b/CodeHub.Core/ViewModels/Users/UsersViewModel.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using CodeHub.Core.Services; +using Octokit; +using ReactiveUI; +using Splat; + +namespace CodeHub.Core.ViewModels.Users +{ + public class UsersViewModel : ReactiveObject + { + private readonly IApplicationService _applicationService; + private readonly IAlertDialogService _dialogService; + private readonly ReactiveList _internalItems + = new ReactiveList(resetChangeThreshold: double.MaxValue); + + public ReactiveCommand LoadCommand { get; } + + public ReactiveCommand LoadMoreCommand { get; } + + public IReadOnlyReactiveList Items { get; private set; } + + public ReactiveCommand ItemSelected { get; } + + private ObservableAsPropertyHelper _hasMore; + public bool HasMore => _hasMore.Value; + + private Uri _nextPage; + private Uri NextPage + { + get { return _nextPage; } + set { this.RaiseAndSetIfChanged(ref _nextPage, value); } + } + + private string _searchText; + public string SearchText + { + get { return _searchText; } + set { this.RaiseAndSetIfChanged(ref _searchText, value); } + } + + private readonly ObservableAsPropertyHelper _isEmpty; + public bool IsEmpty => _isEmpty.Value; + + public static UsersViewModel CreateWatchersViewModel(string owner, string name) + => new UsersViewModel(ApiUrls.Watchers(owner, name)); + + public static UsersViewModel CreateFollowersViewModel(string user) + => new UsersViewModel(ApiUrls.Followers(user)); + + public static UsersViewModel CreateFollowingViewModel(string user) + => new UsersViewModel(ApiUrls.Following(user)); + + public static UsersViewModel CreateTeamMembersViewModel(int id) + => new UsersViewModel(ApiUrls.TeamMembers(id)); + + public static UsersViewModel CreateOrgMembersViewModel(string org) + => new UsersViewModel(ApiUrls.Members(org)); + + public static UsersViewModel CreateStargazersViewModel(string owner, string name) + => new UsersViewModel(ApiUrls.Stargazers(owner, name)); + + public static UsersViewModel CreateCollaboratorsViewModel(string owner, string name) + => new UsersViewModel(ApiUrls.RepoCollaborators(owner, name)); + + public UsersViewModel( + Uri uri, + IApplicationService applicationService = null, + IAlertDialogService dialogService = null) + { + _applicationService = applicationService ?? Locator.Current.GetService(); + _dialogService = dialogService ?? Locator.Current.GetService(); + + NextPage = uri; + + var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; + + ItemSelected = ReactiveCommand.Create(x => x); + + var userItems = _internalItems.CreateDerivedCollection( + x => new UserItemViewModel(x, GoToUser)); + + var searchUpdated = this.WhenAnyValue(x => x.SearchText) + .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); + + Items = userItems + .CreateDerivedCollection( + x => x, + x => x.Login.ContainsKeyword(SearchText), + signalReset: searchUpdated); + + LoadCommand = ReactiveCommand.CreateFromTask(async t => + { + _internalItems.Clear(); + var parameters = new Dictionary(); + parameters["per_page"] = 100.ToString(); + var items = await RetrieveItems(uri, parameters); + _internalItems.AddRange(items); + return items.Count > 0; + }); + + var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); + LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => + { + var items = await RetrieveItems(NextPage); + _internalItems.AddRange(items); + return items.Count > 0; + }, canLoadMore); + + LoadCommand.Select(_ => _internalItems.Count == 0) + .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); + + LoadCommand.ThrownExceptions.Subscribe(LoadingError); + + LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); + + _hasMore = this.WhenAnyValue(x => x.NextPage) + .Select(x => x != null) + .ToProperty(this, x => x.HasMore); + } + + private void LoadingError(Exception err) + { + _dialogService.Alert("Error Loading", err.Message).ToBackground(); + } + + private void GoToUser(UserItemViewModel item) + { + ItemSelected.ExecuteNow(item); + } + + private async Task> RetrieveItems( + Uri repositoriesUri, IDictionary parameters = null) + { + try + { + var connection = _applicationService.GitHubClient.Connection; + var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); + NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") + ? ret.HttpResponse.ApiInfo.Links["next"] + : null; + return ret.Body; + } + catch + { + NextPage = null; + throw; + } + } + } +} diff --git a/CodeHub.Core/ViewModels/ViewModelExtensions.cs b/CodeHub.Core/ViewModels/ViewModelExtensions.cs index 1fcad412..617de513 100755 --- a/CodeHub.Core/ViewModels/ViewModelExtensions.cs +++ b/CodeHub.Core/ViewModels/ViewModelExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using MvvmCross.Core.ViewModels; -using CodeHub.Core.ViewModels; using GitHubSharp; using System.Collections.Generic; using CodeHub.Core.Services; diff --git a/CodeHub.Core/packages.config b/CodeHub.Core/packages.config index 2992bf36..5fe02aba 100644 --- a/CodeHub.Core/packages.config +++ b/CodeHub.Core/packages.config @@ -1,18 +1,41 @@  - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CodeHub.iOS.sln b/CodeHub.iOS.sln index a17be7f5..58a12150 100644 --- a/CodeHub.iOS.sln +++ b/CodeHub.iOS.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeHub.iOS", "CodeHub.iOS\CodeHub.iOS.csproj", "{B061316A-F386-4FE2-93B7-555584234FF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeHub.Core.iOS", "CodeHub.Core\CodeHub.Core.iOS.csproj", "{B7970173-9022-466B-B57A-7AB1E1F3145F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeHub.Core", "CodeHub.Core\CodeHub.Core.csproj", "{B7970173-9022-466B-B57A-7AB1E1F3145F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeHub", "CodeHub\CodeHub.csproj", "{B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -108,21 +110,60 @@ Global {B7970173-9022-466B-B57A-7AB1E1F3145F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {B7970173-9022-466B-B57A-7AB1E1F3145F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B7970173-9022-466B-B57A-7AB1E1F3145F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|Any CPU.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|Any CPU.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|iPhone.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|iPhone.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AdHoc|Mixed Platforms.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Ad-Hoc|Mixed Platforms.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|iPhone.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.AppStore|Mixed Platforms.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|iPhone.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|Any CPU.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|iPhone.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|iPhone.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7}.Release|Mixed Platforms.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.DotNetNamingPolicy = $1 $1.DirectoryNamespaceAssociation = PrefixedHierarchical - $1.ResourceNamePolicy = FileFormatDefault $0.TextStylePolicy = $2 - $2.FileWidth = 120 - $2.TabsToSpaces = False - $2.inheritsSet = VisualStudio + $2.inheritsSet = null $2.inheritsScope = text/plain + $2.scope = application/javascript + $2.FileWidth = 120 $0.TextStylePolicy = $3 - $3.inheritsSet = VisualStudio - $3.inheritsScope = text/plain $3.scope = text/x-csharp + $3.FileWidth = 160 + $3.TabsToSpaces = True $0.CSharpFormattingPolicy = $4 $4.IndentSwitchBody = True $4.PropertyBraceStyle = NextLine @@ -146,30 +187,41 @@ Global $4.BeforeDelegateDeclarationParentheses = False $4.NewParentheses = False $4.SpacesBeforeBrackets = False - $4.inheritsSet = Mono - $4.inheritsScope = text/x-csharp $4.scope = text/x-csharp $0.TextStylePolicy = $5 - $5.FileWidth = 120 - $5.inheritsSet = VisualStudio - $5.inheritsScope = text/plain $5.scope = text/plain + $5.TabsToSpaces = True $0.TextStylePolicy = $6 $6.inheritsSet = null $6.scope = application/xml $0.XmlFormattingPolicy = $7 - $7.inheritsSet = Mono - $7.inheritsScope = application/xml $7.scope = application/xml $0.TextStylePolicy = $8 $8.inheritsSet = null $8.scope = text/html $0.TextStylePolicy = $9 $9.inheritsSet = null - $9.scope = application/javascript + $9.scope = text/css $0.TextStylePolicy = $10 - $10.inheritsSet = null - $10.scope = text/css + $10.scope = text/x-cshtml + $10.FileWidth = 80 + $10.TabsToSpaces = True + $10.TabWidth = 2 + $10.IndentWidth = 2 + $0.StandardHeader = $11 + $0.NameConventionPolicy = $12 + $12.Rules = $13 + $13.NamingRule = $14 + $14.Name = Type Parameters + $14.AffectedEntity = TypeParameter + $14.VisibilityMask = VisibilityMask + $14.NamingStyle = PascalCase + $14.IncludeInstanceMembers = True + $14.IncludeStaticEntities = True + $14.RequiredPrefixes = $15 + $15.String = T + $14.RequiredSuffixes = $16 + $16.String = Exception EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CodeHub.iOS/AkavacheSqliteLinkerOverride.cs b/CodeHub.iOS/AkavacheSqliteLinkerOverride.cs new file mode 100644 index 00000000..0e7a2a7a --- /dev/null +++ b/CodeHub.iOS/AkavacheSqliteLinkerOverride.cs @@ -0,0 +1,21 @@ +using System; +using Akavache.Sqlite3; + +// Note: This class file is *required* for iOS to work correctly, and is +// also a good idea for Android if you enable "Link All Assemblies". +namespace CodeHub.iOS +{ + [Preserve] + public static class LinkerPreserve + { + static LinkerPreserve() + { + throw new Exception(typeof(SQLitePersistentBlobCache).FullName); + } + } + + + public class PreserveAttribute : Attribute + { + } +} diff --git a/CodeHub.iOS/AppDelegate.cs b/CodeHub.iOS/AppDelegate.cs index 3c4fdc80..4a6a3875 100644 --- a/CodeHub.iOS/AppDelegate.cs +++ b/CodeHub.iOS/AppDelegate.cs @@ -1,11 +1,5 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Defines the AppDelegate type. -// -// -------------------------------------------------------------------------------------------------------------------- - using System.Collections.Generic; -using System; +using System; using MvvmCross.Core.ViewModels; using Foundation; using UIKit; @@ -23,14 +17,13 @@ using CodeHub.Core.Messages; using CodeHub.iOS.XCallback; using System.Reactive.Linq; +using Splat; +using Microsoft.AppCenter; +using Microsoft.AppCenter.Analytics; +using Microsoft.AppCenter.Crashes; namespace CodeHub.iOS { - /// - /// The UIApplicationDelegate for the application. This class is responsible for launching the - /// User Interface of the application, as well as listening (and optionally responding) to - /// application events from iOS. - /// [Register("AppDelegate")] public class AppDelegate : MvxApplicationDelegate { @@ -40,6 +33,8 @@ public class AppDelegate : MvxApplicationDelegate public IosViewPresenter Presenter { get; private set; } + public static AppDelegate Instance => UIApplication.SharedApplication.Delegate as AppDelegate; + /// /// This is the main entry point of the application. /// @@ -57,38 +52,60 @@ public static void Main(string[] args) /// True or false. public override bool FinishedLaunching(UIApplication app, NSDictionary options) { + AppCenter.Start("eef367be-437c-4c67-abe0-79779b3b8392", typeof(Analytics), typeof(Crashes)); + Window = new UIWindow(UIScreen.MainScreen.Bounds); Presenter = new IosViewPresenter(this.Window); var setup = new Setup(this, Presenter); setup.Initialize(); - // Initialize the error service! - var errorService = Mvx.Resolve(); - errorService.Init(); - var culture = new System.Globalization.CultureInfo("en"); + System.Threading.Thread.CurrentThread.CurrentCulture = culture; System.Threading.Thread.CurrentThread.CurrentUICulture = culture; + System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture; System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture; // Setup theme UIApplication.SharedApplication.SetStatusBarStyle(UIStatusBarStyle.LightContent, true); Theme.Setup(); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); + + Locator.CurrentMutable.RegisterLazySingleton( + () => new ImgurService(), typeof(IImgurService)); + var features = Mvx.Resolve(); - var defaultValueService = Mvx.Resolve(); var purchaseService = Mvx.Resolve(); - purchaseService.ThrownExceptions.Subscribe(ex => { - AlertDialogService.ShowAlert("Error Purchasing", ex.Message); - errorService.Log(ex); + + purchaseService.ThrownExceptions.Subscribe(ex => + { + var error = new Core.UserError("Error Purchasing", ex.Message); + Core.Interactions.Errors.Handle(error).Subscribe(); }); - #if DEBUG + Core.Interactions.Errors.RegisterHandler(interaction => + { + var error = interaction.Input; + AlertDialogService.ShowAlert(error.Title, error.Message); + interaction.SetOutput(System.Reactive.Unit.Default); + }); + +#if DEBUG features.ActivateProDirect(); - #endif +#endif + + //options = new NSDictionary (UIApplication.LaunchOptionsRemoteNotificationKey, + //new NSDictionary ("r", "octokit/octokit.net", "i", "739", "u", "thedillonb")); -// options = new NSDictionary (UIApplication.LaunchOptionsRemoteNotificationKey, -// new NSDictionary ("r", "octokit/octokit.net", "i", "739", "u", "thedillonb")); -// if (options != null) { if (options.ContainsKey(UIApplication.LaunchOptionsRemoteNotificationKey)) @@ -103,11 +120,10 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary options) // Set the client constructor GitHubSharp.Client.ClientConstructor = () => new HttpClient(new CustomHttpMessageHandler()); - bool hasSeenWelcome; - if (!defaultValueService.TryGet("HAS_SEEN_WELCOME_INTRO", out hasSeenWelcome) || !hasSeenWelcome) + if (!Core.Settings.HasSeenWelcome) { - defaultValueService.Set("HAS_SEEN_WELCOME_INTRO", true); - var welcomeViewController = new CodeHub.iOS.ViewControllers.Walkthrough.WelcomePageViewController(); + Core.Settings.HasSeenWelcome = true; + var welcomeViewController = new ViewControllers.Walkthrough.WelcomePageViewController(); welcomeViewController.WantsToDimiss += GoToStartupView; TransitionToViewController(welcomeViewController); } @@ -133,14 +149,16 @@ public void RegisterUserForNotifications() private void GoToStartupView() { - var startup = new CodeHub.iOS.ViewControllers.Application.StartupViewController(); - TransitionToViewController(startup); - MessageBus.Current.Listen() + TransitionToViewController(new ViewControllers.Application.StartupViewController()); + + MessageBus + .Current.Listen() .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => startup.DismissViewController(true, null)); + .Select(_ => new ViewControllers.Application.StartupViewController()) + .Subscribe(TransitionToViewController); } - private void TransitionToViewController(UIViewController viewController) + public void TransitionToViewController(UIViewController viewController) { UIView.Transition(Window, 0.35, UIViewAnimationOptions.TransitionCrossDissolve, () => Window.RootViewController = viewController, null); @@ -174,6 +192,7 @@ private void HandleNotification(NSDictionary data, bool fromBootup) { var viewDispatcher = Mvx.Resolve(); var appService = Mvx.Resolve(); + var accountsService = Mvx.Resolve(); var repoId = RepositoryIdentifier.FromFullName(data["r"].ToString()); var parameters = new Dictionary() {{"Username", repoId?.Owner}, {"Repository", repoId?.Name}}; @@ -200,16 +219,22 @@ private void HandleNotification(NSDictionary data, bool fromBootup) } request.ParameterValues = parameters; + request.PresentationValues = new Dictionary + { + { Core.PresentationValues.SlideoutRootPresentation, string.Empty } + }; var username = data["u"].ToString(); if (appService.Account == null || !appService.Account.Username.Equals(username)) { - var user = appService.Accounts.FirstOrDefault(x => x.Username.Equals(username)); + var accounts = accountsService.GetAccounts().Result.ToList(); + + var user = accounts.FirstOrDefault(x => x.Username.Equals(username)); if (user != null) { appService.DeactivateUser(); - appService.Accounts.SetDefault(user); + accountsService.SetActiveAccount(user).Wait(); } } @@ -217,8 +242,7 @@ private void HandleNotification(NSDictionary data, bool fromBootup) if (appService.Account == null && !fromBootup) { - var startupViewModelRequest = MvxViewModelRequest.GetDefaultRequest(); - viewDispatcher.ShowViewModel(startupViewModelRequest); + MessageBus.Current.SendMessage(new LogoutMessage()); } } catch (Exception e) @@ -237,11 +261,12 @@ public override void RegisteredForRemoteNotifications(UIApplication application, DeviceToken = deviceToken.Description.Trim('<', '>').Replace(" ", ""); var app = Mvx.Resolve(); + var accounts = Mvx.Resolve(); if (app.Account != null && !app.Account.IsPushNotificationsEnabled.HasValue) { Mvx.Resolve().Register().ToBackground(); app.Account.IsPushNotificationsEnabled = true; - app.Accounts.Update(app.Account); + accounts.Save(app.Account); } } diff --git a/CodeHub.iOS/Bootstrap/MessengerPluginBootstrap.cs b/CodeHub.iOS/Bootstrap/MessengerPluginBootstrap.cs deleted file mode 100644 index 557a3970..00000000 --- a/CodeHub.iOS/Bootstrap/MessengerPluginBootstrap.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MvvmCross.Platform.Plugins; - -namespace CodeHub.iOS.Bootstrap -{ - public class MessengerPluginBootstrap - : MvxPluginBootstrapAction - { - } -} \ No newline at end of file diff --git a/CodeHub.iOS/CodeHub.iOS.csproj b/CodeHub.iOS/CodeHub.iOS.csproj index be3ee5c4..67e3ec1c 100644 --- a/CodeHub.iOS/CodeHub.iOS.csproj +++ b/CodeHub.iOS/CodeHub.iOS.csproj @@ -22,34 +22,17 @@ prompt 4 false - None + SdkOnly true - i386 + x86_64 Entitlements.plist iPhone Developer - true true - true NSUrlSessionHandler - AppleTLS + Default false true - - none - true - bin\iPhoneSimulator\Release - prompt - 4 - false - true - true - - - ARMv7 - Entitlements.plist - iPhone Developer - true full @@ -61,28 +44,13 @@ false iPhone Developer true - ARMv7 + ARM64 true true Entitlements.plist - true - - - none - true - bin\iPhone\Release - prompt - 4 - false - iPhone Developer - true - ARMv7, ARM64 - Entitlements.plist - - - true + none @@ -98,12 +66,10 @@ ARMv7, ARM64 Automatic:AdHoc Entitlements.plist - true - true --linkskip=CodeHubiOS --linkskip=CodeHub.Core --linkskip=GitHubSharp - Full + SdkOnly NSUrlSessionHandler - AppleTLS + Default none @@ -112,17 +78,16 @@ prompt 4 False - iPhone Distribution - Automatic:AppStore + iPhone Distribution: Dillon Buchanan (T39PW4C23Z) + ARMv7, ARM64 true Entitlements.plist - true - true --linkskip=CodeHubiOS --linkskip=CodeHub.Core --linkskip=GitHubSharp - Full + SdkOnly + NSUrlSessionHandler @@ -130,17 +95,8 @@ - - - - - - - - - @@ -148,45 +104,23 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -198,20 +132,17 @@ - - - + - @@ -240,31 +171,6 @@ - - - - - - RazorTemplatePreprocessor - UpgradeDetailsRazorView.cs - - - UpgradeDetailsRazorView.cshtml - - - RazorTemplatePreprocessor - SyntaxHighlighterView.cs - - - SyntaxHighlighterView.cshtml - - - RazorTemplatePreprocessor - MarkdownView.cs - - - MarkdownView.cshtml - @@ -288,7 +194,6 @@ - @@ -305,12 +210,10 @@ - - @@ -325,10 +228,9 @@ - - + @@ -341,31 +243,13 @@ - - - RazorTemplatePreprocessor - CommentsView.cs - - - CommentsView.cshtml - - - - - - - - - - - @@ -373,19 +257,74 @@ - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + FeedbackCellView.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - lib\MonoTouch.TTTAttributedLabel.dll - ..\packages\MonoTouch.SlideoutNavigation.1.0.1\lib\Xamarin.iOS10\MonoTouch.Slideout.dll @@ -408,53 +347,117 @@ ..\packages\Splat.1.6.2\lib\Xamarin.iOS10\Splat.dll - - ..\packages\reactiveui-core.6.5.0\lib\Xamarin.iOS10\ReactiveUI.dll + + ..\packages\GitHubClient.1.0.15\lib\portable45-net45+win8+wpa81\GitHubSharp.dll - - ..\packages\Humanizer.Core.2.0.1\lib\portable-win+net45+wp8+wpa81\Humanizer.dll + + lib\Xamarin.TTTAttributedLabel.dll - - ..\packages\Newtonsoft.Json.8.0.3\lib\portable-net40+sl5+wp80+win8+wpa81\Newtonsoft.Json.dll + + ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.dll - - ..\packages\GitHubClient.1.0.14.0\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\GitHubSharp.dll + + ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll - - ..\packages\BTProgressHUD.1.2.0.2\lib\Xamarin.iOS10\BTProgressHUD.dll + + ..\packages\akavache.core.5.0.0\lib\Xamarin.iOS10\Akavache.dll - - ..\packages\MvvmCross.Platform.4.1.4\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll + + ..\packages\akavache.sqlite3.5.0.0\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll - ..\packages\MvvmCross.Platform.4.1.4\lib\Xamarin.iOS10\MvvmCross.Platform.dll + ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.dll - - ..\packages\MvvmCross.Plugin.Messenger.4.1.4\lib\Xamarin.iOS10\MvvmCross.Plugins.Messenger.dll - - - ..\packages\MvvmCross.Core.4.1.4\lib\Xamarin.iOS10\MvvmCross.iOS.dll + + ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll - ..\packages\MvvmCross.Core.4.1.4\lib\Xamarin.iOS10\MvvmCross.Core.dll + ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.Core.dll - - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll + + ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.iOS.dll - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Binding.dll + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.dll + + + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll - ..\packages\MvvmCross.Binding.4.1.4\lib\Xamarin.iOS10\MvvmCross.Localization.dll + ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Localization.dll + + + ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll + + + ..\packages\reactiveui-core.7.4.0\lib\Xamarin.iOS10\ReactiveUI.dll + + + + ..\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll + + + ..\packages\BTProgressHUD.1.2.0.6\lib\Xamarin.iOS10\BTProgressHUD.dll + + + ..\packages\SQLitePCLRaw.core.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.core.dll + + + ..\packages\SQLitePCLRaw.lib.e_sqlite3.ios_unified.static.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.lib.e_sqlite3.dll + + + ..\packages\SQLitePCLRaw.provider.internal.ios_unified.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.provider.internal.dll + + + ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_e_sqlite3.dll + + + ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll + + + ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll + + + ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.dll + + + + ..\packages\Octokit.0.29.0\lib\netstandard1.1\Octokit.dll + + + ..\packages\Microsoft.AppCenter.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.dll + + + ..\packages\Microsoft.AppCenter.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.iOS.Bindings.dll + + + ..\packages\Microsoft.AppCenter.Analytics.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Analytics.dll + + + ..\packages\Microsoft.AppCenter.Analytics.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Analytics.iOS.Bindings.dll + + + ..\packages\Microsoft.AppCenter.Crashes.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Crashes.dll + + + ..\packages\Microsoft.AppCenter.Crashes.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Crashes.iOS.Bindings.dll + + + ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.Abstractions.dll + + + ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.dll + + + ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.Abstractions.dll + + + ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.dll - - - - @@ -463,37 +466,28 @@ - - - - - - - - - - - - - - - - - - - + + Resources\octicons.ttf + + + + + + + + @@ -519,10 +513,13 @@ + + + @@ -547,14 +544,17 @@ + + + @@ -566,8 +566,11 @@ + + + @@ -577,9 +580,7 @@ - - @@ -588,14 +589,18 @@ + + + + + + + + + + - - - {B7970173-9022-466B-B57A-7AB1E1F3145F} - CodeHub.Core.iOS - - @@ -615,11 +620,28 @@ + + + + + + + + + + + {B7970173-9022-466B-B57A-7AB1E1F3145F} + CodeHub.Core + + + {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7} + CodeHub + - - 3.7.3.4 - False - + + + + diff --git a/CodeHub.iOS/DialogElements/ChangesetElement.cs b/CodeHub.iOS/DialogElements/ChangesetElement.cs index 405114c3..d1e8ec59 100755 --- a/CodeHub.iOS/DialogElements/ChangesetElement.cs +++ b/CodeHub.iOS/DialogElements/ChangesetElement.cs @@ -53,8 +53,9 @@ public ChangesetCell(string key) public override void LayoutSubviews() { base.LayoutSubviews(); - var addRemoveX = ContentView.Frame.Width - 90f; - AddRemoveView.Frame = new CoreGraphics.CGRect(addRemoveX, 12, 80f, 18f); + var addRemoveX = ContentView.Frame.Width - 90f; + var addRemoveY = (ContentView.Frame.Height / 2) - 9f; + AddRemoveView.Frame = new CoreGraphics.CGRect(addRemoveX, addRemoveY, 80f, 18f); var textFrame = TextLabel.Frame; textFrame.Width = addRemoveX - textFrame.X - 5f; diff --git a/CodeHub.iOS/DialogElements/Element.cs b/CodeHub.iOS/DialogElements/Element.cs index a5cba5ec..fa4414ca 100644 --- a/CodeHub.iOS/DialogElements/Element.cs +++ b/CodeHub.iOS/DialogElements/Element.cs @@ -7,14 +7,8 @@ namespace CodeHub.iOS.DialogElements public abstract class Element { private WeakReference
_weakSection; - public Section Section - { - get - { - Section sec = null; - return (_weakSection?.TryGetTarget(out sec) ?? false) ? sec : null; - } - } + + public Section Section => _weakSection?.Get(); internal void SetSection(Section section) { diff --git a/CodeHub.iOS/DialogElements/GistElement.cs b/CodeHub.iOS/DialogElements/GistElement.cs deleted file mode 100644 index fde6189b..00000000 --- a/CodeHub.iOS/DialogElements/GistElement.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using UIKit; -using Foundation; -using CodeHub.iOS.TableViewCells; -using GitHubSharp.Models; -using System.Linq; -using CodeHub.Core.Utilities; - -namespace CodeHub.iOS.DialogElements -{ - public class GistElement : Element - { - private readonly Action _action; - private readonly GistModel _gist; - private readonly string _title; - - public GistElement(GistModel gist, Action action) - { - _gist = gist; - _action = action; - _title = _gist.Files?.Select(x => x.Key).FirstOrDefault() ?? "Gist #" + _gist.Id; - } - - public override UITableViewCell GetCell (UITableView tv) - { - var c = tv.DequeueReusableCell(GistCellView.Key) as GistCellView ?? GistCellView.Create(); - c.Set(_title, _gist.Description, _gist.CreatedAt, new GitHubAvatar(_gist.Owner?.AvatarUrl)); - return c; - } - - public override bool Matches(string text) - { - return _title.ContainsKeyword(text) || _gist.Description.ContainsKeyword(text); - } - - public override void Selected(UITableView tableView, NSIndexPath path) - { - base.Selected(tableView, path); - _action?.Invoke(); - } - } -} - diff --git a/CodeHub.iOS/DialogElements/HtmlElement.cs b/CodeHub.iOS/DialogElements/HtmlElement.cs index 9bf4d7e2..809a4a1e 100644 --- a/CodeHub.iOS/DialogElements/HtmlElement.cs +++ b/CodeHub.iOS/DialogElements/HtmlElement.cs @@ -47,6 +47,11 @@ public void SetValue(string value) HasValue = value != null; } + public void SetLayout() + { + WebView.SetNeedsLayout(); + } + private async Task GetSize() { if (HasValue) diff --git a/CodeHub.iOS/DialogElements/NewsFeedElement.cs b/CodeHub.iOS/DialogElements/NewsFeedElement.cs old mode 100755 new mode 100644 index c8d55295..b98f00db --- a/CodeHub.iOS/DialogElements/NewsFeedElement.cs +++ b/CodeHub.iOS/DialogElements/NewsFeedElement.cs @@ -1,116 +1,123 @@ -using System; -using System.Collections.Generic; -using CoreGraphics; -using System.Linq; -using Foundation; -using UIKit; -using CodeHub.iOS.TableViewCells; -using Humanizer; - -namespace CodeHub.iOS.DialogElements -{ - public class NewsFeedElement : Element - { - private readonly string _time; - private readonly Uri _imageUri; - private readonly UIImage _actionImage; - private readonly Action _tapped; - private readonly bool _multilined; - - private readonly NSMutableAttributedString _attributedHeader; - private readonly NSMutableAttributedString _attributedBody; - private readonly List _headerLinks; - private readonly List _bodyLinks; - - public static UIColor LinkColor = Theme.CurrentTheme.MainTitleColor; - - private UIImage LittleImage { get; set; } - - public Action WebLinkClicked; - - public class TextBlock - { - public string Value; - public Action Tapped; - - public TextBlock() - { - } - - public TextBlock(string value) - { - Value = value; - } - - public TextBlock(string value, Action tapped = null) - : this (value) - { - Tapped = tapped; - } - } - - public NewsFeedElement(string imageUrl, DateTimeOffset time, IEnumerable headerBlocks, IEnumerable bodyBlocks, UIImage littleImage, Action tapped, bool multilined) - { - Uri.TryCreate(imageUrl, UriKind.Absolute, out _imageUri); - _time = time.Humanize(); - _actionImage = littleImage; - _tapped = tapped; - _multilined = multilined; - - var header = CreateAttributedStringFromBlocks(UIFont.PreferredBody, Theme.CurrentTheme.MainTextColor, headerBlocks); - _attributedHeader = header.Item1; - _headerLinks = header.Item2; - - var body = CreateAttributedStringFromBlocks(UIFont.PreferredSubheadline, Theme.CurrentTheme.MainSubtitleColor, bodyBlocks); - _attributedBody = body.Item1; - _bodyLinks = body.Item2; - } - - private static Tuple> CreateAttributedStringFromBlocks(UIFont font, UIColor primaryColor, IEnumerable blocks) - { - var attributedString = new NSMutableAttributedString(); - var links = new List(); - - nint lengthCounter = 0; - int i = 0; - - foreach (var b in blocks) - { - UIColor color = null; - if (b.Tapped != null) - color = LinkColor; - - color = color ?? primaryColor; - - var ctFont = new CoreText.CTFont(font.Name, font.PointSize); - var str = new NSAttributedString(b.Value, new CoreText.CTStringAttributes() { ForegroundColor = color.CGColor, Font = ctFont }); - attributedString.Append(str); - var strLength = str.Length; - - if (b.Tapped != null) - { - var weakTapped = new WeakReference(b.Tapped); - links.Add(new NewsCellView.Link { Range = new NSRange(lengthCounter, strLength), Callback = () => weakTapped.Get()?.Invoke(), Id = i++ }); - } - - lengthCounter += strLength; - } - - return new Tuple>(attributedString, links); - } - - - public override UITableViewCell GetCell (UITableView tv) - { - var cell = tv.DequeueReusableCell(NewsCellView.Key) as NewsCellView ?? NewsCellView.Create(); - cell.Set(_imageUri, _time, _actionImage, _attributedHeader, _attributedBody, _headerLinks, _bodyLinks, WebLinkClicked, _multilined); - return cell; - } - - public override void Selected(UITableView tableView, NSIndexPath path) - { - base.Selected(tableView, path); - _tapped?.Invoke(); - } - } +using System; +using System.Collections.Generic; +using CoreGraphics; +using Foundation; +using UIKit; +using CodeHub.iOS.TableViewCells; +using Humanizer; + +namespace CodeHub.iOS.DialogElements +{ + public class NewsFeedElement : Element + { + private readonly string _time; + private readonly Uri _imageUri; + private readonly UIImage _actionImage; + private readonly Action _tapped; + private readonly bool _multilined; + + private readonly NSMutableAttributedString _attributedHeader; + private readonly NSMutableAttributedString _attributedBody; + private readonly List _headerLinks; + private readonly List _bodyLinks; + + public static UIColor LinkColor = Theme.CurrentTheme.MainTitleColor; + + public Action WebLinkClicked; + + public class TextBlock + { + public string Value; + public Action Tapped; + + public TextBlock() + { + } + + public TextBlock(string value) + { + Value = value; + } + + public TextBlock(string value, Action tapped = null) + : this (value) + { + Tapped = tapped; + } + } + + public NewsFeedElement(string imageUrl, DateTimeOffset time, IEnumerable headerBlocks, IEnumerable bodyBlocks, UIImage littleImage, Action tapped, bool multilined) + { + Uri.TryCreate(imageUrl, UriKind.Absolute, out _imageUri); + _time = time.Humanize(); + _actionImage = littleImage; + _tapped = tapped; + _multilined = multilined; + + var header = CreateAttributedStringFromBlocks(UIFont.PreferredBody, Theme.CurrentTheme.MainTextColor, headerBlocks); + _attributedHeader = header.Item1; + _headerLinks = header.Item2; + + var body = CreateAttributedStringFromBlocks(UIFont.PreferredSubheadline, Theme.CurrentTheme.MainSubtitleColor, bodyBlocks); + _attributedBody = body.Item1; + _bodyLinks = body.Item2; + } + + private static Tuple> CreateAttributedStringFromBlocks(UIFont font, UIColor primaryColor, IEnumerable blocks) + { + var attributedString = new NSMutableAttributedString(); + var links = new List(); + + nint lengthCounter = 0; + int i = 0; + + CoreText.CTFont ctFont; + + try + { + ctFont = new CoreText.CTFont(font.FamilyName, font.PointSize); + } + catch + { + ctFont = CGFont.CreateWithFontName(font.Name).ToCTFont(font.PointSize); + } + + foreach (var b in blocks) + { + UIColor color = null; + if (b.Tapped != null) + color = LinkColor; + + color = color ?? primaryColor; + + var str = new NSAttributedString(b.Value, new CoreText.CTStringAttributes() { ForegroundColor = color.CGColor, Font = ctFont }); + attributedString.Append(str); + var strLength = str.Length; + + if (b.Tapped != null) + { + var weakTapped = new WeakReference(b.Tapped); + links.Add(new NewsCellView.Link { Range = new NSRange(lengthCounter, strLength), Callback = () => weakTapped.Get()?.Invoke(), Id = i++ }); + } + + lengthCounter += strLength; + } + + return new Tuple>(attributedString, links); + } + + + public override UITableViewCell GetCell (UITableView tv) + { + var cell = tv.DequeueReusableCell(NewsCellView.Key) as NewsCellView ?? NewsCellView.Create(); + cell.Set(_imageUri, _time, _actionImage, _attributedHeader, _attributedBody, _headerLinks, _bodyLinks, WebLinkClicked, _multilined); + return cell; + } + + public override void Selected(UITableView tableView, NSIndexPath path) + { + base.Selected(tableView, path); + _tapped?.Invoke(); + } + } } \ No newline at end of file diff --git a/CodeHub.iOS/DialogElements/RepositoryElement.cs b/CodeHub.iOS/DialogElements/RepositoryElement.cs deleted file mode 100755 index 18cf32e1..00000000 --- a/CodeHub.iOS/DialogElements/RepositoryElement.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Foundation; -using UIKit; -using CodeHub.iOS.TableViewCells; -using CodeHub.Core.Utilities; - -namespace CodeHub.iOS.DialogElements -{ - public class RepositoryElement : Element - { - private readonly string _name; - private readonly int _followers; - private readonly int _forks; - private readonly string _description; - private readonly string _owner; - private readonly GitHubAvatar _avatar; - - public UIColor BackgroundColor { get; set; } - - public bool ShowOwner { get; set; } - - public RepositoryElement(string name, int followers, int forks, string description, string owner, GitHubAvatar avatar) - { - _name = name; - _followers = followers; - _forks = forks; - _description = description; - _owner = owner; - _avatar = avatar; - ShowOwner = true; - } - - public event Action Tapped; - - public override UITableViewCell GetCell (UITableView tv) - { - var cell = tv.DequeueReusableCell(RepositoryCellView.Key) as RepositoryCellView ?? RepositoryCellView.Create(); - cell.Bind(_name, _followers.ToString(), _forks.ToString(), _description, ShowOwner ? _owner : null, _avatar); - return cell; - } - - public override bool Matches(string text) - { - var name = _name ?? string.Empty; - return name.IndexOf(text, StringComparison.OrdinalIgnoreCase) != -1; - } - - public override void Selected(UITableView tableView, NSIndexPath path) - { - base.Selected(tableView, path); - Tapped?.Invoke(); - } - } -} - diff --git a/CodeHub.iOS/DialogElements/RootElement.cs b/CodeHub.iOS/DialogElements/RootElement.cs index 0df4339d..985e6e65 100644 --- a/CodeHub.iOS/DialogElements/RootElement.cs +++ b/CodeHub.iOS/DialogElements/RootElement.cs @@ -48,12 +48,6 @@ internal int IndexOf (Section target) return -1; } - /// - /// Adds a new section to this RootElement - /// - /// - /// The section to add, if the root is visible, the section is inserted with no animation - /// public void Add (Section section) { if (section == null) @@ -164,33 +158,6 @@ public void Reset(params Section[] sections) Reset((IEnumerable
)sections); } -// public void Reload (Section section, UITableViewRowAnimation animation = UITableViewRowAnimation.Automatic) -// { -// if (section == null) -// throw new ArgumentNullException ("section"); -// if (section.Root == null || section.Root != this) -// throw new ArgumentException ("Section is not attached to this root"); -// -// int idx = 0; -// foreach (var sect in _sections) -// { -// if (sect == section) -// { -// try -// { -// _tableView.Get()?.BeginUpdates(); -// _tableView.Get()?.ReloadSections (new NSIndexSet ((uint) idx), animation); -// } -// finally -// { -// _tableView.Get()?.EndUpdates(); -// } -// return; -// } -// idx++; -// } -// } - public void Reload (Element element) { Reload(new [] { element }); diff --git a/CodeHub.iOS/DialogElements/SplitButtonElement.cs b/CodeHub.iOS/DialogElements/SplitButtonElement.cs index 425d92d8..964ba361 100644 --- a/CodeHub.iOS/DialogElements/SplitButtonElement.cs +++ b/CodeHub.iOS/DialogElements/SplitButtonElement.cs @@ -168,11 +168,13 @@ public SplitButton() _caption = new UILabel(); _caption.TextColor = CaptionColor; _caption.Font = CaptionFont; + _caption.TextAlignment = UITextAlignment.Center; this.Add(_caption); _text = new UILabel(); _text.TextColor = TextColor; _text.Font = TextFont; + _text.TextAlignment = UITextAlignment.Center; this.Add(_text); this.TouchUpInside += (sender, e) => Touch?.Invoke(); diff --git a/CodeHub.iOS/DialogElements/SplitViewElement.cs b/CodeHub.iOS/DialogElements/SplitViewElement.cs index f657ec94..d5309061 100644 --- a/CodeHub.iOS/DialogElements/SplitViewElement.cs +++ b/CodeHub.iOS/DialogElements/SplitViewElement.cs @@ -150,8 +150,8 @@ public override void LayoutSubviews() { base.LayoutSubviews(); - var offset = IsPad ? 24f : 18f; - var rightOffset = IsPad ? 16f : 14f; + var offset = IsPad ? 18f : 16f; + var rightOffset = IsPad ? 16f : 18f; var height = (this.Bounds.Height - 24f); _image.Frame = new CGRect(offset, 12, height, height); diff --git a/CodeHub.iOS/Diff/app.js b/CodeHub.iOS/Diff/app.js deleted file mode 100644 index 2ed40255..00000000 --- a/CodeHub.iOS/Diff/app.js +++ /dev/null @@ -1,116 +0,0 @@ -function diff(base_text, new_text) { - var base = difflib.stringAsLines(base_text); - var newtxt = difflib.stringAsLines(new_text); - var sm = new difflib.SequenceMatcher(base, newtxt); - var opcodes = sm.get_opcodes(); - var diffoutputdiv = document.body; - while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); - diffoutputdiv.appendChild(diffview.buildView({ - baseTextLines: base, - newTextLines: newtxt, - opcodes: opcodes, - baseTextName: "Base Text", - newTextName: "New Text", - contextSize: 3, - viewType: 1 - })); - - $('td').each(function(i, el) { - $(el).click(function() { - invokeNative("comment", {"line_to": $(el).parent().data('to'), "line_from": $(el).parent().data('from')}); - }); - }); -} - -function loadFileAsPatch(path) { - $.get(path, function(data) { - patch(data); - }); -} - -function escapeHtml(data) { - return $('
').text(data).html(); -} - -function patch(p) { - var $body = $('body'); - var $table = $("
"); - - function createRow(x, y, type, line, lineNum) { - $table.append("" + x + "" + y + "" + escapeHtml(line) + ""); - }; - - var lines = p.split("\n"); - var baseLine = 0; - var newLine = 0; - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (line.lastIndexOf("@@", 0) === 0) { - createRow("...", "...", "skip", line, i); - var r = /@@ -(\d+).+\+(\d+)/i; - var arr = r.exec(line); - baseLine = arr[1]; - newLine = arr[2]; - } else { - if (line.lastIndexOf("+", 0) === 0) { - createRow("", newLine, "insert", line, i); - newLine++; - } else if (line.lastIndexOf("-", 0) === 0) { - createRow(baseLine, "", "delete", line, i); - baseLine++; - } else { - createRow(baseLine, newLine, "equal", line, i); - baseLine++; - newLine++; - } - } - } - - $body.append($table); - - $('td:not(.skip)').each(function(i, el) { - $(el).click(function() { - var fileLine = $(el).parent().data('y'); - if (fileLine === "") - fileLine = $(el).parent().data('x') - invokeNative("comment", {"patch_line": $(el).parent().data('to'), "file_line": fileLine}); - }); - }); -} - -function invokeNative(functionName, args) { - try - { - var iframe = document.createElement('IFRAME'); - iframe.setAttribute('src', 'app://' + functionName + '#' + JSON.stringify(args)); - document.body.appendChild(iframe); - iframe.parentNode.removeChild(iframe); - iframe = null; - } - catch (err) - { - alert(err.message); - } -} - -function setComments(comments) { - - $('tr.comment').remove(); - - for (var i = 0; i < comments.length; i++) { - var comment = comments[i]; - var $comment = $("
" + comment.user + "
" + comment.content + "
"); - - if (comment['line_to'] != null) { - $("tr[data-to='" + comment.line_to + "']").after($comment); - } - else if (comment['line_from'] != null) { - $("tr[data-from='" + comment.line_from + "']").after($comment); - } - else if (comment['parent'] != null) { - $("tr[data-id='" + comment.parent + "']").after($comment); - } - } -} - -window.onload = function() { document.location.href = 'app://ready'}; diff --git a/CodeHub.iOS/Diff/diffindex.html b/CodeHub.iOS/Diff/diffindex.html deleted file mode 100644 index b003a364..00000000 --- a/CodeHub.iOS/Diff/diffindex.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Diff - - - - - - - - - \ No newline at end of file diff --git a/CodeHub.iOS/Diff/difflib.js b/CodeHub.iOS/Diff/difflib.js deleted file mode 100644 index db2c0e3a..00000000 --- a/CodeHub.iOS/Diff/difflib.js +++ /dev/null @@ -1,407 +0,0 @@ -/*** -This is part of jsdifflib v1.0. - -Copyright (c) 2007, Snowtide Informatics Systems, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of the Snowtide Informatics Systems nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT -SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. -***/ -/* Author: Chas Emerick */ -__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true}; - -difflib = { - defaultJunkFunction: function (c) { - return __whitespace.hasOwnProperty(c); - }, - - stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); }, - - stringAsLines: function (str) { - var lfpos = str.indexOf("\n"); - var crpos = str.indexOf("\r"); - var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r"; - - var lines = str.split(linebreak); - for (var i = 0; i < lines.length; i++) { - lines[i] = difflib.stripLinebreaks(lines[i]); - } - - return lines; - }, - - // iteration-based reduce implementation - __reduce: function (func, list, initial) { - if (initial != null) { - var value = initial; - var idx = 0; - } else if (list) { - var value = list[0]; - var idx = 1; - } else { - return null; - } - - for (; idx < list.length; idx++) { - value = func(value, list[idx]); - } - - return value; - }, - - // comparison function for sorting lists of numeric tuples - __ntuplecomp: function (a, b) { - var mlen = Math.max(a.length, b.length); - for (var i = 0; i < mlen; i++) { - if (a[i] < b[i]) return -1; - if (a[i] > b[i]) return 1; - } - - return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1); - }, - - __calculate_ratio: function (matches, length) { - return length ? 2.0 * matches / length : 1.0; - }, - - // returns a function that returns true if a key passed to the returned function - // is in the dict (js object) provided to this function; replaces being able to - // carry around dict.has_key in python... - __isindict: function (dict) { - return function (key) { return dict.hasOwnProperty(key); }; - }, - - // replacement for python's dict.get function -- need easy default values - __dictget: function (dict, key, defaultValue) { - return dict.hasOwnProperty(key) ? dict[key] : defaultValue; - }, - - SequenceMatcher: function (a, b, isjunk) { - this.set_seqs = function (a, b) { - this.set_seq1(a); - this.set_seq2(b); - } - - this.set_seq1 = function (a) { - if (a == this.a) return; - this.a = a; - this.matching_blocks = this.opcodes = null; - } - - this.set_seq2 = function (b) { - if (b == this.b) return; - this.b = b; - this.matching_blocks = this.opcodes = this.fullbcount = null; - this.__chain_b(); - } - - this.__chain_b = function () { - var b = this.b; - var n = b.length; - var b2j = this.b2j = {}; - var populardict = {}; - for (var i = 0; i < b.length; i++) { - var elt = b[i]; - if (b2j.hasOwnProperty(elt)) { - var indices = b2j[elt]; - if (n >= 200 && indices.length * 100 > n) { - populardict[elt] = 1; - delete b2j[elt]; - } else { - indices.push(i); - } - } else { - b2j[elt] = [i]; - } - } - - for (var elt in populardict) { - if (populardict.hasOwnProperty(elt)) { - delete b2j[elt]; - } - } - - var isjunk = this.isjunk; - var junkdict = {}; - if (isjunk) { - for (var elt in populardict) { - if (populardict.hasOwnProperty(elt) && isjunk(elt)) { - junkdict[elt] = 1; - delete populardict[elt]; - } - } - for (var elt in b2j) { - if (b2j.hasOwnProperty(elt) && isjunk(elt)) { - junkdict[elt] = 1; - delete b2j[elt]; - } - } - } - - this.isbjunk = difflib.__isindict(junkdict); - this.isbpopular = difflib.__isindict(populardict); - } - - this.find_longest_match = function (alo, ahi, blo, bhi) { - var a = this.a; - var b = this.b; - var b2j = this.b2j; - var isbjunk = this.isbjunk; - var besti = alo; - var bestj = blo; - var bestsize = 0; - var j = null; - - var j2len = {}; - var nothing = []; - for (var i = alo; i < ahi; i++) { - var newj2len = {}; - var jdict = difflib.__dictget(b2j, a[i], nothing); - for (var jkey in jdict) { - if (jdict.hasOwnProperty(jkey)) { - j = jdict[jkey]; - if (j < blo) continue; - if (j >= bhi) break; - newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1; - if (k > bestsize) { - besti = i - k + 1; - bestj = j - k + 1; - bestsize = k; - } - } - } - j2len = newj2len; - } - - while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { - besti--; - bestj--; - bestsize++; - } - - while (besti + bestsize < ahi && bestj + bestsize < bhi && - !isbjunk(b[bestj + bestsize]) && - a[besti + bestsize] == b[bestj + bestsize]) { - bestsize++; - } - - while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { - besti--; - bestj--; - bestsize++; - } - - while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) && - a[besti + bestsize] == b[bestj + bestsize]) { - bestsize++; - } - - return [besti, bestj, bestsize]; - } - - this.get_matching_blocks = function () { - if (this.matching_blocks != null) return this.matching_blocks; - var la = this.a.length; - var lb = this.b.length; - - var queue = [[0, la, 0, lb]]; - var matching_blocks = []; - var alo, ahi, blo, bhi, qi, i, j, k, x; - while (queue.length) { - qi = queue.pop(); - alo = qi[0]; - ahi = qi[1]; - blo = qi[2]; - bhi = qi[3]; - x = this.find_longest_match(alo, ahi, blo, bhi); - i = x[0]; - j = x[1]; - k = x[2]; - - if (k) { - matching_blocks.push(x); - if (alo < i && blo < j) - queue.push([alo, i, blo, j]); - if (i+k < ahi && j+k < bhi) - queue.push([i + k, ahi, j + k, bhi]); - } - } - - matching_blocks.sort(difflib.__ntuplecomp); - - var i1 = j1 = k1 = block = 0; - var non_adjacent = []; - for (var idx in matching_blocks) { - if (matching_blocks.hasOwnProperty(idx)) { - block = matching_blocks[idx]; - i2 = block[0]; - j2 = block[1]; - k2 = block[2]; - if (i1 + k1 == i2 && j1 + k1 == j2) { - k1 += k2; - } else { - if (k1) non_adjacent.push([i1, j1, k1]); - i1 = i2; - j1 = j2; - k1 = k2; - } - } - } - - if (k1) non_adjacent.push([i1, j1, k1]); - - non_adjacent.push([la, lb, 0]); - this.matching_blocks = non_adjacent; - return this.matching_blocks; - } - - this.get_opcodes = function () { - if (this.opcodes != null) return this.opcodes; - var i = 0; - var j = 0; - var answer = []; - this.opcodes = answer; - var block, ai, bj, size, tag; - var blocks = this.get_matching_blocks(); - for (var idx in blocks) { - if (blocks.hasOwnProperty(idx)) { - block = blocks[idx]; - ai = block[0]; - bj = block[1]; - size = block[2]; - tag = ''; - if (i < ai && j < bj) { - tag = 'replace'; - } else if (i < ai) { - tag = 'delete'; - } else if (j < bj) { - tag = 'insert'; - } - if (tag) answer.push([tag, i, ai, j, bj]); - i = ai + size; - j = bj + size; - - if (size) answer.push(['equal', ai, i, bj, j]); - } - } - - return answer; - } - - // this is a generator function in the python lib, which of course is not supported in javascript - // the reimplementation builds up the grouped opcodes into a list in their entirety and returns that. - this.get_grouped_opcodes = function (n) { - if (!n) n = 3; - var codes = this.get_opcodes(); - if (!codes) codes = [["equal", 0, 1, 0, 1]]; - var code, tag, i1, i2, j1, j2; - if (codes[0][0] == 'equal') { - code = codes[0]; - tag = code[0]; - i1 = code[1]; - i2 = code[2]; - j1 = code[3]; - j2 = code[4]; - codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2]; - } - if (codes[codes.length - 1][0] == 'equal') { - code = codes[codes.length - 1]; - tag = code[0]; - i1 = code[1]; - i2 = code[2]; - j1 = code[3]; - j2 = code[4]; - codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]; - } - - var nn = n + n; - var groups = []; - for (var idx in codes) { - if (codes.hasOwnProperty(idx)) { - code = codes[idx]; - tag = code[0]; - i1 = code[1]; - i2 = code[2]; - j1 = code[3]; - j2 = code[4]; - if (tag == 'equal' && i2 - i1 > nn) { - groups.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]); - i1 = Math.max(i1, i2-n); - j1 = Math.max(j1, j2-n); - } - - groups.push([tag, i1, i2, j1, j2]); - } - } - - if (groups && groups[groups.length - 1][0] == 'equal') groups.pop(); - - return groups; - } - - this.ratio = function () { - matches = difflib.__reduce( - function (sum, triple) { return sum + triple[triple.length - 1]; }, - this.get_matching_blocks(), 0); - return difflib.__calculate_ratio(matches, this.a.length + this.b.length); - } - - this.quick_ratio = function () { - var fullbcount, elt; - if (this.fullbcount == null) { - this.fullbcount = fullbcount = {}; - for (var i = 0; i < this.b.length; i++) { - elt = this.b[i]; - fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1; - } - } - fullbcount = this.fullbcount; - - var avail = {}; - var availhas = difflib.__isindict(avail); - var matches = numb = 0; - for (var i = 0; i < this.a.length; i++) { - elt = this.a[i]; - if (availhas(elt)) { - numb = avail[elt]; - } else { - numb = difflib.__dictget(fullbcount, elt, 0); - } - avail[elt] = numb - 1; - if (numb > 0) matches++; - } - - return difflib.__calculate_ratio(matches, this.a.length + this.b.length); - } - - this.real_quick_ratio = function () { - var la = this.a.length; - var lb = this.b.length; - return _calculate_ratio(Math.min(la, lb), la + lb); - } - - this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction; - this.a = this.b = null; - this.set_seqs(a, b); - } -} diff --git a/CodeHub.iOS/Diff/diffview.css b/CodeHub.iOS/Diff/diffview.css deleted file mode 100644 index 6d29eef0..00000000 --- a/CodeHub.iOS/Diff/diffview.css +++ /dev/null @@ -1,161 +0,0 @@ -/* -This is part of jsdifflib v1.0. - -Copyright 2007 - 2011 Chas Emerick . All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are -permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this list - of conditions and the following disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those of the -authors and should not be interpreted as representing official policies, either expressed -or implied, of Chas Emerick. -*/ -body { - margin:0; -} - -table.diff { - border-collapse:collapse; - border:1px solid darkgray; - white-space:pre; - width:100%; -} -table.diff tbody { - font-family:Courier, monospace -} -table.diff tbody th { - font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; - background:#EED; - font-weight:normal; - border:1px solid #BBC; - color:#886; - padding:.3em .3em; - text-align:right; -} -table.diff thead { - border-bottom:1px solid #BBC; - background:#EFEFEF; - font-family:Verdana; -} -table.diff thead th.texttitle { - text-align:left -} -table.diff tbody td { - padding:.3em .4em; - width: 100%; -} -table.diff .empty { - background-color:#DDD; -} -table.diff .replace { - background-color:#FD8 -} -table.diff .delete { - background-color:#E99; -} -table.diff .skip { - background-color:#EFEFEF; - border:1px solid #AAA; - border-right:1px solid #BBC; -} -table.diff .insert { - background-color:#9E9 -} -table.diff th.author { - text-align:left; - border-top:1px solid #BBC; - background:#EFEFEF -} - -.comment > td { -background:#EED; -border:1px solid #BBC; -} - -.comment > td > div.inner { -margin: 10px; -border-radius: 3px; -background:#fff; -border:1px solid #BBC; -padding: 15px 10px 10px 10px; -} - -.comment > td > div.inner > header { -padding-left:43px; -position:relative; -color:#3b73af; -font-weight: bold; -display:inline; -} - -.comment > td > div.inner > header > img { -position: absolute; -top: 0px; -left: 0px; -border-radius: 3px; -width: 32px; -height: 32px; -display: inline; -border: none; -} - -.comment > td > div.inner > .content { -overflow: hidden; -word-wrap: break-word; -margin:0; -padding:0; -padding-left: 43px; -} - -.addcomment { -background:#EED; -border:1px solid #BBC; -} - -.addcomment textarea { -border:1px solid #999999; -width:100%; -margin:5px 5px; -padding:3px; --webkit-box-sizing: border-box; --moz-box-sizing: border-box; -box-sizing: border-box; -} - -.addcomment .inner { -padding: 10px 10px; -} - -.addcomment a.ok { - border:1px solid #25729a; -webkit-border-radius: 3px; -moz-border-radius: 3px;border-radius: 3px;font-size:12px;font-family:arial, helvetica, sans-serif; padding: 10px 10px 10px 10px; text-decoration:none; display:inline-block;text-shadow: -1px -1px 0 rgba(0,0,0,0.3);font-weight:bold; color: #FFFFFF; - background-color: #3093c7; background-image: -webkit-gradient(linear, left top, left bottom, from(#3093c7), to(#1c5a85)); - background-image: -webkit-linear-gradient(top, #3093c7, #1c5a85); - background-image: -moz-linear-gradient(top, #3093c7, #1c5a85); - background-image: -ms-linear-gradient(top, #3093c7, #1c5a85); - background-image: -o-linear-gradient(top, #3093c7, #1c5a85); - background-image: linear-gradient(to bottom, #3093c7, #1c5a85);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#3093c7, endColorstr=#1c5a85); - margin-left: 5px; -} - -.addcomment a.cancel { - margin-left: 20px; - color: #3b73af; - text-decoration: none; -} diff --git a/CodeHub.iOS/Diff/diffview.js b/CodeHub.iOS/Diff/diffview.js deleted file mode 100644 index 2013c7f5..00000000 --- a/CodeHub.iOS/Diff/diffview.js +++ /dev/null @@ -1,212 +0,0 @@ -/* -This is part of jsdifflib v1.0. - -Copyright 2007 - 2011 Chas Emerick . All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are -permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this list - of conditions and the following disclaimer in the documentation and/or other materials - provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those of the -authors and should not be interpreted as representing official policies, either expressed -or implied, of Chas Emerick. -*/ -diffview = { - /** - * Builds and returns a visual diff view. The single parameter, `params', should contain - * the following values: - * - * - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher - * - newTextLines: the array of strings that was used as the new text input to SequenceMatcher - * - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes() - * - baseTextName: the title to be displayed above the base text listing in the diff view; defaults - * to "Base Text" - * - newTextName: the title to be displayed above the new text listing in the diff view; defaults - * to "New Text" - * - contextSize: the number of lines of context to show around differences; by default, all lines - * are shown - * - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is - * generated - */ - buildView: function (params) { - var baseTextLines = params.baseTextLines; - var newTextLines = params.newTextLines; - var opcodes = params.opcodes; - var baseTextName = params.baseTextName ? params.baseTextName : "Base Text"; - var newTextName = params.newTextName ? params.newTextName : "New Text"; - var contextSize = params.contextSize; - var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0; - - if (baseTextLines == null) - throw "Cannot build diff view; baseTextLines is not defined."; - if (newTextLines == null) - throw "Cannot build diff view; newTextLines is not defined."; - if (!opcodes) - throw "Canno build diff view; opcodes is not defined."; - - function celt (name, clazz) { - var e = document.createElement(name); - e.className = clazz; - return e; - } - - function telt (name, text) { - var e = document.createElement(name); - e.appendChild(document.createTextNode(text)); - return e; - } - - function ctelt (name, clazz, text) { - var e = document.createElement(name); - e.className = clazz; - e.appendChild(document.createTextNode(text)); - return e; - } - - - var tdata = document.createElement("thead"); - /* - var node = document.createElement("tr"); - tdata.appendChild(node); - if (inline) { - node.appendChild(document.createElement("th")); - node.appendChild(document.createElement("th")); - node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName)); - } else { - node.appendChild(document.createElement("th")); - node.appendChild(ctelt("th", "texttitle", baseTextName)); - node.appendChild(document.createElement("th")); - node.appendChild(ctelt("th", "texttitle", newTextName)); - } - */ - tdata = [tdata]; - - - var rows = []; - var node2; - var node; - - /** - * Adds two cells to the given row; if the given row corresponds to a real - * line number (based on the line index tidx and the endpoint of the - * range in question tend), then the cells will contain the line number - * and the line of text from textLines at position tidx (with the class of - * the second cell set to the name of the change represented), and tidx + 1 will - * be returned. Otherwise, tidx is returned, and two empty cells are added - * to the given row. - */ - function addCells (row, tidx, tend, textLines, change) { - if (tidx < tend) { - row.appendChild(telt("th", (tidx + 1).toString())); - row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); - return tidx + 1; - } else { - row.appendChild(document.createElement("th")); - row.appendChild(celt("td", "empty")); - return tidx; - } - } - - function addCellsInline (row, tidx, tidx2, textLines, change) { - if (tidx != null) { - var att = document.createAttribute("data-from"); - att.value = (tidx + 1).toString(); - row.setAttributeNode(att); - } - if (tidx2 != null) { - var att = document.createAttribute("data-to"); - att.value = (tidx2 + 1).toString(); - row.setAttributeNode(att); - } - row.appendChild(telt("th", tidx == null ? "" : (tidx + 1).toString())); - row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 + 1).toString())); - row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); - } - - for (var idx = 0; idx < opcodes.length; idx++) { - code = opcodes[idx]; - change = code[0]; - var b = code[1]; - var be = code[2]; - var n = code[3]; - var ne = code[4]; - var rowcnt = Math.max(be - b, ne - n); - var toprows = []; - var botrows = []; - for (var i = 0; i < rowcnt; i++) { - // jump ahead if we've alredy provided leading context or if this is the first range - if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") { - var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize); - if (jump > 1) { - toprows.push(node = document.createElement("tr")); - - b += jump; - n += jump; - i += jump - 1; - node.appendChild(telt("th", "...")); - if (!inline) node.appendChild(ctelt("td", "skip", "")); - node.appendChild(telt("th", "...")); - node.appendChild(ctelt("td", "skip", "")); - - // skip last lines if they're all equal - if (idx + 1 == opcodes.length) { - break; - } else { - continue; - } - } - } - - toprows.push(node = document.createElement("tr")); - if (inline) { - if (change == "insert") { - addCellsInline(node, null, n++, newTextLines, change); - } else if (change == "replace") { - botrows.push(node2 = document.createElement("tr")); - if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete"); - if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert"); - } else if (change == "delete") { - addCellsInline(node, b++, null, baseTextLines, change); - } else { - // equal - addCellsInline(node, b++, n++, baseTextLines, change); - } - } else { - b = addCells(node, b, be, baseTextLines, change); - n = addCells(node, n, ne, newTextLines, change); - } - } - - for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]); - for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]); - } - -// rows.push(node = ctelt("th", "author", "diff view generated by ")); -// node.setAttribute("colspan", inline ? 3 : 4); -// node.appendChild(node2 = telt("a", "jsdifflib")); -// node2.setAttribute("href", "http://github.com/cemerick/jsdifflib"); - - tdata.push(node = document.createElement("tbody")); - for (var idx in rows) node.appendChild(rows[idx]); - - node = celt("table", "diff" + (inline ? " inlinediff" : "")); - for (var idx in tdata) node.appendChild(tdata[idx]); - return node; - } -} \ No newline at end of file diff --git a/CodeHub.iOS/Images/Buttons/cancel.png b/CodeHub.iOS/Images/Buttons/cancel.png deleted file mode 100644 index 0714cf49..00000000 Binary files a/CodeHub.iOS/Images/Buttons/cancel.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Buttons/cancel@2x.png b/CodeHub.iOS/Images/Buttons/cancel@2x.png deleted file mode 100644 index 815e9198..00000000 Binary files a/CodeHub.iOS/Images/Buttons/cancel@2x.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Buttons/grey_button.png b/CodeHub.iOS/Images/Buttons/grey_button.png deleted file mode 100755 index 387db23f..00000000 Binary files a/CodeHub.iOS/Images/Buttons/grey_button.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Buttons/grey_button@2x.png b/CodeHub.iOS/Images/Buttons/grey_button@2x.png deleted file mode 100755 index 9209f22e..00000000 Binary files a/CodeHub.iOS/Images/Buttons/grey_button@2x.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Buttons/save.png b/CodeHub.iOS/Images/Buttons/save.png deleted file mode 100755 index c68aef99..00000000 Binary files a/CodeHub.iOS/Images/Buttons/save.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Buttons/save@2x.png b/CodeHub.iOS/Images/Buttons/save@2x.png deleted file mode 100755 index 7b72a1a0..00000000 Binary files a/CodeHub.iOS/Images/Buttons/save@2x.png and /dev/null differ diff --git a/CodeHub.iOS/Images/Images.cs b/CodeHub.iOS/Images/Images.cs index d63999e1..923a908d 100755 --- a/CodeHub.iOS/Images/Images.cs +++ b/CodeHub.iOS/Images/Images.cs @@ -1,41 +1,125 @@ -using UIKit; -using MonoTouch.UIKit; - +using UIKit; +using MonoTouch.UIKit; + namespace CodeHub.iOS { public static class Images { - public static UIImage LoginUserUnknown { get { return UIImageHelper.FromFileAuto("Images/login_user_unknown").ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); } } - public static UIImage Avatar { get { return UIImageHelper.FromFileAuto("Images/avatar"); } } - public static UIImage DownChevron { get { return CreateTemplateFromAuto("Images/down_chevron"); } } + public static UIImage LoginUserUnknown + { + get + { + var img = UIImage.FromBundle("UnknownUser"); + img.AccessibilityLabel = "Unknown User"; + return img; + } + } + + public static UIImage Avatar + { + get + { + var img = UIImage.FromBundle("Avatar"); + img.AccessibilityLabel = "Avatar"; + return img; + } + } + + public static UIImage DownChevron + { + get + { + var img = CreateTemplateFromAuto("Images/down_chevron"); + img.AccessibilityElementsHidden = true; + return img; + } + } public static class Logos - { - public static UIImage DotComMascot { get { return UIImage.FromFile("Images/Logos/dotcom-mascot.png"); } } + { + public static UIImage DotComMascot { get { return UIImage.FromFile("Images/Logos/dotcom-mascot.png"); } } public static UIImage EnterpriseMascot { get { return UIImage.FromFile("Images/Logos/enterprise-mascot.png"); } } } public static class Buttons { - public static UIImage BlackButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/black_button"); } } - public static UIImage GreyButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/grey_button"); } } - public static UIImage CheckButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/check"); } } - public static UIImage BackButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/back"); } } - public static UIImage ThreeLinesButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/three_lines"); } } - public static UIImage CancelButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/cancel"); } } - public static UIImage SortButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/sort"); } } - public static UIImage SaveButton { get { return UIImageHelper.FromFileAuto("Images/Buttons/save"); } } - } - - public static class Web - { - public static UIImage BackButton { get { return UIImageHelper.FromFileAuto("Images/Web/back"); } } - public static UIImage FowardButton { get { return UIImageHelper.FromFileAuto("Images/Web/forward"); } } + public static UIImage BlackButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Buttons/black_button"); + img.AccessibilityElementsHidden = true; + return img; + } + } + + public static UIImage CheckButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Buttons/check"); + img.AccessibilityLabel = "Check"; + return img; + } + } + + public static UIImage BackButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Buttons/back"); + img.AccessibilityLabel = "Back"; + return img; + } + } + + public static UIImage ThreeLinesButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Buttons/three_lines"); + img.AccessibilityLabel = "Menu"; + return img; + } + } + + public static UIImage SortButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Buttons/sort"); + img.AccessibilityLabel = "Sort"; + return img; + } + } } - private static UIImage CreateTemplateFromAuto(string path) - { - return UIImageHelper.FromFileAuto(path); + public static class Web + { + public static UIImage BackButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Web/back"); + img.AccessibilityLabel = "Back"; + return img; + } + } + + public static UIImage FowardButton + { + get + { + var img = UIImageHelper.FromFileAuto("Images/Web/forward"); + img.AccessibilityLabel = "Forward"; + return img; + } + } + } + + private static UIImage CreateTemplateFromAuto(string path) + { + return UIImageHelper.FromFileAuto(path); } } } diff --git a/CodeHub.iOS/Info.plist b/CodeHub.iOS/Info.plist index 31710dce..65287a53 100755 --- a/CodeHub.iOS/Info.plist +++ b/CodeHub.iOS/Info.plist @@ -2,6 +2,20 @@ + Fabric + + APIKey + 64cbd285ce671bdff5629ee3d00e79e4ae834f39 + Kits + + + KitInfo + + KitName + Crashlytics + + + UIDeviceFamily 1 @@ -21,7 +35,7 @@ CFBundleIdentifier com.dillonbuchanan.codehub CFBundleShortVersionString - 2.9.3 + 2.19.1 UIStatusBarTintParameters UINavigationBar @@ -55,7 +69,7 @@ UIInterfaceOrientationLandscapeRight CFBundleVersion - 2.9.3.0 + 2.19.1.1 UILaunchStoryboardName Launch NSAppTransportSecurity @@ -73,5 +87,11 @@ CFBundleName CodeHub + NSPhotoLibraryUsageDescription + Photos can be uploaded to comments created within the app. + NSCalendarsUsageDescription + Calendar is never used within this app. + NSPhotoLibraryAddUsageDescription + Photos can be saved from readable content within the app. diff --git a/CodeHub.iOS/Launch.storyboard b/CodeHub.iOS/Launch.storyboard index e802e089..a0879d1c 100644 --- a/CodeHub.iOS/Launch.storyboard +++ b/CodeHub.iOS/Launch.storyboard @@ -1,9 +1,14 @@ - - + + + + + - + + + @@ -12,21 +17,21 @@ - + - - - + + - - + + + - - + + @@ -35,85 +40,12 @@ + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/CodeHub.iOS/LinkerPleaseInclude.cs b/CodeHub.iOS/LinkerPleaseInclude.cs index eae37fbf..b9ceea8d 100644 --- a/CodeHub.iOS/LinkerPleaseInclude.cs +++ b/CodeHub.iOS/LinkerPleaseInclude.cs @@ -1,10 +1,8 @@ -using System; -using MvvmCross.Platform.IoC; -using Foundation; +using MvvmCross.Platform.IoC; namespace CodeHub.iOS { - [Preserve(AllMembers = true)] + [Preserve] public class LinkerPleaseInclude { public void Include(MvxPropertyInjector injector){ diff --git a/CodeHub.iOS/Briefcase.png b/CodeHub.iOS/Resources/Briefcase.png similarity index 100% rename from CodeHub.iOS/Briefcase.png rename to CodeHub.iOS/Resources/Briefcase.png diff --git a/CodeHub.iOS/CodeHubLaunch.png b/CodeHub.iOS/Resources/CodeHubLaunch.png similarity index 100% rename from CodeHub.iOS/CodeHubLaunch.png rename to CodeHub.iOS/Resources/CodeHubLaunch.png diff --git a/CodeHub.iOS/CodeHubRound.png b/CodeHub.iOS/Resources/CodeHubRound.png similarity index 100% rename from CodeHub.iOS/CodeHubRound.png rename to CodeHub.iOS/Resources/CodeHubRound.png diff --git a/CodeHub.iOS/Feedback.png b/CodeHub.iOS/Resources/Feedback.png similarity index 100% rename from CodeHub.iOS/Feedback.png rename to CodeHub.iOS/Resources/Feedback.png diff --git a/CodeHub.iOS/Heart.png b/CodeHub.iOS/Resources/Heart.png similarity index 100% rename from CodeHub.iOS/Heart.png rename to CodeHub.iOS/Resources/Heart.png diff --git a/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/Contents.json b/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/Contents.json index d7bf606f..ef35c190 100644 --- a/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/Contents.json +++ b/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/Contents.json @@ -1,9 +1,13 @@ { "images": [ { - "filename": "Icon-Small.png", - "size": "29x29", - "scale": "1x", + "size": "20x20", + "scale": "2x", + "idiom": "iphone" + }, + { + "size": "20x20", + "scale": "3x", "idiom": "iphone" }, { @@ -30,18 +34,6 @@ "scale": "3x", "idiom": "iphone" }, - { - "filename": "Icon.png", - "size": "57x57", - "scale": "1x", - "idiom": "iphone" - }, - { - "filename": "Icon@2x.png", - "size": "57x57", - "scale": "2x", - "idiom": "iphone" - }, { "filename": "Icon-60@2x.png", "size": "60x60", @@ -55,38 +47,36 @@ "idiom": "iphone" }, { - "filename": "Icon-Small.png", - "size": "29x29", + "size": "20x20", "scale": "1x", "idiom": "ipad" }, { - "filename": "Icon-Small@2x.png", - "size": "29x29", + "size": "20x20", "scale": "2x", "idiom": "ipad" }, { - "filename": "Icon-Small-40.png", - "size": "40x40", + "filename": "Icon-Small.png", + "size": "29x29", "scale": "1x", "idiom": "ipad" }, { - "filename": "Icon-Small-40@2x.png", - "size": "40x40", + "filename": "Icon-Small@2x.png", + "size": "29x29", "scale": "2x", "idiom": "ipad" }, { - "filename": "Icon-Small-50.png", - "size": "50x50", + "filename": "Icon-Small-40.png", + "size": "40x40", "scale": "1x", "idiom": "ipad" }, { - "filename": "Icon-Small-50@2x.png", - "size": "50x50", + "filename": "Icon-Small-40@2x.png", + "size": "40x40", "scale": "2x", "idiom": "ipad" }, @@ -96,18 +86,6 @@ "scale": "2x", "idiom": "ipad" }, - { - "filename": "Icon-72.png", - "size": "72x72", - "scale": "1x", - "idiom": "ipad" - }, - { - "filename": "Icon-72@2x.png", - "size": "72x72", - "scale": "2x", - "idiom": "ipad" - }, { "filename": "Icon-76.png", "size": "76x76", @@ -121,14 +99,10 @@ "idiom": "ipad" }, { - "size": "60x60", - "scale": "2x", - "idiom": "car" - }, - { - "size": "60x60", - "scale": "3x", - "idiom": "car" + "filename": "appstore.png", + "size": "1024x1024", + "scale": "1x", + "idiom": "ios-marketing" }, { "role": "notificationCenter", diff --git a/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/appstore.png b/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/appstore.png new file mode 100644 index 00000000..b6b6bbc7 Binary files /dev/null and b/CodeHub.iOS/Resources/Images.xcassets/AppIcons.appiconset/appstore.png differ diff --git a/CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/Contents.json b/CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/Contents.json new file mode 100644 index 00000000..f246079d --- /dev/null +++ b/CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/Contents.json @@ -0,0 +1,84 @@ +{ + "images": [ + { + "idiom": "universal" + }, + { + "filename": "avatar.png", + "scale": "1x", + "idiom": "universal" + }, + { + "filename": "avatar@2x.png", + "scale": "2x", + "idiom": "universal" + }, + { + "scale": "3x", + "idiom": "universal" + }, + { + "idiom": "iphone" + }, + { + "scale": "1x", + "idiom": "iphone" + }, + { + "scale": "2x", + "idiom": "iphone" + }, + { + "subtype": "retina4", + "scale": "2x", + "idiom": "iphone" + }, + { + "scale": "3x", + "idiom": "iphone" + }, + { + "idiom": "ipad" + }, + { + "scale": "1x", + "idiom": "ipad" + }, + { + "scale": "2x", + "idiom": "ipad" + }, + { + "idiom": "watch" + }, + { + "scale": "2x", + "idiom": "watch" + }, + { + "screenWidth": "{130,145}", + "scale": "2x", + "idiom": "watch" + }, + { + "screenWidth": "{146,165}", + "scale": "2x", + "idiom": "watch" + }, + { + "idiom": "mac" + }, + { + "scale": "1x", + "idiom": "mac" + }, + { + "scale": "2x", + "idiom": "mac" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/CodeHub.iOS/Images/avatar.png b/CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/avatar.png similarity index 100% rename from CodeHub.iOS/Images/avatar.png rename to CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/avatar.png diff --git a/CodeHub.iOS/Images/avatar@2x.png b/CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/avatar@2x.png similarity index 100% rename from CodeHub.iOS/Images/avatar@2x.png rename to CodeHub.iOS/Resources/Images.xcassets/Avatar.imageset/avatar@2x.png diff --git a/CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/Contents.json b/CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/Contents.json new file mode 100644 index 00000000..1ce7bf7c --- /dev/null +++ b/CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/Contents.json @@ -0,0 +1,85 @@ +{ + "images": [ + { + "idiom": "universal" + }, + { + "filename": "login_user_unknown.png", + "scale": "1x", + "idiom": "universal" + }, + { + "filename": "login_user_unknown@2x.png", + "scale": "2x", + "idiom": "universal" + }, + { + "scale": "3x", + "idiom": "universal" + }, + { + "idiom": "iphone" + }, + { + "scale": "1x", + "idiom": "iphone" + }, + { + "scale": "2x", + "idiom": "iphone" + }, + { + "subtype": "retina4", + "scale": "2x", + "idiom": "iphone" + }, + { + "scale": "3x", + "idiom": "iphone" + }, + { + "idiom": "ipad" + }, + { + "scale": "1x", + "idiom": "ipad" + }, + { + "scale": "2x", + "idiom": "ipad" + }, + { + "idiom": "watch" + }, + { + "scale": "2x", + "idiom": "watch" + }, + { + "screenWidth": "{130,145}", + "scale": "2x", + "idiom": "watch" + }, + { + "screenWidth": "{146,165}", + "scale": "2x", + "idiom": "watch" + }, + { + "idiom": "mac" + }, + { + "scale": "1x", + "idiom": "mac" + }, + { + "scale": "2x", + "idiom": "mac" + } + ], + "info": { + "version": 1, + "author": "xcode", + "template-rendering-intent": "template" + } +} \ No newline at end of file diff --git a/CodeHub.iOS/Images/login_user_unknown.png b/CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/login_user_unknown.png similarity index 100% rename from CodeHub.iOS/Images/login_user_unknown.png rename to CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/login_user_unknown.png diff --git a/CodeHub.iOS/Images/login_user_unknown@2x.png b/CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/login_user_unknown@2x.png similarity index 100% rename from CodeHub.iOS/Images/login_user_unknown@2x.png rename to CodeHub.iOS/Resources/Images.xcassets/UnknownUser.imageset/login_user_unknown@2x.png diff --git a/CodeHub.iOS/Organizations.png b/CodeHub.iOS/Resources/Organizations.png similarity index 100% rename from CodeHub.iOS/Organizations.png rename to CodeHub.iOS/Resources/Organizations.png diff --git a/CodeHub.iOS/Rocket.png b/CodeHub.iOS/Resources/Rocket.png similarity index 100% rename from CodeHub.iOS/Rocket.png rename to CodeHub.iOS/Resources/Rocket.png diff --git a/CodeHub.iOS/Services/AccountPreferencesService.cs b/CodeHub.iOS/Services/AccountPreferencesService.cs deleted file mode 100755 index 97d8f26f..00000000 --- a/CodeHub.iOS/Services/AccountPreferencesService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CodeHub.Core.Services; -using System.IO; -using System; - -namespace CodeHub.iOS.Services -{ - public class AccountPreferencesService : IAccountPreferencesService - { - public readonly static string BaseDir = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.Personal), ".."); - - public string AccountsDir - { - get { return System.IO.Path.Combine(BaseDir, "Documents/accounts"); } - } - } -} diff --git a/CodeHub.iOS/Services/AlertDialogService.cs b/CodeHub.iOS/Services/AlertDialogService.cs index 23167ce3..96cbdbcf 100644 --- a/CodeHub.iOS/Services/AlertDialogService.cs +++ b/CodeHub.iOS/Services/AlertDialogService.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; using Foundation; using CoreGraphics; +using System.Collections.Generic; +using System.Linq; +using BigTed; namespace CodeHub.iOS.Services { @@ -30,22 +33,38 @@ public Task Alert(string title, string message) public static void ShowAlert(string title, string message, Action dismissed = null) { + var window = new UIWindow(UIScreen.MainScreen.Bounds); + window.RootViewController = new UIViewController(); + var alert = UIAlertController.Create(title, message, UIAlertControllerStyle.Alert); alert.AddAction(UIAlertAction.Create("Ok", UIAlertActionStyle.Default, x => { dismissed?.Invoke(); alert.Dispose(); + window.Dispose(); })); - UIApplication.SharedApplication.KeyWindow.GetVisibleViewController().PresentViewController(alert, true, null); + + var topWindow = UIApplication.SharedApplication.Windows.Last(); + window.WindowLevel = topWindow.WindowLevel + 1; + + window.MakeKeyAndVisible(); + window.RootViewController.PresentViewController(alert, true, null); } - public static void ShareUrl(string url, UIBarButtonItem barButtonItem = null) + public static void Share(string title = null, string body = null, string url = null, UIBarButtonItem barButtonItem = null) { try { - var item = new NSUrl(url); - var activityItems = new NSObject[] { item }; + var activityItems = new List(); + if (body != null) + activityItems.Add(new NSString(body)); + if (url != null) + activityItems.Add(new NSUrl(url)); + UIActivity[] applicationActivities = null; - var activityController = new UIActivityViewController (activityItems, applicationActivities); + var activityController = new UIActivityViewController (activityItems.ToArray(), applicationActivities); + + if (title != null) + activityController.SetValueForKey(new NSString(title), new NSString("subject")); if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) { @@ -94,6 +113,31 @@ public Task PromptTextBox(string title, string message, string defaultVa alert.Show(); return tcs.Task; } + + public static UIColor BackgroundTint; + + public void Show(string text) + { + ProgressHUD.Shared.HudBackgroundColour = BackgroundTint; + BTProgressHUD.Show(text, maskType: ProgressHUD.MaskType.Gradient); + UIApplication.SharedApplication.BeginIgnoringInteractionEvents(); + } + + public void ShowSuccess(string text) + { + BTProgressHUD.ShowSuccessWithStatus(text); + } + + public void ShowError(string text) + { + BTProgressHUD.ShowErrorWithStatus(text); + } + + public void Hide() + { + BTProgressHUD.Dismiss(); + UIApplication.SharedApplication.EndIgnoringInteractionEvents(); + } } } diff --git a/CodeHub.iOS/Services/DefaultValueService.cs b/CodeHub.iOS/Services/DefaultValueService.cs deleted file mode 100755 index 44f4a46c..00000000 --- a/CodeHub.iOS/Services/DefaultValueService.cs +++ /dev/null @@ -1,85 +0,0 @@ -using CodeHub.Core.Services; -using Foundation; - -namespace CodeHub.iOS.Services -{ - public class DefaultValueService : IDefaultValueService - { - public static NSUserDefaults Defaults = NSUserDefaults.StandardUserDefaults; - - public bool TryGet(string key, out string value) - { - if (Defaults[key] == null) - { - value = default(string); - return false; - } - - value = Defaults.StringForKey(key); - return true; - } - - public bool TryGet(string key, out int value) - { - if (Defaults[key] == null) - { - value = default(int); - return false; - } - - value = (int)Defaults.IntForKey(key); - return true; - } - - public bool TryGet(string key, out bool value) - { - if (Defaults[key] == null) - { - value = default(bool); - return false; - } - - value = Defaults.BoolForKey(key); - return true; - } - - public void Set(string key, string value) - { - if (value == null) - Clear(key); - else - { - Defaults.SetString(value, key); - Defaults.Synchronize(); - } - } - - public void Set(string key, int? value) - { - if (!value.HasValue) - Clear(key); - else - { - Defaults.SetInt(value.Value, key); - Defaults.Synchronize(); - } - } - - public void Set(string key, bool? value) - { - if (!value.HasValue) - Clear(key); - else - { - Defaults.SetBool(value.Value, key); - Defaults.Synchronize(); - } - } - - public void Clear(string key) - { - Defaults.RemoveObject(key); - Defaults.Synchronize(); - } - } -} diff --git a/CodeHub.iOS/Services/ErrorService.cs b/CodeHub.iOS/Services/ErrorService.cs deleted file mode 100644 index 7c9c86bf..00000000 --- a/CodeHub.iOS/Services/ErrorService.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using CodeHub.Core.Services; -using System.Net.Http; -using CodeHub.Core; -using System.Text; -using UIKit; -using System.IO; -using System.Threading.Tasks; -using Foundation; - -namespace CodeHub.iOS.Services -{ - public class ErrorService : IErrorService - { - private readonly IAnalyticsService _analyticsService; - private readonly string _appVersion, _systemVersion; - - private static string GetFilePath() - { - var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments); - return Path.Combine (documents, "..", "tmp", "crash.log"); - } - - public ErrorService(IAnalyticsService analyticsService) - { - _analyticsService = analyticsService; - _appVersion = UIApplication.SharedApplication.GetVersion(); - _systemVersion = UIDevice.CurrentDevice.SystemVersion; - } - - public void Init() - { - TaskScheduler.UnobservedTaskException += (sender, e) => { - if (!e.Observed) - { - SendError(e.Exception, true); - e.SetObserved(); - } - }; - - AppDomain.CurrentDomain.UnhandledException += (sender, e) => { - var ex = e.ExceptionObject as Exception; - if (ex == null) return; - - if (e.IsTerminating) - WriteFile(ex, true); - else - SendError(ex, false); - }; - - Task.Run(SendPersistedError); - } - - public void Log(Exception e, bool fatal = false) - { - if (fatal) - { - WriteFile(e, fatal); - } - else - { - SendError(e, fatal); - } - } - - private void SendError(Exception e, bool fatal) - { - SendError(Serialize(e, fatal)); - } - - - private static void SendError(string data) - { - var client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, "https://6o8w5n7wyc.execute-api.us-east-1.amazonaws.com/prod"); - request.Headers.Add("x-api-key", Secrets.ErrorReportingKey); - request.Content = new StringContent(data, Encoding.UTF8, "application/json"); - client.SendAsync(request).ToBackground(); - } - - private void WriteFile(Exception e, bool fatal) - { - var body = Serialize(e, fatal); - File.WriteAllText(GetFilePath(), body, Encoding.UTF8); - } - - private void SendPersistedError() - { - var path = GetFilePath(); - if (File.Exists(path)) - { - var body = File.ReadAllText(path, Encoding.UTF8); - SendError(body); - File.Delete(path); - } - } - - private string Serialize(Exception e, bool fatal) - { - var sb = new StringBuilder(); - var ex = e; - while (ex != null) - { - sb.AppendLine(ex.Message + ": " + ex.StackTrace); - if (ex.InnerException != null) - sb.AppendLine("-------------"); - ex = ex.InnerException; - } - - var error = new { - e.Message, - Stack = sb.ToString(), - ApplicationVersion = _appVersion, - SystemVersion = _systemVersion, - TargetName = e.TargetSite?.Name, - Fatal = fatal.ToString(), - Name = NSBundle.MainBundle.BundleIdentifier, - Trace = string.Join(" -> ", _analyticsService.GetVisitedScreens()) - }; - - return Newtonsoft.Json.JsonConvert.SerializeObject(error); - } - } -} - diff --git a/CodeHub.iOS/Services/FeaturesService.cs b/CodeHub.iOS/Services/FeaturesService.cs index 17404ed0..19f5fb9c 100644 --- a/CodeHub.iOS/Services/FeaturesService.cs +++ b/CodeHub.iOS/Services/FeaturesService.cs @@ -1,22 +1,23 @@ using CodeHub.Core.Services; -using System; using System.Threading.Tasks; using UIKit; +using Plugin.Settings.Abstractions; +using Plugin.Settings; namespace CodeHub.iOS.Services { public class FeaturesService : IFeaturesService { - private readonly IDefaultValueService _defaultValueService; + private readonly ISettings _defaultValueService; private readonly IInAppPurchaseService _inAppPurchaseService; public const string ProEdition = "com.dillonbuchanan.codehub.pro"; public const string EnterpriseEdition = "com.dillonbuchanan.codehub.enterprise_support"; public const string PushNotifications = "com.dillonbuchanan.codehub.push"; - public FeaturesService(IDefaultValueService defaultValueService, IInAppPurchaseService inAppPurchaseService) + public FeaturesService(IInAppPurchaseService inAppPurchaseService) { - _defaultValueService = defaultValueService; + _defaultValueService = CrossSettings.Current; _inAppPurchaseService = inAppPurchaseService; } @@ -36,7 +37,7 @@ public async Task ActivatePro() public void ActivateProDirect() { - _defaultValueService.Set(ProEdition, true); + _defaultValueService.AddOrUpdateValue(ProEdition, true); } public async Task RestorePro() @@ -47,8 +48,7 @@ public async Task RestorePro() private bool IsActivated(string id) { - bool value; - return _defaultValueService.TryGet(id, out value) && value; + return _defaultValueService.GetValueOrDefault(id, false); } private void ActivateUserNotifications() diff --git a/CodeHub.iOS/Services/InAppPurchaseService.cs b/CodeHub.iOS/Services/InAppPurchaseService.cs index d1ece55b..6d80f2ea 100644 --- a/CodeHub.iOS/Services/InAppPurchaseService.cs +++ b/CodeHub.iOS/Services/InAppPurchaseService.cs @@ -4,9 +4,9 @@ using Foundation; using System.Collections.Generic; using System.Reactive.Subjects; -using CodeHub.Core.Services; using System.Linq; using Splat; +using Plugin.Settings; namespace CodeHub.iOS.Services { @@ -28,13 +28,11 @@ public class InAppPurchaseService : IInAppPurchaseService, IEnableLogger private TaskCompletionSource _restoreSource; private readonly LinkedList _productDataRequests = new LinkedList(); private readonly ISubject _errorSubject = new Subject(); - private readonly IDefaultValueService _defaultValueService; public IObservable ThrownExceptions { get { return _errorSubject; } } - public InAppPurchaseService(IDefaultValueService defaultValueService) + public InAppPurchaseService() { - _defaultValueService = defaultValueService; _observer = new TransactionObserver(this); SKPaymentQueue.DefaultQueue.AddTransactionObserver(_observer); } @@ -94,8 +92,8 @@ private void CompleteTransaction (SKPaymentTransaction transaction) var productId = transaction?.Payment?.ProductIdentifier; if (productId == null) throw new Exception("Unable to complete transaction as iTunes returned an empty product identifier!"); - - _defaultValueService.Set(productId, true); + + CrossSettings.Current.AddOrUpdateValue(productId, true); _actionSource?.TrySetResult(true); } @@ -105,11 +103,11 @@ private void RestoreTransaction (SKPaymentTransaction transaction) if (productId == null) throw new Exception("Unable to restore transaction as iTunes returned an empty product identifier!"); - _defaultValueService.Set(productId, true); + CrossSettings.Current.AddOrUpdateValue(productId, true); if (productId == FeaturesService.EnterpriseEdition || productId == FeaturesService.PushNotifications) - _defaultValueService.Set(FeaturesService.ProEdition, true); + Core.Settings.IsProEnabled = true; } private void FailedTransaction (SKPaymentTransaction transaction) @@ -168,7 +166,7 @@ public override void UpdatedTransactions(SKPaymentQueue queue, SKPaymentTransact } } - public override void PaymentQueueRestoreCompletedTransactionsFinished(SKPaymentQueue queue) + public override void RestoreCompletedTransactionsFinished(SKPaymentQueue queue) { this.Log().Debug("Payment queue restore complete"); _inAppPurchases._restoreSource?.TrySetResult(true); diff --git a/CodeHub.iOS/Services/MarkdownService.cs b/CodeHub.iOS/Services/MarkdownService.cs index b228f211..353acdda 100755 --- a/CodeHub.iOS/Services/MarkdownService.cs +++ b/CodeHub.iOS/Services/MarkdownService.cs @@ -1,7 +1,8 @@ using JavaScriptCore; using Foundation; using CodeHub.Core.Services; - +using System.Threading.Tasks; + namespace CodeHub.iOS.Services { public class MarkdownService : IMarkdownService @@ -18,11 +19,11 @@ public MarkdownService() _val = _ctx[new NSString("marked")]; } - public string Convert(string c) + public Task Convert(string c) { if (string.IsNullOrEmpty(c)) - return string.Empty; - return _val.Call(JSValue.From(c, _ctx)).ToString(); + return Task.FromResult(string.Empty); + return Task.Run(() => _val.Call(JSValue.From(c, _ctx)).ToString()); } } } diff --git a/CodeHub.iOS/Services/NetworkActivityService.cs b/CodeHub.iOS/Services/NetworkActivityService.cs new file mode 100644 index 00000000..c4b60a04 --- /dev/null +++ b/CodeHub.iOS/Services/NetworkActivityService.cs @@ -0,0 +1,18 @@ +using CodeHub.Core.Services; +using CodeHub.iOS.Utilities; + +namespace CodeHub.iOS.Services +{ + public class NetworkActivityService : INetworkActivityService + { + public void PopNetworkActive() + { + NetworkActivity.PopNetworkActive(); + } + + public void PushNetworkActive() + { + NetworkActivity.PushNetworkActive(); + } + } +} diff --git a/CodeHub.iOS/Services/PushNotificationsService.cs b/CodeHub.iOS/Services/PushNotificationsService.cs index dee65777..33faf3a3 100644 --- a/CodeHub.iOS/Services/PushNotificationsService.cs +++ b/CodeHub.iOS/Services/PushNotificationsService.cs @@ -70,6 +70,7 @@ public async Task Deregister() { new KeyValuePair("token", del.DeviceToken), new KeyValuePair("oauth", user.OAuth), + new KeyValuePair("user", user.Username), new KeyValuePair("domain", "https://api.github.com"), }); diff --git a/CodeHub.iOS/Setup.cs b/CodeHub.iOS/Setup.cs index beb2af32..34a3e5fd 100755 --- a/CodeHub.iOS/Setup.cs +++ b/CodeHub.iOS/Setup.cs @@ -73,16 +73,6 @@ protected override IMvxApplication CreateApp() .AsInterfaces() .RegisterAsLazySingleton(); - this.CreatableTypes() - .EndingWith("Factory") - .AsInterfaces() - .RegisterAsDynamic(); - - this.CreatableTypes(typeof(Core.App).Assembly) - .EndingWith("Factory") - .AsInterfaces() - .RegisterAsDynamic(); - return new Core.App(); } diff --git a/CodeHub.iOS/TableViewCells/FeedbackCellView.cs b/CodeHub.iOS/TableViewCells/FeedbackCellView.cs new file mode 100644 index 00000000..d06591d4 --- /dev/null +++ b/CodeHub.iOS/TableViewCells/FeedbackCellView.cs @@ -0,0 +1,50 @@ +using System; +using Foundation; +using UIKit; +using CodeHub.Core.ViewModels.App; +using ReactiveUI; +using System.Reactive.Linq; +using SDWebImage; + +namespace CodeHub.iOS.TableViewCells +{ + public partial class FeedbackCellView : ReactiveTableViewCell + { + public static readonly UINib Nib = UINib.FromName("FeedbackCellView", NSBundle.MainBundle); + public static readonly NSString Key = new NSString("FeedbackCellView"); + private static nfloat DefaultContentConstraintSize = 0.0f; + + public FeedbackCellView(IntPtr handle) + : base(handle) + { + } + + public override void AwakeFromNib() + { + base.AwakeFromNib(); + + MainImageView.Layer.MasksToBounds = true; + MainImageView.Layer.CornerRadius = MainImageView.Frame.Height / 2f; + ContentView.Opaque = true; + + SeparatorInset = new UIEdgeInsets(0, TitleLabel.Frame.Left, 0, 0); + TitleLabel.TextColor = Theme.CurrentTheme.MainTitleColor; + DetailsLabel.TextColor = UIColor.Gray; + DefaultContentConstraintSize = DetailsConstraint.Constant; + + this.WhenAnyValue(x => x.ViewModel) + .Where(x => x != null) + .Subscribe(x => { + TitleLabel.Text = x.Title; + DetailsLabel.Text = "Created " + x.CreatedString; + DetailsConstraint.Constant = string.IsNullOrEmpty(DetailsLabel.Text) ? 0f : DefaultContentConstraintSize; + + if (string.IsNullOrEmpty(x.ImageUrl)) + MainImageView.Image = Images.LoginUserUnknown; + else + MainImageView.SetImage(new NSUrl(x.ImageUrl), Images.LoginUserUnknown); + }); + } + } +} + diff --git a/CodeHub.iOS/TableViewCells/FeedbackCellView.designer.cs b/CodeHub.iOS/TableViewCells/FeedbackCellView.designer.cs new file mode 100644 index 00000000..3f6238d7 --- /dev/null +++ b/CodeHub.iOS/TableViewCells/FeedbackCellView.designer.cs @@ -0,0 +1,50 @@ +// WARNING +// +// This file has been generated automatically by Xamarin Studio to store outlets and +// actions made in the UI designer. If it is removed, they will be lost. +// Manual changes to this file may not be handled correctly. +// +using Foundation; +using System.CodeDom.Compiler; + +namespace CodeHub.iOS.TableViewCells +{ + [Register ("FeedbackCellView")] + partial class FeedbackCellView + { + [Outlet] + UIKit.NSLayoutConstraint DetailsConstraint { get; set; } + + [Outlet] + UIKit.UILabel DetailsLabel { get; set; } + + [Outlet] + UIKit.UIImageView MainImageView { get; set; } + + [Outlet] + UIKit.UILabel TitleLabel { get; set; } + + void ReleaseDesignerOutlets () + { + if (DetailsLabel != null) { + DetailsLabel.Dispose (); + DetailsLabel = null; + } + + if (MainImageView != null) { + MainImageView.Dispose (); + MainImageView = null; + } + + if (TitleLabel != null) { + TitleLabel.Dispose (); + TitleLabel = null; + } + + if (DetailsConstraint != null) { + DetailsConstraint.Dispose (); + DetailsConstraint = null; + } + } + } +} diff --git a/CodeHub.iOS/TableViewCells/FeedbackCellView.xib b/CodeHub.iOS/TableViewCells/FeedbackCellView.xib new file mode 100644 index 00000000..3d27797a --- /dev/null +++ b/CodeHub.iOS/TableViewCells/FeedbackCellView.xib @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeHub.iOS/TableViewCells/GistCellView.cs b/CodeHub.iOS/TableViewCells/GistCellView.cs index 1addd65e..d2ad8e39 100644 --- a/CodeHub.iOS/TableViewCells/GistCellView.cs +++ b/CodeHub.iOS/TableViewCells/GistCellView.cs @@ -1,23 +1,19 @@ using System; using Foundation; using UIKit; -using CodeHub.Core.Utilities; -using ObjCRuntime; -using Humanizer; +using ReactiveUI; +using CodeHub.Core.ViewModels.Gists; +using System.Reactive.Linq; namespace CodeHub.iOS.TableViewCells { - public partial class GistCellView : UITableViewCell + public partial class GistCellView : ReactiveTableViewCell { public static readonly UINib Nib = UINib.FromName("GistCellView", NSBundle.MainBundle); public static readonly NSString Key = new NSString("GistCellView"); private static nfloat DefaultContentConstraintSize = 0.0f; - public GistCellView() - { - } - - public GistCellView(IntPtr handle) + public GistCellView(IntPtr handle) : base(handle) { } @@ -34,22 +30,17 @@ public override void AwakeFromNib() TimeLabel.TextColor = Theme.CurrentTheme.MainSubtitleColor; ContentLabel.TextColor = Theme.CurrentTheme.MainTextColor; DefaultContentConstraintSize = ContentConstraint.Constant; - } - public static GistCellView Create() - { - var cell = new GistCellView(); - var views = NSBundle.MainBundle.LoadNib("GistCellView", cell, null); - return Runtime.GetNSObject( views.ValueAt(0) ) as GistCellView; - } - - public void Set(string title, string description, DateTimeOffset time, GitHubAvatar avatar) - { - TitleLabel.Text = title; - ContentLabel.Text = description; - TimeLabel.Text = time.Humanize(); - ContentConstraint.Constant = string.IsNullOrEmpty(description) ? 0f : DefaultContentConstraintSize; - MainImageView.SetAvatar(avatar); + this.WhenAnyValue(x => x.ViewModel) + .Where(x => x != null) + .Subscribe(x => + { + TitleLabel.Text = x.Title; + ContentLabel.Text = x.Description; + TimeLabel.Text = x.UpdatedString; + ContentConstraint.Constant = string.IsNullOrEmpty(x.Description) ? 0f : DefaultContentConstraintSize; + MainImageView.SetAvatar(x.Avatar); + }); } protected override void Dispose(bool disposing) diff --git a/CodeHub.iOS/TableViewCells/IssueCellView.cs b/CodeHub.iOS/TableViewCells/IssueCellView.cs index 7ed321cf..42440057 100755 --- a/CodeHub.iOS/TableViewCells/IssueCellView.cs +++ b/CodeHub.iOS/TableViewCells/IssueCellView.cs @@ -25,7 +25,7 @@ public static IssueCellView Create() { cell.Caption.TextColor = Theme.CurrentTheme.MainTitleColor; cell.Number.TextColor = Theme.CurrentTheme.MainTitleColor; - cell.AddSubview(new SeperatorIssues {Frame = new CGRect(65f, 5f, 1f, cell.Frame.Height - 10f)}); + cell.ContentView.AddSubview(new SeperatorIssues {Frame = new CGRect(65f, 5f, 1f, cell.Frame.Height - 10f)}); cell.Image1.Image = Octicon.Gear.ToImage(12); cell.Image2.Image = Octicon.CommentDiscussion.ToImage(12); cell.Image3.Image = Octicon.Person.ToImage(12); @@ -33,7 +33,6 @@ public static IssueCellView Create() cell.SeparatorInset = new UIEdgeInsets(0, 0, 0, 0); } - //Create the icons return cell; } @@ -63,47 +62,7 @@ public void Bind(string title, string status, string priority, string assigned, Label4.Text = lastUpdated.UtcDateTime.Humanize(); Number.Text = "#" + id; IssueType.Text = kind; - - /* - if (model.CommentCount > 0) - { - var ms = model.CommentCount.ToString (); - var ssize = ms.MonoStringLength(CountFont); - var boxWidth = Math.Min (22 + ssize, 18); - AddSubview(new CounterView(model.CommentCount) { Frame = new RectangleF(Bounds.Width-30-boxWidth, Bounds.Height / 2 - 8, boxWidth, 16) }); - } - */ } -// -// static readonly UIFont CountFont = UIFont.BoldSystemFontOfSize (13); -// -// private class CounterView : UIView -// { -// private readonly int _counter; -// public CounterView(int counter) -// : base () -// { -// _counter = counter; -// BackgroundColor = UIColor.Clear; -// } -// -// public override void Draw(RectangleF rect) -// { -// if (_counter > 0){ -// var ctx = UIGraphics.GetCurrentContext (); -// var ms = _counter.ToString (); -// -// var crect = Bounds; -// -// UIColor.Gray.SetFill (); -// GraphicsUtil.FillRoundedRect (ctx, crect, 3); -// UIColor.White.SetColor (); -// crect.X += 5; -// DrawString (ms, crect, CountFont); -// } -// base.Draw(rect); -// } -// } private class SeperatorIssues : UIView { @@ -121,8 +80,6 @@ public override void Draw(CGRect rect) base.Draw(rect); var context = UIGraphics.GetCurrentContext(); - //context.BeginPath(); - //context.ClipToRect(new RectangleF(63f, 0f, 3f, rect.Height)); using (var cs = CGColorSpace.CreateDeviceRGB ()) { using (var gradient = new CGGradient (cs, new nfloat [] { 1f, 1f, 1f, 1.0f, diff --git a/CodeHub.iOS/TableViewCells/NewsCellView.cs b/CodeHub.iOS/TableViewCells/NewsCellView.cs index cd264d70..2a582cba 100644 --- a/CodeHub.iOS/TableViewCells/NewsCellView.cs +++ b/CodeHub.iOS/TableViewCells/NewsCellView.cs @@ -23,7 +23,7 @@ public NewsCellView(IntPtr handle) : base(handle) { } - class LabelDelegate : MonoTouch.TTTAttributedLabel.TTTAttributedLabelDelegate { + class LabelDelegate : Xamarin.TTTAttributedLabel.TTTAttributedLabelDelegate { private readonly List _links; private readonly Action _webLinkClicked; @@ -34,7 +34,7 @@ public LabelDelegate(List links, Action webLinkClicked) _webLinkClicked = webLinkClicked; } - public override void DidSelectLinkWithURL (MonoTouch.TTTAttributedLabel.TTTAttributedLabel label, NSUrl url) + public override void DidSelectLinkWithURL (Xamarin.TTTAttributedLabel.TTTAttributedLabel label, NSUrl url) { try { @@ -88,21 +88,26 @@ public void Set(Uri imgUrl, string time, UIImage actionImage, Body.AddLinkToURL(new NSUrl(b.Id.ToString()), b.Range); AdjustableConstraint.Constant = Body.Hidden ? 0f : 6f; + + AccessibilityHint = header.Value; } public static NewsCellView Create() { + var linkAttributes = new NSMutableDictionary(); + linkAttributes.Add(UIStringAttributeKey.UnderlineStyle, NSNumber.FromBoolean(true)); + var cell = (NewsCellView)Nib.Instantiate(null, null)[0]; + cell.SeparatorInset = new UIEdgeInsets(0, 48f, 0, 0); cell.Body.LinkAttributes = new NSDictionary(); - cell.Body.ActiveLinkAttributes = new NSMutableDictionary(); - cell.Body.ActiveLinkAttributes[CoreText.CTStringAttributeKey.UnderlineStyle] = NSNumber.FromBoolean(true); + cell.Body.ActiveLinkAttributes = linkAttributes; cell.Body.Lines = 4; cell.Body.LineBreakMode = UILineBreakMode.TailTruncation; + cell.Body.AccessibilityElementsHidden = true; cell.Header.LinkAttributes = new NSDictionary(); - cell.Header.ActiveLinkAttributes = new NSMutableDictionary(); - cell.Header.ActiveLinkAttributes[CoreText.CTStringAttributeKey.UnderlineStyle] = NSNumber.FromBoolean(true); + cell.Header.ActiveLinkAttributes = linkAttributes; cell.Header.Lines = 2; cell.Header.LineBreakMode = UILineBreakMode.TailTruncation; diff --git a/CodeHub.iOS/TableViewCells/NewsCellView.designer.cs b/CodeHub.iOS/TableViewCells/NewsCellView.designer.cs index 9f31e077..d098fffa 100644 --- a/CodeHub.iOS/TableViewCells/NewsCellView.designer.cs +++ b/CodeHub.iOS/TableViewCells/NewsCellView.designer.cs @@ -19,10 +19,10 @@ partial class NewsCellView UIKit.NSLayoutConstraint AdjustableConstraint { get; set; } [Outlet] - MonoTouch.TTTAttributedLabel.TTTAttributedLabel Body { get; set; } + Xamarin.TTTAttributedLabel.TTTAttributedLabel Body { get; set; } [Outlet] - MonoTouch.TTTAttributedLabel.TTTAttributedLabel Header { get; set; } + Xamarin.TTTAttributedLabel.TTTAttributedLabel Header { get; set; } [Outlet] UIKit.UIImageView Image { get; set; } diff --git a/CodeHub.iOS/TableViewCells/NewsCellView.xib b/CodeHub.iOS/TableViewCells/NewsCellView.xib index 30bc3993..f99e799c 100644 --- a/CodeHub.iOS/TableViewCells/NewsCellView.xib +++ b/CodeHub.iOS/TableViewCells/NewsCellView.xib @@ -1,8 +1,7 @@ - + - - + @@ -12,7 +11,7 @@ - + diff --git a/CodeHub.iOS/TableViewCells/RepositoryCellView.cs b/CodeHub.iOS/TableViewCells/RepositoryCellView.cs index d6c73cdc..0a18b38c 100755 --- a/CodeHub.iOS/TableViewCells/RepositoryCellView.cs +++ b/CodeHub.iOS/TableViewCells/RepositoryCellView.cs @@ -1,83 +1,66 @@ -using System; -using Foundation; -using ObjCRuntime; -using SDWebImage; -using CodeHub.iOS; -using MvvmCross.Binding.iOS.Views; -using CodeHub.Core.Utilities; -using UIKit; - +using System; +using Foundation; +using UIKit; +using CodeHub.Core.ViewModels.Repositories; +using ReactiveUI; +using System.Reactive.Linq; + namespace CodeHub.iOS.TableViewCells { - public partial class RepositoryCellView : MvxTableViewCell - { - public static NSString Key = new NSString("RepositoryCellView"); + public partial class RepositoryCellView : ReactiveTableViewCell + { + public static readonly UINib Nib = UINib.FromName("RepositoryCellView", NSBundle.MainBundle); + public static NSString Key = new NSString("RepositoryCellView"); + private static nfloat DefaultConstraintSize = 0.0f; - public static RepositoryCellView Create() + public RepositoryCellView(IntPtr handle) + : base(handle) { - var cell = new RepositoryCellView(); - var views = NSBundle.MainBundle.LoadNib("RepositoryCellView", cell, null); - cell = Runtime.GetNSObject( views.ValueAt(0) ) as RepositoryCellView; + SeparatorInset = new UIEdgeInsets(0, 56f, 0, 0); + } - if (cell != null) - { - cell.SeparatorInset = new UIEdgeInsets(0, 56f, 0, 0); - - cell.Caption.TextColor = Theme.CurrentTheme.MainTitleColor; - cell.Description.TextColor = Theme.CurrentTheme.MainTextColor; - - cell.Image1.Image = Octicon.Star.ToImage(12); - cell.Image3.Image = Octicon.RepoForked.ToImage(12); - cell.UserImage.Image = Octicon.Person.ToImage(12); + public override void AwakeFromNib() + { + base.AwakeFromNib(); - cell.BigImage.Layer.MasksToBounds = true; - cell.BigImage.Layer.CornerRadius = cell.BigImage.Bounds.Height / 2f; - } + CaptionLabel.TextColor = Theme.CurrentTheme.MainTitleColor; + ContentLabel.TextColor = Theme.CurrentTheme.MainTextColor; - //Create the icons - return cell; - } - - public override NSString ReuseIdentifier - { - get - { - return Key; - } - } + FollowersImageVIew.TintColor = FollowersLabel.TextColor; + ForksImageView.TintColor = ForksLabel.TextColor; + UserImageView.TintColor = UserLabel.TextColor; - public RepositoryCellView() - { - } + FollowersImageVIew.Image = Octicon.Star.ToImage(FollowersImageVIew.Frame.Height); + ForksImageView.Image = Octicon.RepoForked.ToImage(ForksImageView.Frame.Height); + UserImageView.Image = Octicon.Person.ToImage(UserImageView.Frame.Height); - public RepositoryCellView(IntPtr handle) - : base(handle) - { + OwnerImageView.Layer.CornerRadius = OwnerImageView.Bounds.Height / 2f; + OwnerImageView.Layer.MasksToBounds = true; + OwnerImageView.ContentMode = UIViewContentMode.ScaleAspectFill; + + DefaultConstraintSize = ContentConstraint.Constant; + + this.WhenAnyValue(x => x.ViewModel) + .Where(x => x != null) + .Subscribe(x => + { + CaptionLabel.Text = x.Name; + FollowersLabel.Text = x.Stars; + ForksLabel.Text = x.Forks; + ContentLabel.Hidden = string.IsNullOrEmpty(x.Description); + ContentLabel.Text = x.Description ?? string.Empty; + UserLabel.Hidden = !x.ShowOwner || string.IsNullOrEmpty(x.Owner); + UserImageView.Hidden = UserLabel.Hidden; + UserLabel.Text = x.Owner ?? string.Empty; + ContentConstraint.Constant = string.IsNullOrEmpty(ContentLabel.Text) ? 0f : DefaultConstraintSize; + OwnerImageView.SetAvatar(x.Avatar); + }); } - public void Bind(string name, string name2, string name3, string description, string repoOwner, GitHubAvatar imageUrl) + protected override void Dispose(bool disposing) { - Caption.Text = name; - Label1.Text = name2; - Label3.Text = name3; - Description.Hidden = description == null; - Description.Text = description ?? string.Empty; - - RepoName.Hidden = repoOwner == null; - UserImage.Hidden = RepoName.Hidden; - RepoName.Text = repoOwner ?? string.Empty; - - BigImage.Image = Images.Avatar; - - try - { - var uri = imageUrl.ToUri(64)?.AbsoluteUri; - if (uri != null) - BigImage.SetImage(new NSUrl(uri), Images.Avatar); - } - catch - { - } + ReleaseDesignerOutlets(); + base.Dispose(disposing); } } } diff --git a/CodeHub.iOS/TableViewCells/RepositoryCellView.designer.cs b/CodeHub.iOS/TableViewCells/RepositoryCellView.designer.cs index 0e6f62c9..2b9f6013 100755 --- a/CodeHub.iOS/TableViewCells/RepositoryCellView.designer.cs +++ b/CodeHub.iOS/TableViewCells/RepositoryCellView.designer.cs @@ -1,89 +1,107 @@ // WARNING // // This file has been generated automatically by Xamarin Studio to store outlets and -// actions made in the Xcode designer. If it is removed, they will be lost. +// actions made in the UI designer. If it is removed, they will be lost. // Manual changes to this file may not be handled correctly. -// - -using Foundation; - +// +using Foundation; +using System.CodeDom.Compiler; + namespace CodeHub.iOS.TableViewCells { - [Register ("RepositoryCellView")] + [Register("RepositoryCellView")] partial class RepositoryCellView { [Outlet] - UIKit.UIImageView BigImage { get; set; } + UIKit.UILabel CaptionLabel { get; set; } + + [Outlet] + UIKit.NSLayoutConstraint ContentConstraint { get; set; } [Outlet] - UIKit.UILabel Caption { get; set; } + UIKit.UILabel ContentLabel { get; set; } [Outlet] - UIKit.UILabel Description { get; set; } + UIKit.UIImageView FollowersImageVIew { get; set; } [Outlet] - UIKit.UIImageView Image1 { get; set; } + UIKit.UILabel FollowersLabel { get; set; } [Outlet] - UIKit.UIImageView Image3 { get; set; } + UIKit.UIImageView ForksImageView { get; set; } [Outlet] - UIKit.UILabel Label1 { get; set; } + UIKit.UILabel ForksLabel { get; set; } [Outlet] - UIKit.UILabel Label3 { get; set; } + UIKit.UIImageView OwnerImageView { get; set; } [Outlet] - UIKit.UILabel RepoName { get; set; } + UIKit.UIImageView UserImageView { get; set; } [Outlet] - UIKit.UIImageView UserImage { get; set; } - - void ReleaseDesignerOutlets () + UIKit.UILabel UserLabel { get; set; } + + void ReleaseDesignerOutlets() { - if (BigImage != null) { - BigImage.Dispose (); - BigImage = null; + if (CaptionLabel != null) + { + CaptionLabel.Dispose(); + CaptionLabel = null; + } + + if (ContentLabel != null) + { + ContentLabel.Dispose(); + ContentLabel = null; } - if (Caption != null) { - Caption.Dispose (); - Caption = null; + if (FollowersImageVIew != null) + { + FollowersImageVIew.Dispose(); + FollowersImageVIew = null; } - if (Description != null) { - Description.Dispose (); - Description = null; + if (FollowersLabel != null) + { + FollowersLabel.Dispose(); + FollowersLabel = null; } - if (Image1 != null) { - Image1.Dispose (); - Image1 = null; + if (ForksImageView != null) + { + ForksImageView.Dispose(); + ForksImageView = null; } - if (Image3 != null) { - Image3.Dispose (); - Image3 = null; + if (ForksLabel != null) + { + ForksLabel.Dispose(); + ForksLabel = null; } - if (Label1 != null) { - Label1.Dispose (); - Label1 = null; + if (OwnerImageView != null) + { + OwnerImageView.Dispose(); + OwnerImageView = null; } - if (Label3 != null) { - Label3.Dispose (); - Label3 = null; + if (UserImageView != null) + { + UserImageView.Dispose(); + UserImageView = null; } - if (RepoName != null) { - RepoName.Dispose (); - RepoName = null; + if (UserLabel != null) + { + UserLabel.Dispose(); + UserLabel = null; } - if (UserImage != null) { - UserImage.Dispose (); - UserImage = null; + if (ContentConstraint != null) + { + ContentConstraint.Dispose(); + ContentConstraint = null; } } } diff --git a/CodeHub.iOS/TableViewCells/RepositoryCellView.xib b/CodeHub.iOS/TableViewCells/RepositoryCellView.xib index dbf1c948..241860be 100755 --- a/CodeHub.iOS/TableViewCells/RepositoryCellView.xib +++ b/CodeHub.iOS/TableViewCells/RepositoryCellView.xib @@ -1,135 +1,136 @@ - - + + + - - + + - - - - - - - - - - - - - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + - + diff --git a/CodeHub.iOS/TableViewCells/UserTableViewCell.cs b/CodeHub.iOS/TableViewCells/UserTableViewCell.cs new file mode 100644 index 00000000..32ac247e --- /dev/null +++ b/CodeHub.iOS/TableViewCells/UserTableViewCell.cs @@ -0,0 +1,42 @@ +using System; +using Foundation; +using UIKit; +using CodeHub.Core.ViewModels.Users; +using ReactiveUI; +using CoreGraphics; + +namespace CodeHub.iOS.TableViewCells +{ + public class UserTableViewCell : ReactiveTableViewCell + { + public static readonly NSString Key = new NSString("UserTableViewCell"); + private const float ImageSpacing = 10f; + private static readonly CGRect ImageFrame = new CGRect(ImageSpacing, 6f, 32, 32); + + public UserTableViewCell(IntPtr handle) + : base(handle) + { + ImageView.Layer.CornerRadius = ImageFrame.Height / 2f; + ImageView.Layer.MasksToBounds = true; + ImageView.ContentMode = UIViewContentMode.ScaleAspectFill; + ContentView.Opaque = true; + + SeparatorInset = new UIEdgeInsets(0, ImageFrame.Right + ImageSpacing, 0, 0); + + this.WhenActivated(d => + { + d(this.WhenAnyValue(x => x.ViewModel.Login).Subscribe(x => TextLabel.Text = x)); + d(this.WhenAnyValue(x => x.ViewModel.Avatar).Subscribe(x => ImageView.SetAvatar(x))); + }); + } + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + ImageView.Frame = ImageFrame; + TextLabel.Frame = new CGRect(ImageFrame.Right + ImageSpacing, TextLabel.Frame.Y, + TextLabel.Frame.Width, TextLabel.Frame.Height); + } + } +} + diff --git a/CodeHub.iOS/TableViewSources/DialogTableViewSource.cs b/CodeHub.iOS/TableViewSources/DialogTableViewSource.cs new file mode 100644 index 00000000..37ad77be --- /dev/null +++ b/CodeHub.iOS/TableViewSources/DialogTableViewSource.cs @@ -0,0 +1,118 @@ +using UIKit; +using Foundation; +using System; +using System.Reactive.Subjects; +using CoreGraphics; +using CodeHub.iOS.DialogElements; +using System.Reactive.Linq; + +namespace CodeHub.iOS.TableViewSources +{ + public class DialogTableViewSource : UITableViewSource + { + private readonly RootElement _root; + private readonly Subject _scrolledSubject = new Subject(); + private readonly Subject _selectedSubject = new Subject(); + + public IObservable ScrolledObservable { get { return _scrolledSubject.AsObservable(); } } + + public IObservable SelectedObservable { get { return _selectedSubject.AsObservable(); } } + + public RootElement Root + { + get { return _root; } + } + + #if DEBUG + ~DialogTableViewSource() + { + Console.WriteLine("Goodbye DialogTableViewSource"); + } + #endif + + public DialogTableViewSource(UITableView container) + { + container.RowHeight = UITableView.AutomaticDimension; + _root = new RootElement(container); + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + return Root[(int)section].Count; + } + + public override nint NumberOfSections(UITableView tableView) + { + return Root.Count; + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + return Root[(int)section].Header; + } + + public override string TitleForFooter(UITableView tableView, nint section) + { + return Root[(int)section].Footer; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + var section = Root[indexPath.Section]; + var element = section[indexPath.Row]; + return element.GetCell(tableView); + } + + public override void RowDeselected(UITableView tableView, NSIndexPath indexPath) + { + var section = Root[indexPath.Section]; + var element = section[indexPath.Row]; + element.Deselected(tableView, indexPath); + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + var section = Root[indexPath.Section]; + var element = section[indexPath.Row]; + element.Selected(tableView, indexPath); + _selectedSubject.OnNext(element); + } + + public override UIView GetViewForHeader(UITableView tableView, nint section) + { + var sectionElement = Root[(int)section]; + return sectionElement.HeaderView; + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + var sectionElement = Root[(int)section]; + return sectionElement.HeaderView == null ? -1 : sectionElement.HeaderView.Frame.Height; + } + + public override UIView GetViewForFooter(UITableView tableView, nint section) + { + var sectionElement = Root[(int)section]; + return sectionElement.FooterView; + } + + public override nfloat GetHeightForFooter(UITableView tableView, nint section) + { + var sectionElement = Root[(int)section]; + return sectionElement.FooterView == null ? -1 : sectionElement.FooterView.Frame.Height; + } + + public override void Scrolled(UIScrollView scrollView) + { + _scrolledSubject.OnNext(Root.TableView.ContentOffset); + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + var section = Root[indexPath.Section]; + var element = section[indexPath.Row]; + var sizable = element as IElementSizing; + return sizable == null ? tableView.RowHeight : sizable.GetHeight(tableView, indexPath); + } + } +} diff --git a/CodeHub.iOS/TableViewSources/FeedbackTableViewSource.cs b/CodeHub.iOS/TableViewSources/FeedbackTableViewSource.cs new file mode 100644 index 00000000..eb3f7803 --- /dev/null +++ b/CodeHub.iOS/TableViewSources/FeedbackTableViewSource.cs @@ -0,0 +1,17 @@ +using ReactiveUI; +using CodeHub.iOS.TableViewCells; +using CodeHub.Core.ViewModels.App; +using UIKit; + +namespace CodeHub.iOS.TableViewSources +{ + public class FeedbackTableViewSource : ReactiveTableViewSource + { + public FeedbackTableViewSource(UITableView tableView, IReactiveNotifyCollectionChanged collection) + : base(tableView, collection, FeedbackCellView.Key, UITableView.AutomaticDimension, 69.0f) + { + tableView.RegisterNibForCellReuse(FeedbackCellView.Nib, FeedbackCellView.Key); + } + } +} + diff --git a/CodeHub.iOS/TableViewSources/GistTableViewSource.cs b/CodeHub.iOS/TableViewSources/GistTableViewSource.cs new file mode 100644 index 00000000..b11fdf4e --- /dev/null +++ b/CodeHub.iOS/TableViewSources/GistTableViewSource.cs @@ -0,0 +1,17 @@ +using ReactiveUI; +using CodeHub.Core.ViewModels.Gists; +using UIKit; +using CodeHub.iOS.TableViewCells; + +namespace CodeHub.iOS.TableViewSources +{ + public class GistTableViewSource : ReactiveTableViewSource + { + public GistTableViewSource(UITableView tableView, IReactiveNotifyCollectionChanged collection) + : base(tableView, collection, GistCellView.Key, UITableView.AutomaticDimension, 60f) + { + tableView.RegisterNibForCellReuse(GistCellView.Nib, GistCellView.Key); + } + } +} + diff --git a/CodeHub.iOS/TableViewSources/ReactiveTableViewSource.cs b/CodeHub.iOS/TableViewSources/ReactiveTableViewSource.cs new file mode 100644 index 00000000..f33ba729 --- /dev/null +++ b/CodeHub.iOS/TableViewSources/ReactiveTableViewSource.cs @@ -0,0 +1,83 @@ +using System; +using UIKit; +using System.Reactive.Subjects; +using System.Reactive; +using CodeHub.Core.ViewModels; +using ReactiveUI; +using System.Reactive.Linq; +using CoreGraphics; + +namespace CodeHub.iOS.TableViewSources +{ + public abstract class ReactiveTableViewSource : ReactiveUI.ReactiveTableViewSource, IInformsEnd + { + private readonly Subject _requestMoreSubject = new Subject(); + private readonly Subject _scrollSubject = new Subject(); + + public IObservable DidScroll + { + get { return _scrollSubject.AsObservable(); } + } + + public IObservable RequestMore + { + get { return _requestMoreSubject; } + } + + public override void Scrolled(UIScrollView scrollView) + { + _scrollSubject.OnNext(scrollView.ContentOffset); + } + + ~ReactiveTableViewSource() + { + Console.WriteLine("Destorying " + GetType().Name); + } + + protected ReactiveTableViewSource(UITableView tableView, nfloat height, nfloat? heightHint = null) + : base(tableView) + { + tableView.RowHeight = height; + tableView.EstimatedRowHeight = heightHint ?? tableView.EstimatedRowHeight; + } + + protected ReactiveTableViewSource(UITableView tableView, IReactiveNotifyCollectionChanged collection, + Foundation.NSString cellKey, nfloat height, nfloat? heightHint = null, Action initializeCellAction = null) + : base(tableView, collection, cellKey, (float)height, initializeCellAction) + { + tableView.RowHeight = height; + tableView.EstimatedRowHeight = heightHint ?? tableView.EstimatedRowHeight; + } + + public override void WillDisplay(UITableView tableView, UITableViewCell cell, Foundation.NSIndexPath indexPath) + { + if (indexPath.Section == (NumberOfSections(tableView) - 1) && + indexPath.Row == (RowsInSection(tableView, indexPath.Section) - 1)) + { + // We need to skip an event loop to stay out of trouble + BeginInvokeOnMainThread(() => _requestMoreSubject.OnNext(Unit.Default)); + } + } + + public override void RowSelected(UITableView tableView, Foundation.NSIndexPath indexPath) + { + var item = ItemAt(indexPath) as ICanGoToViewModel; + item?.GoToCommand.ExecuteNow(); + + base.RowSelected(tableView, indexPath); + } + + protected override void Dispose(bool disposing) + { + _requestMoreSubject.Dispose(); + _scrollSubject.Dispose(); + base.Dispose(disposing); + } + } + + public interface IInformsEnd + { + IObservable RequestMore { get; } + } +} + diff --git a/CodeHub.iOS/TableViewSources/RepositoryTableViewSource.cs b/CodeHub.iOS/TableViewSources/RepositoryTableViewSource.cs new file mode 100644 index 00000000..39f8aca4 --- /dev/null +++ b/CodeHub.iOS/TableViewSources/RepositoryTableViewSource.cs @@ -0,0 +1,26 @@ +using ReactiveUI; +using CodeHub.Core.ViewModels.Repositories; +using CodeHub.iOS.TableViewCells; +using System; +using UIKit; + +namespace CodeHub.iOS.TableViewSources +{ + public class RepositoryTableViewSource : ReactiveTableViewSource + { + private readonly static nfloat _estimatedHeight = 100.0f; + + public RepositoryTableViewSource(UITableView tableView, IReactiveNotifyCollectionChanged collection) + : base(tableView, collection, RepositoryCellView.Key, UITableView.AutomaticDimension, _estimatedHeight) + { + tableView.RegisterNibForCellReuse(RepositoryCellView.Nib, RepositoryCellView.Key); + } + + public RepositoryTableViewSource(UITableView tableView) + : base(tableView, UITableView.AutomaticDimension, _estimatedHeight) + { + tableView.RegisterNibForCellReuse(RepositoryCellView.Nib, RepositoryCellView.Key); + } + } +} + diff --git a/CodeHub.iOS/TableViewSources/UserTableViewSource.cs b/CodeHub.iOS/TableViewSources/UserTableViewSource.cs new file mode 100644 index 00000000..c5d8e40d --- /dev/null +++ b/CodeHub.iOS/TableViewSources/UserTableViewSource.cs @@ -0,0 +1,16 @@ +using ReactiveUI; +using CodeHub.Core.ViewModels.Users; +using CodeHub.iOS.TableViewCells; + +namespace CodeHub.iOS.TableViewSources +{ + public class UserTableViewSource : ReactiveTableViewSource + { + public UserTableViewSource(UIKit.UITableView tableView, IReactiveNotifyCollectionChanged collection) + : base(tableView, collection, UserTableViewCell.Key, 44) + { + tableView.RegisterClassForCellReuse(typeof(UserTableViewCell), UserTableViewCell.Key); + } + } +} + diff --git a/CodeHub.iOS/Theme.cs b/CodeHub.iOS/Theme.cs index 7db6ce61..0a986597 100755 --- a/CodeHub.iOS/Theme.cs +++ b/CodeHub.iOS/Theme.cs @@ -1,5 +1,6 @@ using UIKit; using CodeHub.iOS.Views; +using System; namespace CodeHub.iOS { @@ -32,11 +33,17 @@ public static void Setup() UINavigationBar.Appearance.BarTintColor = primaryColor; UINavigationBar.Appearance.SetTitleTextAttributes(new UITextAttributes { TextColor = UIColor.White, Font = UIFont.SystemFontOfSize(18f) }); CodeHub.iOS.Utilities.Hud.BackgroundTint = UIColor.FromRGBA(228, 228, 228, 128); + CodeHub.iOS.Services.AlertDialogService.BackgroundTint = UIColor.FromRGBA(228, 228, 228, 128); + UINavigationBar.Appearance.BackIndicatorImage = Images.Buttons.BackButton; UINavigationBar.Appearance.BackIndicatorTransitionMaskImage = Images.Buttons.BackButton; - UIBarButtonItem.Appearance.SetBackButtonTitlePositionAdjustment(new UIOffset(0, -System.nfloat.MaxValue), UIBarMetrics.LandscapePhone); - UIBarButtonItem.Appearance.SetBackButtonTitlePositionAdjustment(new UIOffset(0, -System.nfloat.MaxValue), UIBarMetrics.Default); + var version = new Version(UIDevice.CurrentDevice.SystemVersion); + if (version.Major < 11) + { + UIBarButtonItem.Appearance.SetBackButtonTitlePositionAdjustment(new UIOffset(0, -System.nfloat.MaxValue), UIBarMetrics.LandscapePhone); + UIBarButtonItem.Appearance.SetBackButtonTitlePositionAdjustment(new UIOffset(0, -System.nfloat.MaxValue), UIBarMetrics.Default); + } UISegmentedControl.Appearance.TintColor = UIColor.FromRGB(110, 110, 117); UISegmentedControl.AppearanceWhenContainedIn(typeof(UINavigationBar)).TintColor = UIColor.White; diff --git a/CodeHub.iOS/TouchViewPresenter.cs b/CodeHub.iOS/TouchViewPresenter.cs index 4708253b..24a5ac4d 100755 --- a/CodeHub.iOS/TouchViewPresenter.cs +++ b/CodeHub.iOS/TouchViewPresenter.cs @@ -1,73 +1,68 @@ -using System; -using MvvmCross.Platform; -using MvvmCross.Core.ViewModels; -using UIKit; -using CodeHub.Core; -using MonoTouch.SlideoutNavigation; -using MvvmCross.iOS.Views.Presenters; -using MvvmCross.iOS.Views; - -namespace CodeHub.iOS -{ - public class IosViewPresenter : MvxBaseIosViewPresenter - { - private readonly UIWindow _window; - private UINavigationController _generalNavigationController; - - public SlideoutNavigationController SlideoutNavigationController { get; set; } - - public IosViewPresenter(UIWindow window) - { - _window = window; - } - - public override void ChangePresentation(MvxPresentationHint hint) - { - var closeHint = hint as MvxClosePresentationHint; - if (closeHint != null) - { - for (int i = _generalNavigationController.ViewControllers.Length - 1; i >= 1; i--) - { - var vc = _generalNavigationController.ViewControllers[i]; - var touchView = vc as IMvxIosView; - if (touchView != null && touchView.ViewModel == closeHint.ViewModelToClose) - { - _generalNavigationController.PopToViewController(_generalNavigationController.ViewControllers[i - 1], true); - return; - } - } - - //If it didnt trigger above it's because it was probably the root. - _generalNavigationController.PopToRootViewController(true); - } - } - - public override void Show(MvxViewModelRequest request) - { - var viewCreator = Mvx.Resolve(); - var view = viewCreator.CreateView(request); - var uiView = view as UIViewController; - - if (uiView == null) - throw new InvalidOperationException("Asking to show a view which is not a UIViewController!"); - - if (request.PresentationValues != null && request.PresentationValues.ContainsKey(PresentationValues.SlideoutRootPresentation)) - { - var openButton = new UIBarButtonItem { Image = Images.Buttons.ThreeLinesButton }; - var mainNavigationController = new MainNavigationController(uiView, SlideoutNavigationController, openButton); - _generalNavigationController = mainNavigationController; - SlideoutNavigationController.SetMainViewController(mainNavigationController, true); - } - else - { - _generalNavigationController.PushViewController(uiView, true); - } - } - - public override bool PresentModalViewController(UIViewController viewController, bool animated) - { - _window?.GetVisibleViewController()?.PresentViewController(viewController, true, null); - return true; - } - } -} +using System; +using MvvmCross.Platform; +using MvvmCross.Core.ViewModels; +using UIKit; +using CodeHub.Core; +using MonoTouch.SlideoutNavigation; +using MvvmCross.iOS.Views.Presenters; +using MvvmCross.iOS.Views; + +namespace CodeHub.iOS +{ + public class IosViewPresenter : MvxBaseIosViewPresenter + { + private readonly UIWindow _window; + + public SlideoutNavigationController SlideoutNavigationController { get; set; } + + public IosViewPresenter(UIWindow window) + { + _window = window; + } + + public override void ChangePresentation(MvxPresentationHint hint) + { + if (hint is MvxClosePresentationHint) + { + var navController = SlideoutNavigationController.MainViewController as UINavigationController; + navController?.PopViewController(true); + } + + base.ChangePresentation(hint); + } + + public override void Show(MvxViewModelRequest request) + { + var viewCreator = Mvx.Resolve(); + var view = viewCreator.CreateView(request); + var uiView = view as UIViewController; + + if (uiView == null) + throw new InvalidOperationException("Asking to show a view which is not a UIViewController!"); + + if (uiView is IMvxModalIosView) + { + _window?.GetVisibleViewController()?.PresentViewController(uiView, true, null); + return; + } + + if (request.PresentationValues != null && request.PresentationValues.ContainsKey(PresentationValues.SlideoutRootPresentation)) + { + var openButton = new UIBarButtonItem { Image = Images.Buttons.ThreeLinesButton }; + var mainNavigationController = new MainNavigationController(uiView, SlideoutNavigationController, openButton); + SlideoutNavigationController.SetMainViewController(mainNavigationController, true); + } + else + { + var navController = SlideoutNavigationController.MainViewController as UINavigationController; + navController?.PushViewController(uiView, true); + } + } + + public override bool PresentModalViewController(UIViewController viewController, bool animated) + { + _window?.GetVisibleViewController()?.PresentViewController(viewController, true, null); + return true; + } + } +} diff --git a/CodeHub.iOS/UrlRouterProvider.cs b/CodeHub.iOS/UrlRouterProvider.cs index 4da9566f..dee45c2c 100644 --- a/CodeHub.iOS/UrlRouterProvider.cs +++ b/CodeHub.iOS/UrlRouterProvider.cs @@ -12,10 +12,10 @@ namespace CodeHub.iOS public static class UrlRouteProvider { private static Route[] Routes = { - new Route("^gist.github.com/$", typeof(CodeHub.Core.ViewModels.Gists.UserGistsViewModel)), - new Route("^gist.github.com/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Gists.UserGistsViewModel)), + //new Route("^gist.github.com/$", typeof(CodeHub.Core.ViewModels.Gists.UserGistsViewModel)), + //new Route("^gist.github.com/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Gists.UserGistsViewModel)), new Route("^gist.github.com/(?[^/]*)/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Gists.GistViewModel)), - new Route("^[^/]*/stars/$", typeof(CodeHub.Core.ViewModels.Repositories.RepositoriesStarredViewModel)), + //new Route("^[^/]*/stars/$", typeof(CodeHub.Core.ViewModels.Repositories.RepositoriesStarredViewModel)), new Route("^[^/]*/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.User.UserViewModel)), new Route("^[^/]*/(?[^/]*)/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Repositories.RepositoryViewModel)), new Route("^[^/]*/(?[^/]*)/(?[^/]*)/pulls/$", typeof(CodeHub.Core.ViewModels.PullRequests.PullRequestsViewModel)), @@ -24,7 +24,7 @@ public static class UrlRouteProvider new Route("^[^/]*/(?[^/]*)/(?[^/]*)/commits/$", typeof(CodeHub.Core.ViewModels.Changesets.CommitsViewModel)), new Route("^[^/]*/(?[^/]*)/(?[^/]*)/commits/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Changesets.ChangesetViewModel)), new Route("^[^/]*/(?[^/]*)/(?[^/]*)/issues/(?[^/]*)/$", typeof(CodeHub.Core.ViewModels.Issues.IssueViewModel)), - new Route("^[^/]*/(?[^/]*)/(?[^/]*)/tree/(?[^/]*)/(?.*)$", typeof(CodeHub.Core.ViewModels.Source.SourceTreeViewModel)), + //new Route("^[^/]*/(?[^/]*)/(?[^/]*)/tree/(?[^/]*)/(?.*)$", typeof(CodeHub.Core.ViewModels.Source.SourceTreeViewModel)), }; public static bool Handle(string path) diff --git a/CodeHub.iOS/Utilities/Hud.cs b/CodeHub.iOS/Utilities/Hud.cs index da1f42cb..13e86d87 100644 --- a/CodeHub.iOS/Utilities/Hud.cs +++ b/CodeHub.iOS/Utilities/Hud.cs @@ -65,6 +65,12 @@ public static IDisposable SubscribeStatus(this IObservable @this, string m return new CompositeDisposable(d, d2); } + + public static IDisposable Activate(this IHud hud, string text) + { + hud.Show(text); + return Disposable.Create(hud.Hide); + } } public interface IHud diff --git a/CodeHub.iOS/Utilities/NetworkActivity.cs b/CodeHub.iOS/Utilities/NetworkActivity.cs index 4c98923f..4a0364e7 100644 --- a/CodeHub.iOS/Utilities/NetworkActivity.cs +++ b/CodeHub.iOS/Utilities/NetworkActivity.cs @@ -1,4 +1,6 @@ -using UIKit; +using System; +using System.Reactive.Disposables; +using UIKit; namespace CodeHub.iOS.Utilities { @@ -37,6 +39,12 @@ public static void PopNetworkActive () MainApp.NetworkActivityIndicatorVisible = false; } } + + public static IDisposable ActivateNetwork() + { + PushNetworkActive(); + return Disposable.Create(PopNetworkActive); + } } } diff --git a/CodeHub.iOS/Utilities/ReactiveCommandExtensions.cs b/CodeHub.iOS/Utilities/ReactiveCommandExtensions.cs new file mode 100644 index 00000000..81b73e4c --- /dev/null +++ b/CodeHub.iOS/Utilities/ReactiveCommandExtensions.cs @@ -0,0 +1,44 @@ +using System; +using UIKit; +using System.Reactive.Linq; +using System.Reactive.Disposables; +using System.Reactive; + +// Analysis disable once CheckNamespace +namespace ReactiveUI +{ + public static class ReactiveCommandExtensions + { + public static IDisposable ToBarButtonItem(this IObservable @this, UIImage image, Action assignment) + { + return ToBarButtonItem(@this, () => new UIBarButtonItem { Image = image }, assignment); + } + + public static IDisposable ToBarButtonItem(this IObservable @this, UIBarButtonSystemItem systemItem, Action assignment) + { + return ToBarButtonItem(@this, () => new UIBarButtonItem(systemItem), assignment); + } + + public static IDisposable ToBarButtonItem(this IObservable @this, Func creator, Action assignment) + { + var unassignDisposable = Disposable.Create(() => assignment(null)); + IDisposable recentEventDisposable = Disposable.Empty; + + var mainDisposable = @this.Subscribe(x => { + recentEventDisposable?.Dispose(); + + var button = creator(); + var canExecuteDisposable = x.CanExecute.Subscribe(t => button.Enabled = t); + var clickDisposable = Observable.FromEventPattern(t => button.Clicked += t, t => button.Clicked -= t) + .Select(_ => Unit.Default) + .InvokeCommand(x); + + recentEventDisposable = new CompositeDisposable(clickDisposable, canExecuteDisposable); + assignment(button); + }); + + return new CompositeDisposable(mainDisposable, unassignDisposable, Disposable.Create(() => recentEventDisposable.Dispose())); + } + } +} + diff --git a/CodeHub.iOS/Utilities/ShaType.cs b/CodeHub.iOS/Utilities/ShaType.cs new file mode 100644 index 00000000..3f59ebac --- /dev/null +++ b/CodeHub.iOS/Utilities/ShaType.cs @@ -0,0 +1,9 @@ +namespace CodeHub.iOS.Utilities +{ + public enum ShaType + { + Branch, + Tag, + Hash + } +} diff --git a/CodeHub.iOS/Utilities/UIKitExtensions.cs b/CodeHub.iOS/Utilities/UIKitExtensions.cs index 8695f45a..b7000a28 100644 --- a/CodeHub.iOS/Utilities/UIKitExtensions.cs +++ b/CodeHub.iOS/Utilities/UIKitExtensions.cs @@ -2,6 +2,8 @@ using System.Reactive.Linq; using System.Reactive; using Foundation; +using System.Linq; +using System.Reactive.Disposables; // Analysis disable once CheckNamespace namespace UIKit @@ -48,6 +50,20 @@ public static IObservable GetSearchObservable(this UISearchBar @this) return Observable.FromEventPattern(t => @this.SearchButtonClicked += t, t => @this.SearchButtonClicked -= t).Select(_ => Unit.Default); } + public static void SetLoading(this UIActivityIndicatorView @this, bool loading) + { + if (loading) + @this.StartAnimating(); + else + @this.StopAnimating(); + } + + public static IDisposable DisableInteraction(this UIApplication application) + { + application.BeginIgnoringInteractionEvents(); + return Disposable.Create(application.EndIgnoringInteractionEvents); + } + public static string GetVersion(this UIApplication _) { string shortVersion = string.Empty; @@ -70,18 +86,54 @@ public static string GetVersion(this UIApplication _) return string.IsNullOrEmpty(bundleVersion) ? shortVersion : string.Format("{0} ({1})", shortVersion, bundleVersion); } + + public static bool LastItemVisible(this UITableView tableView) + { + var paths = tableView.IndexPathsForVisibleRows; + var sections = tableView.NumberOfSections(); + if (sections == 0) + return true; + + var rows = tableView.NumberOfRowsInSection(sections - 1); + if (rows == 0) + return true; + + var indexPath = NSIndexPath.FromItemSection(rows - 1, sections - 1); + return paths.Contains(indexPath); + } + + public static void PushViewController(this UIViewController This, UIViewController view) + => This.NavigationController?.PushViewController(view, true); + + public static void PresentModalViewController(this UIViewController This, UIViewController view) + { + view.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + view.NavigationItem.LeftBarButtonItem.GetClickedObservable().Subscribe( + _ => This.DismissViewController(true, null)); + + var navController = new UINavigationController(view); + This.PresentViewController(navController, true, null); + } } public static class UIFontExtensions { public static UIFont MakeBold(this UIFont font) { - return UIFont.FromDescriptor(font.FontDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Bold), font.PointSize); + var descriptor = font.FontDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Bold); + if (descriptor == null) + return font; + + return UIFont.FromDescriptor(descriptor, font.PointSize); } public static UIFont MakeItalic(this UIFont font) { - return UIFont.FromDescriptor(font.FontDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Italic), font.PointSize); + var descriptor = font.FontDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Italic); + if (descriptor == null) + return font; + + return UIFont.FromDescriptor(descriptor, font.PointSize); } } diff --git a/CodeHub.iOS/Utilities/ViewControllerExtensions.cs b/CodeHub.iOS/Utilities/ViewControllerExtensions.cs index d94a04ac..122d45d5 100755 --- a/CodeHub.iOS/Utilities/ViewControllerExtensions.cs +++ b/CodeHub.iOS/Utilities/ViewControllerExtensions.cs @@ -1,12 +1,17 @@ using System; -using System.Threading; using System.Threading.Tasks; +using CodeHub.iOS.Services; using UIKit; namespace CodeHub.iOS.Utilities { public static class ViewControllerExtensions { + public static void ShowError(this UIViewController viewController, string title, Exception exception) + { + AlertDialogService.ShowAlert(title, exception.Message); + } + public static IHud CreateHud(this UIViewController controller) { return new Hud(controller.View); diff --git a/CodeHub.iOS/ViewControllers/Accounts/AccountsViewController.cs b/CodeHub.iOS/ViewControllers/Accounts/AccountsViewController.cs index bfaa9751..10e8afc4 100644 --- a/CodeHub.iOS/ViewControllers/Accounts/AccountsViewController.cs +++ b/CodeHub.iOS/ViewControllers/Accounts/AccountsViewController.cs @@ -1,23 +1,22 @@ -using MvvmCross.Platform; using CodeHub.Core.Services; -using CodeHub.iOS.ViewControllers; using UIKit; using Foundation; using System; using CodeHub.Core.Data; using CoreGraphics; -using CodeHub.Core.ViewModels.Accounts; using CodeHub.iOS.DialogElements; using System.Linq; using ReactiveUI; using CodeHub.Core.Messages; +using Splat; +using System.Threading.Tasks; namespace CodeHub.iOS.ViewControllers.Accounts { public class AccountsViewController : DialogViewController { - private readonly IAccountsService _accountsService = Mvx.Resolve(); - private readonly IApplicationService _applicationService = Mvx.Resolve(); + private readonly IAccountsService _accountsService = Locator.Current.GetService(); + private readonly IApplicationService _applicationService = Locator.Current.GetService(); public AccountsViewController() : base(UITableViewStyle.Plain) { @@ -39,30 +38,9 @@ public override void ViewDidLoad() TableView.RowHeight = 74; } - private void SelectAccount(GitHubAccount githubAccount) + private async Task SelectAccount(Account account) { - var isEnterprise = githubAccount.IsEnterprise || !string.IsNullOrEmpty(githubAccount.Password); - - if (githubAccount.DontRemember) - { - //Hack for now - if (isEnterprise) - { - var vc = new AddAccountViewController(); - vc.ViewModel.Init(new AddAccountViewModel.NavObject { AttemptedAccountId = githubAccount.Id }); - NavigationController.PushViewController(vc, true); - } - else - { - var loginViewController = new LoginViewController(); - loginViewController.ViewModel.Init(LoginViewModel.NavObject.CreateDontRemember(githubAccount)); - NavigationController.PushViewController(loginViewController, true); - } - - return; - } - - _applicationService.ActivateUser(githubAccount, null); + await _accountsService.SetActiveAccount(account); MessageBus.Current.SendMessage(new LogoutMessage()); } @@ -70,12 +48,14 @@ public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); - var accountsService = Mvx.Resolve(); var weakVm = new WeakReference(this); var accountSection = new Section(); - accountSection.AddAll(accountsService.Select(account => + + var activeAccount = _accountsService.GetActiveAccount().Result; + accountSection.AddAll(_accountsService.GetAccounts().Result.Select(account => { - var t = new AccountElement(account, account.Equals(accountsService.ActiveAccount)); + var isEqual = account.Id == activeAccount?.Id; + var t = new AccountElement(account, isEqual); t.Tapped += () => weakVm.Get()?.SelectAccount(account); return t; })); @@ -97,8 +77,9 @@ private void Delete(Element element) //Remove the designated username _accountsService.Remove(accountElement.Account); + var activeAccount = _accountsService.GetActiveAccount().Result; - if (_accountsService.ActiveAccount != null && _accountsService.ActiveAccount.Equals(accountElement.Account)) + if (activeAccount != null && activeAccount.Equals(accountElement.Account)) { _applicationService.DeactivateUser(); } @@ -156,9 +137,9 @@ protected class AccountElement : Element private readonly bool _currentAccount; - public GitHubAccount Account { get; private set; } + public Account Account { get; private set; } - public AccountElement(GitHubAccount account, bool currentAccount) + public AccountElement(Account account, bool currentAccount) { Account = account; _currentAccount = currentAccount; diff --git a/CodeHub.iOS/ViewControllers/Accounts/AddAccountView.xib b/CodeHub.iOS/ViewControllers/Accounts/AddAccountView.xib index 221316ae..cbb3ba96 100755 --- a/CodeHub.iOS/ViewControllers/Accounts/AddAccountView.xib +++ b/CodeHub.iOS/ViewControllers/Accounts/AddAccountView.xib @@ -1,99 +1,133 @@ - - + + + + + - + + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + - + - - - + + - - - - + - + @@ -102,15 +136,13 @@ - - - + diff --git a/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.cs b/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.cs index c40fce27..101a473c 100755 --- a/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.cs +++ b/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.cs @@ -3,37 +3,44 @@ using Foundation; using UIKit; using CodeHub.iOS.Utilities; -using CodeHub.iOS.ViewControllers; -using MvvmCross.Platform; -using CodeHub.Core.Services; -using CodeHub.Core.Factories; using System; -using GitHubSharp; -using System.Linq; -using System.Reactive.Threading.Tasks; using ReactiveUI; +using System.Reactive.Linq; +using CodeHub.Core.Utilities; namespace CodeHub.iOS.ViewControllers.Accounts { public partial class AddAccountViewController : BaseViewController { + private readonly UIColor ComponentTextColor = UIColor.White; private readonly UIColor EnterpriseBackgroundColor = UIColor.FromRGB(50, 50, 50); + private readonly UIColor ComponentBackground = UIColor.FromRGB(0x3C, 0x3C, 0x3C); - public AddAccountViewModel ViewModel { get; } + public AddAccountViewModel ViewModel { get; } = new AddAccountViewModel(); public AddAccountViewController() : base("AddAccountView", null) { - ViewModel = new AddAccountViewModel(Mvx.Resolve(), Mvx.Resolve()); - ViewModel.Init(new AddAccountViewModel.NavObject()); + NavigationItem.BackBarButtonItem = new UIBarButtonItem(); + + Title = "Login"; + + OnActivation(d => + { + d(this.WhenAnyObservable(x => x.ViewModel.LoginCommand.IsExecuting) + .SubscribeStatus("Logging in...")); + }); } public override void ViewDidLoad() { base.ViewDidLoad(); - Title = "Login"; + var scopes = string.Join(", ", OctokitClientFactory.Scopes); + DescriptionLabel.Text = string.Format("The provided Personal Access Token must allow access to the following scopes: {0}", scopes); + DescriptionLabel.TextColor = ComponentTextColor; + AuthenticationSelector.TintColor = ComponentTextColor.ColorWithAlpha(0.9f); View.BackgroundColor = EnterpriseBackgroundColor; Logo.Image = Images.Logos.EnterpriseMascot; @@ -44,7 +51,7 @@ public override void ViewDidLoad() //Set some generic shadowing LoginButton.Layer.ShadowColor = UIColor.Black.CGColor; LoginButton.Layer.ShadowOffset = new CGSize(0, 1); - LoginButton.Layer.ShadowOpacity = 0.3f; + LoginButton.Layer.ShadowOpacity = 0.2f; var attributes = new UIStringAttributes { ForegroundColor = UIColor.LightGray, @@ -53,14 +60,17 @@ public override void ViewDidLoad() Domain.AttributedPlaceholder = new NSAttributedString("Domain", attributes); User.AttributedPlaceholder = new NSAttributedString("Username", attributes); Password.AttributedPlaceholder = new NSAttributedString("Password", attributes); + TokenTextField.AttributedPlaceholder = new NSAttributedString("Token", attributes); - foreach (var i in new [] { Domain, User, Password }) + foreach (var i in new [] { Domain, User, Password, TokenTextField }) { i.Layer.BorderColor = UIColor.Black.CGColor; i.Layer.BorderWidth = 1; i.Layer.CornerRadius = 4; } + SelectAuthenticationScheme(0); + Domain.ShouldReturn = delegate { User.BecomeFirstResponder(); return true; @@ -70,6 +80,7 @@ public override void ViewDidLoad() Password.BecomeFirstResponder(); return true; }; + Password.ShouldReturn = delegate { Password.ResignFirstResponder(); LoginButton.SendActionForControlEvents(UIControlEvent.TouchUpInside); @@ -77,34 +88,54 @@ public override void ViewDidLoad() }; OnActivation(d => - { - d(User.GetChangedObservable().Subscribe(x => ViewModel.Username = x)); - d(Password.GetChangedObservable().Subscribe(x => ViewModel.Password = x)); - d(Domain.GetChangedObservable().Subscribe(x => ViewModel.Domain = x)); - d(LoginButton.GetClickedObservable().BindCommand(ViewModel.LoginCommand)); - d(ViewModel.Bind(x => x.IsLoggingIn).SubscribeStatus("Logging in...")); - d(ViewModel.LoginCommand.ThrownExceptions.Subscribe(HandleLoginException)); - }); + { + d(User.GetChangedObservable() + .Subscribe(x => ViewModel.Username = x)); + + d(Password.GetChangedObservable() + .Subscribe(x => ViewModel.Password = x)); + + d(Domain.GetChangedObservable() + .Subscribe(x => ViewModel.Domain = x)); + + d(TokenTextField.GetChangedObservable() + .Subscribe(x => ViewModel.Token = x)); + + d(LoginButton.GetClickedObservable() + .InvokeReactiveCommand(ViewModel.LoginCommand)); + + d(AuthenticationSelector.GetChangedObservable() + .Do(x => ViewModel.TokenAuthentication = x == 1) + .Subscribe(SelectAuthenticationScheme)); + + d(this.WhenAnyObservable(x => x.ViewModel.LoginCommand.CanExecute) + .Subscribe(x => LoginButton.Enabled = x)); + + d(this.WhenAnyValue(x => x.ViewModel.TokenAuthentication) + .Subscribe(x => AuthenticationSelector.SelectedSegment = x ? 1 : 0)); + }); } - private void HandleLoginException(Exception e) + private void SelectAuthenticationScheme(int scheme) { - var alert = Mvx.Resolve(); - - var authException = e as UnauthorizedException; - if (authException != null && authException.Headers.Contains("X-GitHub-OTP")) - { - alert.PromptTextBox("Authentication Error", "Please provide the two-factor authentication code for this account.", string.Empty, "Login") - .ToObservable() - .Subscribe(x => { - ViewModel.TwoFactor = x; - ViewModel.LoginCommand.ExecuteIfCan(); - }); - } - else + UIView.Animate(0.3, () => { - alert.Alert("Unable to Login!", "Unable to login user " + ViewModel.Username + ": " + e.Message); - } + if (scheme == 0) + { + TokenTextField.Hidden = true; + User.Hidden = false; + Password.Hidden = false; + } + else + { + TokenTextField.Hidden = false; + User.Hidden = true; + Password.Hidden = true; + } + }); + + UIView.Animate(0.3, 0, UIViewAnimationOptions.TransitionCrossDissolve, + () => DescriptionLabel.Alpha = scheme == 0 ? 0 : 1, null); } NSObject _hideNotification, _showNotification; diff --git a/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.designer.cs b/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.designer.cs index 6f4cd1e1..2a2bdaed 100644 --- a/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.designer.cs +++ b/CodeHub.iOS/ViewControllers/Accounts/AddAccountViewController.designer.cs @@ -1,6 +1,6 @@ // WARNING // -// This file has been generated automatically by Xamarin Studio to store outlets and +// This file has been generated automatically by Visual Studio to store outlets and // actions made in the UI designer. If it is removed, they will be lost. // Manual changes to this file may not be handled correctly. // @@ -9,66 +9,98 @@ namespace CodeHub.iOS.ViewControllers.Accounts { - [Register ("AddAccountView")] - partial class AddAccountViewController - { - [Outlet] - UIKit.UIView ContainerView { get; set; } - - [Outlet] - UIKit.UITextField Domain { get; set; } - - [Outlet] - UIKit.UIButton LoginButton { get; set; } - - [Outlet] - UIKit.UIImageView Logo { get; set; } - - [Outlet] - UIKit.UITextField Password { get; set; } - - [Outlet] - UIKit.UIScrollView ScrollView { get; set; } - - [Outlet] - UIKit.UITextField User { get; set; } - - void ReleaseDesignerOutlets () - { - if (ContainerView != null) { - ContainerView.Dispose (); - ContainerView = null; - } - - if (Domain != null) { - Domain.Dispose (); - Domain = null; - } - - if (LoginButton != null) { - LoginButton.Dispose (); - LoginButton = null; - } - - if (Logo != null) { - Logo.Dispose (); - Logo = null; - } - - if (Password != null) { - Password.Dispose (); - Password = null; - } - - if (ScrollView != null) { - ScrollView.Dispose (); - ScrollView = null; - } - - if (User != null) { - User.Dispose (); - User = null; - } - } - } + [Register ("AddAccountView")] + partial class AddAccountViewController + { + [Outlet] + UIKit.UISegmentedControl AuthenticationSelector { get; set; } + + [Outlet] + UIKit.UIView ContainerView { get; set; } + + [Outlet] + UIKit.UILabel DescriptionLabel { get; set; } + + [Outlet] + UIKit.UITextField Domain { get; set; } + + [Outlet] + UIKit.UIButton LoginButton { get; set; } + + [Outlet] + UIKit.UIImageView Logo { get; set; } + + [Outlet] + UIKit.UITextField Password { get; set; } + + [Outlet] + UIKit.UIScrollView ScrollView { get; set; } + + [Outlet] + UIKit.UIStackView StackView { get; set; } + + [Outlet] + UIKit.UITextField TokenTextField { get; set; } + + [Outlet] + UIKit.UITextField User { get; set; } + + void ReleaseDesignerOutlets () + { + if (ContainerView != null) { + ContainerView.Dispose (); + ContainerView = null; + } + + if (Domain != null) { + Domain.Dispose (); + Domain = null; + } + + if (LoginButton != null) { + LoginButton.Dispose (); + LoginButton = null; + } + + if (Logo != null) { + Logo.Dispose (); + Logo = null; + } + + if (Password != null) { + Password.Dispose (); + Password = null; + } + + if (ScrollView != null) { + ScrollView.Dispose (); + ScrollView = null; + } + + if (User != null) { + User.Dispose (); + User = null; + } + + if (StackView != null) { + StackView.Dispose (); + StackView = null; + } + + if (TokenTextField != null) { + TokenTextField.Dispose (); + TokenTextField = null; + } + + if (AuthenticationSelector != null) { + AuthenticationSelector.Dispose (); + AuthenticationSelector = null; + } + + if (DescriptionLabel != null) { + DescriptionLabel.Dispose (); + DescriptionLabel = null; + } + } + } } diff --git a/CodeHub.iOS/ViewControllers/Accounts/NewAccountViewController.cs b/CodeHub.iOS/ViewControllers/Accounts/NewAccountViewController.cs index 9d1f5f0a..b8b06d3c 100644 --- a/CodeHub.iOS/ViewControllers/Accounts/NewAccountViewController.cs +++ b/CodeHub.iOS/ViewControllers/Accounts/NewAccountViewController.cs @@ -1,20 +1,24 @@ using System; using CoreGraphics; using UIKit; -using MvvmCross.Platform; using CodeHub.Core.Services; using CodeHub.iOS.ViewControllers.Application; +using Splat; namespace CodeHub.iOS.ViewControllers.Accounts { public class NewAccountViewController : BaseViewController { + private readonly IFeaturesService _featuresService; private readonly UIColor DotComBackgroundColor = UIColor.FromRGB(239, 239, 244); private readonly UIColor EnterpriseBackgroundColor = UIColor.FromRGB(50, 50, 50); - public NewAccountViewController() + public NewAccountViewController(IFeaturesService featuresService = null) { + _featuresService = featuresService ?? Locator.Current.GetService(); + Title = "New Account"; + NavigationItem.BackBarButtonItem = new UIBarButtonItem(); } public override void ViewDidLoad() @@ -55,19 +59,18 @@ public override void ViewDidLoad() private void DotComButtonTouch () { - NavigationController.PushViewController(new LoginViewController(), true); + NavigationController.PushViewController(new OAuthLoginViewController(), true); } private void EnterpriseButtonTouch () { - var features = Mvx.Resolve(); - if (features.IsProEnabled) + if (_featuresService.IsProEnabled) { NavigationController.PushViewController(new AddAccountViewController(), true); } else { - this.PresentUpgradeViewController(); + UpgradeViewController.Present(this); } } diff --git a/CodeHub.iOS/ViewControllers/Accounts/LoginViewController.cs b/CodeHub.iOS/ViewControllers/Accounts/OAuthLoginViewController.cs old mode 100755 new mode 100644 similarity index 60% rename from CodeHub.iOS/ViewControllers/Accounts/LoginViewController.cs rename to CodeHub.iOS/ViewControllers/Accounts/OAuthLoginViewController.cs index b441b4e2..e39b2adf --- a/CodeHub.iOS/ViewControllers/Accounts/LoginViewController.cs +++ b/CodeHub.iOS/ViewControllers/Accounts/OAuthLoginViewController.cs @@ -1,103 +1,102 @@ -using System; -using CodeHub.Core.ViewModels.Accounts; -using MvvmCross.Platform; -using CodeHub.iOS.Utilities; -using Foundation; -using WebKit; -using CodeHub.Core.Services; -using CodeHub.iOS.Services; -using CodeHub.Core.Factories; -using CodeHub.iOS.ViewControllers; -using System.Reactive.Linq; -using CodeHub.iOS.Views; - -namespace CodeHub.iOS.ViewControllers.Accounts -{ - public class LoginViewController : BaseWebViewController - { - private static readonly string HasSeenWelcomeKey = "HAS_SEEN_OAUTH_INFO"; - - private static readonly string OAuthWelcome = - "In the following screen you will be prompted for your GitHub credentials. This is done through GitHub's OAuth portal, " + - "the recommended way to authenticate.\n\nCodeHub does not save your password. Instead, only the OAuth " + - "token is saved on the device which you may revoke at any time."; - - - public LoginViewModel ViewModel { get; } - - public LoginViewController() - : base(true) - { - Title = "Login"; - ViewModel = new LoginViewModel(Mvx.Resolve()); - ViewModel.Init(new LoginViewModel.NavObject()); - - OnActivation(d => d(ViewModel.Bind(x => x.IsLoggingIn).SubscribeStatus("Logging in..."))); - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - LoadRequest(); - - bool hasSeenWelcome = false; - var defaultValueService = Mvx.Resolve(); - defaultValueService.TryGet(HasSeenWelcomeKey, out hasSeenWelcome); - - if (!hasSeenWelcome) - { - Appeared - .Take(1) - .Subscribe(_ => - { - defaultValueService.Set(HasSeenWelcomeKey, true); - BlurredAlertView.Display(OAuthWelcome); - }); - } - } - - protected override bool ShouldStartLoad(WKWebView webView, WKNavigationAction navigationAction) - { - try - { - //We're being redirected to our redirect URL so we must have been successful - if (navigationAction.Request.Url.Host == "dillonbuchanan.com") - { - var code = navigationAction.Request.Url.Query.Split('=')[1]; - ViewModel.Login(code); - return false; - } - - if (navigationAction.Request.Url.AbsoluteString.StartsWith("https://github.com/join")) - { - Mvx.Resolve().Alert("Error", "Sorry, due to restrictions, creating GitHub accounts cannot be done in CodeHub."); - return false; - } - - return base.ShouldStartLoad(webView, navigationAction); - } - catch - { - Mvx.Resolve().Alert("Error Logging in!", "CodeHub is unable to login you in due to an unexpected error. Please try again."); - return false; - } - } - - protected override void OnLoadError(NSError e) - { - base.OnLoadError(e); - - //Frame interrupted error - if (e.Code == 102 || e.Code == -999) return; - AlertDialogService.ShowAlert("Error", "Unable to communicate with GitHub. " + e.LocalizedDescription); - } - - private void LoadRequest() - { - //Remove all cookies & cache - WKWebsiteDataStore.DefaultDataStore.RemoveDataOfTypes(WKWebsiteDataStore.AllWebsiteDataTypes, NSDate.FromTimeIntervalSince1970(0), - () => Web.LoadRequest(new NSUrlRequest(new NSUrl(ViewModel.LoginUrl)))); - } - } -} - +using System; +using CodeHub.Core.ViewModels.Accounts; +using CodeHub.iOS.Utilities; +using Foundation; +using WebKit; +using CodeHub.Core.Services; +using CodeHub.iOS.Services; +using System.Reactive.Linq; +using CodeHub.iOS.Views; +using System.Linq; +using ReactiveUI; +using Splat; + +namespace CodeHub.iOS.ViewControllers.Accounts +{ + public class OAuthLoginViewController : BaseWebViewController + { + private readonly IAlertDialogService _alertDialogService; + + private static readonly string OAuthWelcome = + "In the following screen you will be prompted for your GitHub credentials. This is done through GitHub's OAuth portal, " + + "the recommended way to authenticate.\n\nCodeHub does not save your password. Instead, only the OAuth " + + "token is saved on the device which you may revoke at any time."; + + public OAuthLoginViewModel ViewModel { get; } = new OAuthLoginViewModel(); + + public OAuthLoginViewController(IAlertDialogService alertDialogService = null) + : base(true) + { + _alertDialogService = alertDialogService ?? Locator.Current.GetService(); + + Title = "Login"; + + OnActivation(d => + { + d(this.WhenAnyObservable(x => x.ViewModel.LoginCommand.IsExecuting) + .SubscribeStatus("Logging in...")); + }); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + LoadRequest(); + + if (!Core.Settings.HasSeenOAuthWelcome) + { + Appeared + .Take(1) + .Do(_ => Core.Settings.HasSeenOAuthWelcome = true) + .Subscribe(_ => BlurredAlertView.Display(OAuthWelcome)); + } + } + + protected override bool ShouldStartLoad(WKWebView webView, WKNavigationAction navigationAction) + { + try + { + //We're being redirected to our redirect URL so we must have been successful + if (navigationAction.Request.Url.Host == "dillonbuchanan.com") + { + var queryParameters = navigationAction.Request.Url.Query.Split('&'); + + var code = queryParameters.FirstOrDefault(x => x.StartsWith("code=", StringComparison.OrdinalIgnoreCase)); + var codeValue = code?.Replace("code=", String.Empty); + ViewModel.LoginCommand.ExecuteNow(codeValue); + return false; + } + + if (navigationAction.Request.Url.AbsoluteString.StartsWith("https://github.com/join")) + { + _alertDialogService.Alert("Error", "Sorry, due to restrictions, creating GitHub accounts cannot be done in CodeHub."); + return false; + } + + return base.ShouldStartLoad(webView, navigationAction); + } + catch + { + _alertDialogService.Alert("Error Logging in!", "CodeHub is unable to login you in due to an unexpected error. Please try again."); + return false; + } + } + + protected override void OnLoadError(NSError e) + { + base.OnLoadError(e); + + //Frame interrupted error + if (e.Code == 102 || e.Code == -999) return; + AlertDialogService.ShowAlert("Error", "Unable to communicate with GitHub. " + e.LocalizedDescription); + } + + private void LoadRequest() + { + //Remove all cookies & cache + WKWebsiteDataStore.DefaultDataStore.RemoveDataOfTypes(WKWebsiteDataStore.AllWebsiteDataTypes, NSDate.FromTimeIntervalSince1970(0), + () => Web.LoadRequest(new NSUrlRequest(new NSUrl(ViewModel.LoginUrl)))); + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Application/DefaultStartupViewController.cs b/CodeHub.iOS/ViewControllers/Application/DefaultStartupViewController.cs deleted file mode 100644 index e7415fd0..00000000 --- a/CodeHub.iOS/ViewControllers/Application/DefaultStartupViewController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.App; -using System; -using UIKit; -using System.Linq; -using CodeHub.iOS.DialogElements; - -namespace CodeHub.iOS.ViewControllers.Application -{ - public class DefaultStartupViewController : ViewModelCollectionDrivenDialogViewController - { - public DefaultStartupViewController() - { - Title = "Default Startup View"; - EnableSearch = false; - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - var vm = (BaseDefaultStartupViewModel)ViewModel; - BindCollection(vm.StartupViews, x => { - var e = new StringElement(x); - e.Clicked.Subscribe(_ => vm.SelectedStartupView = x); - if (string.Equals(vm.SelectedStartupView, x)) - e.Accessory = UITableViewCellAccessory.Checkmark; - return e; - }, true); - - vm.Bind(x => x.SelectedStartupView, true).Subscribe(x => - { - if (Root.Count == 0) - return; - foreach (var m in Root[0].Elements.Cast()) - m.Accessory = (string.Equals(m.Caption, x)) ? UITableViewCellAccessory.Checkmark : UITableViewCellAccessory.None; - }); - } - } -} - diff --git a/CodeHub.iOS/ViewControllers/Application/EnterpriseSupportViewController.cs b/CodeHub.iOS/ViewControllers/Application/EnterpriseSupportViewController.cs new file mode 100644 index 00000000..6de64aeb --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Application/EnterpriseSupportViewController.cs @@ -0,0 +1,163 @@ +using System; +using UIKit; +using System.Reactive.Linq; +using MessageUI; +using CodeHub.Core.Services; +using Splat; +using Foundation; + +namespace CodeHub.iOS.ViewControllers.Application +{ + public class EnterpriseSupportViewController : BaseViewController + { + private static string CodeHubUrl = "https://github.com/CodeHubApp/CodeHub"; + + private readonly IAlertDialogService _alertDialogService; + private readonly UIColor ComponentColor = UIColor.FromWhiteAlpha(0.9f, 1f); + private NSLayoutConstraint[] _defaultConstraints; + private NSLayoutConstraint[] _landscapeConstraints; + + public EnterpriseSupportViewController(IAlertDialogService alertDialogService = null) + { + _alertDialogService = alertDialogService ?? Locator.Current.GetService(); + + Appearing + .Select(_ => NavigationController) + .Where(x => x != null) + .Subscribe(x => x.NavigationBar.ShadowImage = new UIImage()); + } + + private void SubmitFeedback() + { + if (!MFMailComposeViewController.CanSendMail) + { + _alertDialogService.Alert( + "No Email Setup", + "Looks like you don't have email setup on this device. " + + "Add a mail provider and try again.").ToBackground(); + } + else + { + var ctrl = new MFMailComposeViewController(); + ctrl.SetSubject("CodeHub Support"); + ctrl.SetToRecipients(new[] { "codehubapp@gmail.com" }); + ctrl.Finished += (sender, e) => DismissViewController(true, () => + { + if (e.Result == MFMailComposeResult.Sent) + _alertDialogService.Alert("Sent!", "Thanks for your feedback!"); + }); + PresentViewController(ctrl, true, null); + } + } + + private void GoToGitHub() + { + var viewController = new WebBrowserViewController(CodeHubUrl); + PresentViewController(viewController, true, null); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + View.BackgroundColor = UIColor.FromRGB(50, 50, 50); + + var imageView = new UIImageView(Octicon.Heart.ToImage(256, false)); + imageView.ContentMode = UIViewContentMode.ScaleAspectFit; + imageView.TintColor = UIColor.White; + View.Add(imageView); + + var label = new UILabel(); + label.Text = "Or"; + label.TextColor = ComponentColor; + label.TextAlignment = UITextAlignment.Center; + View.Add(label); + + var webButton = new UIButton(UIButtonType.Custom); + webButton.SetTitle("CodeHub on GitHub", UIControlState.Normal); + webButton.TouchUpInside += (sender, e) => GoToGitHub(); + + var button = new UIButton(UIButtonType.Custom); + button.SetTitle("Email Me!", UIControlState.Normal); + button.TouchUpInside += (sender, e) => SubmitFeedback(); + + foreach (var b in new[] { webButton, button }) + { + button.SetTitleColor(ComponentColor, UIControlState.Normal); + b.Font = UIFont.PreferredBody; + b.Layer.BorderColor = label.TextColor.CGColor; + b.Layer.BorderWidth = 1; + b.Layer.CornerRadius = 6f; + View.Add(b); + } + + _defaultConstraints = View.ConstrainLayout(() => + button.Frame.Width == 212 && + button.Frame.GetCenterX() == View.Frame.GetCenterX() && + button.Frame.Top == imageView.Frame.Bottom + 20f && + button.Frame.Height == 44 && + + imageView.Frame.Width == 192 && + imageView.Frame.Height == 192 && + imageView.Frame.GetCenterX() == View.Frame.GetCenterX() && + imageView.Frame.GetCenterY() == View.Frame.GetCenterY() - 96f && + + label.Frame.GetCenterX() == View.Frame.GetCenterX() && + label.Frame.Top == button.Frame.Bottom + 10 && + label.Frame.Width == 192 && + label.Frame.Height == 44 && + + webButton.Frame.Width == button.Frame.Width && + webButton.Frame.GetCenterX() == View.Frame.GetCenterX() && + webButton.Frame.Top == label.Frame.Bottom + 10f && + webButton.Frame.Height == 44); + + + _landscapeConstraints = View.ConstrainLayout(() => + button.Frame.Width == 212 && + button.Frame.GetCenterX() == View.Frame.GetCenterX() && + button.Frame.Top == imageView.Frame.Bottom + 20f && + button.Frame.Height == 44 && + + imageView.Frame.Width == 32 && + imageView.Frame.Height == 32 && + imageView.Frame.GetCenterX() == View.Frame.GetCenterX() && + imageView.Frame.GetCenterY() == View.Frame.GetCenterY() - 96f && + + label.Frame.GetCenterX() == View.Frame.GetCenterX() && + label.Frame.Top == button.Frame.Bottom + 10 && + label.Frame.Width == 100 && + label.Frame.Height == 44 && + + webButton.Frame.Width == button.Frame.Width && + webButton.Frame.GetCenterX() == View.Frame.GetCenterX() && + webButton.Frame.Top == label.Frame.Bottom + 10f && + webButton.Frame.Height == 44, + false); + } + + public override void WillRotate(UIInterfaceOrientation toInterfaceOrientation, double duration) + { + base.WillRotate(toInterfaceOrientation, duration); + + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) + { + if (toInterfaceOrientation == UIInterfaceOrientation.Portrait || toInterfaceOrientation == UIInterfaceOrientation.PortraitUpsideDown) + { + View.RemoveConstraints(_landscapeConstraints); + View.AddConstraints(_defaultConstraints); + View.SetNeedsUpdateConstraints(); + View.UpdateConstraintsIfNeeded(); + } + else + { + View.RemoveConstraints(_defaultConstraints); + View.AddConstraints(_landscapeConstraints); + View.SetNeedsUpdateConstraints(); + View.UpdateConstraintsIfNeeded(); + } + } + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Application/FeedbackComposerViewController.cs b/CodeHub.iOS/ViewControllers/Application/FeedbackComposerViewController.cs new file mode 100644 index 00000000..fabbf4d3 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Application/FeedbackComposerViewController.cs @@ -0,0 +1,81 @@ +using System; +using CodeHub.Core.ViewModels.App; +using UIKit; +using ReactiveUI; +using CodeHub.iOS.DialogElements; +using CodeHub.iOS.TableViewSources; +using System.Reactive.Linq; +using CodeHub.iOS.Views; +using System.Reactive; +using CodeHub.iOS.Utilities; + +namespace CodeHub.iOS.ViewControllers.Application +{ + public class FeedbackComposerViewController : TableViewController + { + public FeedbackComposerViewModel ViewModel { get; } = new FeedbackComposerViewModel(); + + public FeedbackComposerViewController() : base(UITableViewStyle.Plain) + { + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad) + ModalPresentationStyle = UIModalPresentationStyle.FormSheet; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var descriptionElement = new ExpandingInputElement("Description"); + descriptionElement.AccessoryView = x => new MarkdownAccessoryView(x); + var titleElement = new DummyInputElement("Title"); + + var saveButton = new UIBarButtonItem(UIBarButtonSystemItem.Save); + var cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + NavigationItem.LeftBarButtonItem = cancelButton; + NavigationItem.RightBarButtonItem = saveButton; + + var source = new DialogTableViewSource(TableView); + source.Root.Add(new Section { titleElement, descriptionElement }); + TableView.Source = source; + TableView.TableFooterView = new UIView(); + + OnActivation(d => { + d(descriptionElement.Changed + .Subscribe(x => ViewModel.Description = x)); + + d(this.WhenAnyValue(x => x.ViewModel.Title) + .Subscribe(title => Title = title)); + + d(this.WhenAnyValue(x => x.ViewModel.Subject) + .Subscribe(x => titleElement.Value = x)); + + d(this.WhenAnyValue(x => x.ViewModel.Description) + .Subscribe(x => descriptionElement.Value = x)); + + d(titleElement.Changed.Subscribe(x => ViewModel.Subject = x)); + + d(cancelButton.GetClickedObservable() + .Select(_ => Unit.Default) + .InvokeReactiveCommand(ViewModel.DismissCommand)); + + d(this.WhenAnyObservable(x => x.ViewModel.DismissCommand) + .Where(x => x) + .Subscribe(_ => DismissViewController(true, null))); + + d(saveButton.GetClickedObservable() + .Do(x => ResignFirstResponder()) + .Select(_ => Unit.Default) + .InvokeReactiveCommand(ViewModel.SubmitCommand)); + + d(ViewModel.SubmitCommand + .Subscribe(_ => DismissViewController(true, null))); + + d(this.WhenAnyObservable(x => x.ViewModel.SubmitCommand.CanExecute) + .Subscribe(x => saveButton.Enabled = x)); + + d(this.WhenAnyObservable(x => x.ViewModel.SubmitCommand.IsExecuting) + .SubscribeStatus("Submitting feedback")); + }); + } + } +} diff --git a/CodeHub.iOS/ViewControllers/Application/FeedbackViewController.cs b/CodeHub.iOS/ViewControllers/Application/FeedbackViewController.cs new file mode 100644 index 00000000..fef0679d --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Application/FeedbackViewController.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using CodeHub.Core.ViewModels.App; +using CodeHub.iOS.TableViewSources; +using CodeHub.iOS.Views; +using CodeHub.iOS.Views.Issues; +using CoreGraphics; +using ReactiveUI; +using UIKit; + +namespace CodeHub.iOS.ViewControllers.Application +{ + public class FeedbackViewController : TableViewController + { + private readonly UISearchBar _repositorySearchBar = new UISearchBar(new CGRect(0, 0, 320, 44)); + + private readonly Lazy _emptyView = new Lazy((() => + new EmptyListView(Octicon.IssueOpened.ToEmptyListImage(), "There are no open issues."))); + + private readonly Lazy _retryView; + + public FeedbackViewModel ViewModel { get; } = new FeedbackViewModel(); + + public FeedbackViewController() + : base(UITableViewStyle.Plain) + { + _retryView = new Lazy((() => + new RetryListView(Octicon.IssueOpened.ToEmptyListImage(), "Error loading feedback.", LoadData))); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var tableViewSource = new FeedbackTableViewSource(TableView, ViewModel.Items); + TableView.Source = tableViewSource; + + Appearing + .Take(1) + .Subscribe(_ => LoadData()); + + this.WhenActivated(d => + { + d(this.WhenAnyValue(x => x.ViewModel.Title) + .Subscribe(title => Title = title)); + + d(_repositorySearchBar.GetChangedObservable() + .Subscribe(x => ViewModel.SearchKeyword = x)); + + d(ViewModel.WhenAnyValue(x => x.IsEmpty) + .Where(x => x.HasValue) + .Select(x => x.Value) + .Subscribe(SetHasItems)); + + d(ViewModel.Items.Changed.Select(_ => Unit.Default) + .StartWith(Unit.Default) + .Select(_ => ViewModel.Items.Select(x => x.GoToCommand.Select(__ => x))) + .Select(x => Observable.Merge(x)) + .Switch() + .Select(x => new IssueView(x.RepositoryOwner, x.RepositoryName, x.IssueId)) + .Subscribe(x => NavigationController.PushViewController(x, true))); + }); + } + + private void LoadData() + { + if (_emptyView.IsValueCreated) + _emptyView.Value.RemoveFromSuperview(); + if (_retryView.IsValueCreated) + _retryView.Value.RemoveFromSuperview(); + + ViewModel.LoadCommand.Execute() + .Take(1) + .ObserveOn(RxApp.MainThreadScheduler) + .SubscribeError(setHasError); + } + + private void setHasError(Exception error) + { + _retryView.Value.Alpha = 0; + _retryView.Value.Frame = new CGRect(0, 0, View.Bounds.Width, View.Bounds.Height); + View.Add(_retryView.Value); + UIView.Animate(0.8, 0, UIViewAnimationOptions.CurveEaseIn, + () => _retryView.Value.Alpha = 1, null); + } + + private void SetHasItems(bool hasItems) + { + TableView.TableHeaderView = hasItems ? _repositorySearchBar : null; + + if (!hasItems) + { + _emptyView.Value.Alpha = 0; + _emptyView.Value.Frame = new CGRect(0, 0, View.Bounds.Width, View.Bounds.Height); + View.Add(_emptyView.Value); + UIView.Animate(0.8, 0, UIViewAnimationOptions.CurveEaseIn, + () => _emptyView.Value.Alpha = 1, null); + } + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Application/MenuViewController.cs b/CodeHub.iOS/ViewControllers/Application/MenuViewController.cs index 2f3cd063..6c54d901 100644 --- a/CodeHub.iOS/ViewControllers/Application/MenuViewController.cs +++ b/CodeHub.iOS/ViewControllers/Application/MenuViewController.cs @@ -1,18 +1,19 @@ -using CodeHub.iOS.ViewControllers; -using CodeHub.iOS.Views; -using CodeHub.Core.ViewModels.App; -using UIKit; -using System.Linq; -using CodeHub.Core.Utils; -using CodeHub.Core.Services; using System; -using MvvmCross.Platform; -using CodeHub.iOS.DialogElements; using System.Collections.Generic; -using CodeHub.iOS.ViewControllers.Accounts; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using CodeHub.Core.Services; +using CodeHub.Core.ViewModels.App; +using CodeHub.iOS.DialogElements; +using CodeHub.iOS.ViewControllers.Accounts; +using CodeHub.iOS.ViewControllers.Search; +using CodeHub.iOS.ViewControllers.Settings; +using CodeHub.iOS.Views; using CoreGraphics; +using MvvmCross.Platform; +using Splat; +using UIKit; namespace CodeHub.iOS.ViewControllers.Application { @@ -29,6 +30,9 @@ public class MenuViewController : ViewModelDrivenDialogViewController set { base.ViewModel = value; } } + private static bool IsAccountEnterprise + => Locator.Current.GetService().Account?.IsEnterprise ?? false; + /// /// Gets or sets the title. /// @@ -47,9 +51,7 @@ public override string Title { public MenuViewController() : base(false, UITableViewStyle.Plain) { - var appService = Mvx.Resolve(); - var featuresService = Mvx.Resolve(); - ViewModel = new MenuViewModel(appService, featuresService); + ViewModel = new MenuViewModel(); Appeared.Take(1).Subscribe(_ => PromptPushNotifications()); _title = new UILabel(new CGRect(0, 40, 320, 40)); @@ -68,7 +70,7 @@ public MenuViewController() private static async Task PromptPushNotifications() { var appService = Mvx.Resolve(); - if (appService.Account.IsEnterprise) + if (IsAccountEnterprise) return; var featuresService = Mvx.Resolve(); @@ -81,8 +83,9 @@ private static async Task PromptPushNotifications() if (appService.Account.IsPushNotificationsEnabled == null) { var result = await alertDialogService.PromptYesNo("Push Notifications", "Would you like to enable push notifications for this account?"); + var accountsService = Mvx.Resolve(); appService.Account.IsPushNotificationsEnabled = result; - appService.Accounts.Update(appService.Account); + await accountsService.Save(appService.Account); if (result) { @@ -141,17 +144,22 @@ private void CreateMenuRoot() sections.Add(eventsSection); var repoSection = new Section() { HeaderView = new MenuSectionView("Repositories") }; - repoSection.Add(new MenuElement("Owned", () => ViewModel.GoToOwnedRepositoriesCommand.Execute(null), Octicon.Repo.ToImage())); - repoSection.Add(new MenuElement("Starred", () => ViewModel.GoToStarredRepositoriesCommand.Execute(null), Octicon.Star.ToImage())); - repoSection.Add(new MenuElement("Trending", () => ViewModel.GoToTrendingRepositoriesCommand.Execute(null), Octicon.Pulse.ToImage())); - repoSection.Add(new MenuElement("Explore", () => ViewModel.GoToExploreRepositoriesCommand.Execute(null), Octicon.Globe.ToImage())); + repoSection.Add(new MenuElement("Owned", GoToOwnedRepositories, Octicon.Repo.ToImage())); + repoSection.Add(new MenuElement("Starred", GoToStarredRepositories, Octicon.Star.ToImage())); + repoSection.Add(new MenuElement("Trending", GoToTrendingRepositories, Octicon.Pulse.ToImage())); + repoSection.Add(new MenuElement("Search", GoToSearch, Octicon.Search.ToImage())); sections.Add(repoSection); if (ViewModel.PinnedRepositories.Any()) { - _favoriteRepoSection = new Section() { HeaderView = new MenuSectionView("Favorite Repositories") }; + _favoriteRepoSection = new Section { HeaderView = new MenuSectionView("Favorite Repositories") }; foreach (var pinnedRepository in ViewModel.PinnedRepositories) - _favoriteRepoSection.Add(new PinnedRepoElement(pinnedRepository, ViewModel.GoToRepositoryCommand)); + { + var element = new PinnedRepoElement(pinnedRepository); + element.Clicked.Subscribe(_ => GoToRepository(pinnedRepository.Owner, pinnedRepository.Name)); + _favoriteRepoSection.Add(element); + } + sections.Add(_favoriteRepoSection); } else @@ -176,19 +184,19 @@ private void CreateMenuRoot() sections.Add(orgSection); var gistsSection = new Section() { HeaderView = new MenuSectionView("Gists") }; - gistsSection.Add(new MenuElement("My Gists", () => ViewModel.GoToMyGistsCommand.Execute(null), Octicon.Gist.ToImage())); - gistsSection.Add(new MenuElement("Starred", () => ViewModel.GoToStarredGistsCommand.Execute(null), Octicon.Star.ToImage())); - gistsSection.Add(new MenuElement("Public", () => ViewModel.GoToPublicGistsCommand.Execute(null), Octicon.Globe.ToImage())); + gistsSection.Add(new MenuElement("My Gists", GoToOwnedGists, Octicon.Gist.ToImage())); + gistsSection.Add(new MenuElement("Starred", GoToStarredGists, Octicon.Star.ToImage())); + gistsSection.Add(new MenuElement("Public", GoToPublicGists, Octicon.Globe.ToImage())); sections.Add(gistsSection); -// + var infoSection = new Section() { HeaderView = new MenuSectionView("Info & Preferences") }; sections.Add(infoSection); - infoSection.Add(new MenuElement("Settings", () => ViewModel.GoToSettingsCommand.Execute(null), Octicon.Gear.ToImage())); + infoSection.Add(new MenuElement("Settings", GoToSettings, Octicon.Gear.ToImage())); if (ViewModel.ShouldShowUpgrades) - infoSection.Add(new MenuElement("Upgrades", () => ViewModel.GoToUpgradesCommand.Execute(null), Octicon.Lock.ToImage())); + infoSection.Add(new MenuElement("Upgrades", GoToUpgrades, Octicon.Lock.ToImage())); - infoSection.Add(new MenuElement("Feedback & Support", PresentUserVoice, Octicon.CommentDiscussion.ToImage())); + infoSection.Add(new MenuElement("Feedback & Support", GoToSupport, Octicon.CommentDiscussion.ToImage())); infoSection.Add(new MenuElement("Accounts", ProfileButtonClicked, Octicon.Person.ToImage())); Root.Reset(sections); @@ -201,6 +209,11 @@ public override void ViewWillAppear(bool animated) UpdateProfilePicture(); CreateMenuRoot(); + // A user has been activated! + var appService = Mvx.Resolve(); + appService.ActivationAction?.Invoke(); + appService.ActivationAction = null; + #if DEBUG GC.Collect(); GC.Collect(); @@ -208,21 +221,85 @@ public override void ViewWillAppear(bool animated) #endif } - public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) + private void GoToSearch() { - base.DidRotate(fromInterfaceOrientation); - UpdateProfilePicture(); + var vc = new ExploreViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToSettings() + { + var vc = new SettingsViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToRepository(string owner, string name) + { + var vc = new Repositories.RepositoryViewController(owner, name); + NavigationController?.PushViewController(vc, true); } - private void PresentUserVoice() + private void GoToSupport() { - ViewModel.GoToSupport.Execute(null); + var vc = IsAccountEnterprise + ? (UIViewController)new EnterpriseSupportViewController() + : new SupportViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToOwnedRepositories() + { + var vc = Repositories.RepositoriesViewController.CreateMineViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToStarredRepositories() + { + var vc = Repositories.RepositoriesViewController.CreateStarredViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToTrendingRepositories() + { + var vc = new Repositories.TrendingRepositoriesViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToOwnedGists() + { + var username = ViewModel.Account.Username; + var vc = Gists.GistsViewController.CreateUserGistsViewController(username); + NavigationController?.PushViewController(vc, true); + } + + private void GoToStarredGists() + { + var vc = Gists.GistsViewController.CreateStarredGistsViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToPublicGists() + { + var vc = Gists.GistsViewController.CreatePublicGistsViewController(); + NavigationController?.PushViewController(vc, true); + } + + private void GoToUpgrades() + { + var vc = new UpgradeViewController(); + NavigationController?.PushViewController(vc, true); + } + + public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) + { + base.DidRotate(fromInterfaceOrientation); + UpdateProfilePicture(); } private void ProfileButtonClicked() { var vc = new AccountsViewController(); - vc.NavigationItem.LeftBarButtonItem = new UIBarButtonItem { Image = Images.Buttons.CancelButton }; + vc.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); vc.NavigationItem.LeftBarButtonItem.Clicked += (sender, e) => DismissViewController(true, null); PresentViewController(new ThemedNavigationController(vc), true, null); } @@ -252,26 +329,14 @@ public override void ViewDidLoad() ViewModel.LoadCommand.Execute(null); - var appService = Mvx.Resolve (); - - // A user has been activated! - if (appService.ActivationAction != null) - { - appService.ActivationAction(); - appService.ActivationAction = null; - } } private class PinnedRepoElement : MenuElement { - public CodeHub.Core.Data.PinnedRepository PinnedRepo - { - get; - private set; - } + public Core.Data.PinnedRepository PinnedRepo { get; private set; } - public PinnedRepoElement(CodeHub.Core.Data.PinnedRepository pinnedRepo, System.Windows.Input.ICommand command) - : base(pinnedRepo.Name, () => command.Execute(new RepositoryIdentifier(pinnedRepo.Owner, pinnedRepo.Name)), Octicon.Repo.ToImage()) + public PinnedRepoElement(Core.Data.PinnedRepository pinnedRepo) + : base(pinnedRepo.Name, null, Octicon.Repo.ToImage()) { PinnedRepo = pinnedRepo; diff --git a/CodeHub.iOS/ViewControllers/Application/SettingsViewController.cs b/CodeHub.iOS/ViewControllers/Application/SettingsViewController.cs deleted file mode 100644 index 5de24ffb..00000000 --- a/CodeHub.iOS/ViewControllers/Application/SettingsViewController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using MvvmCross.Platform; -using CodeHub.Core.Services; -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.App; -using CodeHub.iOS.Utilities; -using UIKit; -using Foundation; -using CodeHub.iOS.DialogElements; - -namespace CodeHub.iOS.ViewControllers.Application -{ - public class SettingsViewController : ViewModelDrivenDialogViewController - { - public SettingsViewController() - { - Title = "Settings"; - - OnActivation(d => - { - var vm = (SettingsViewModel)ViewModel; - d(vm.Bind(x => x.PushNotificationsEnabled).Subscribe(_ => CreateTable())); - d(vm.Bind(x => x.IsSaving).SubscribeStatus("Saving...")); - CreateTable(); - }); - } - - private void CreateTable() - { - var application = Mvx.Resolve(); - var vm = (SettingsViewModel)ViewModel; - var currentAccount = application.Account; - var accountSection = new Section("Account"); - - var saveCredentials = new BooleanElement("Save Credentials", !currentAccount.DontRemember); - saveCredentials.Changed.Subscribe(x => { - currentAccount.DontRemember = !x; - application.Accounts.Update(currentAccount); - }); - accountSection.Add(saveCredentials); - - var showOrganizationsInEvents = new BooleanElement("Show Organizations in Events", currentAccount.ShowOrganizationsInEvents); - showOrganizationsInEvents.Changed.Subscribe(x => { - currentAccount.ShowOrganizationsInEvents = x; - application.Accounts.Update(currentAccount); - }); - - var showOrganizations = new BooleanElement("List Organizations in Menu", currentAccount.ExpandOrganizations); - showOrganizations.Changed.Subscribe(x => { - currentAccount.ExpandOrganizations = x; - application.Accounts.Update(currentAccount); - }); - - var repoDescriptions = new BooleanElement("Show Repo Descriptions", currentAccount.ShowRepositoryDescriptionInList); - repoDescriptions.Changed.Subscribe(x => { - currentAccount.ShowRepositoryDescriptionInList = x; - application.Accounts.Update(currentAccount); - }); - - var startupView = new StringElement("Startup View", vm.DefaultStartupViewName, UITableViewCellStyle.Value1) - { - Accessory = UITableViewCellAccessory.DisclosureIndicator, - }; - startupView.Clicked.BindCommand(vm.GoToDefaultStartupViewCommand); - - var pushNotifications = new BooleanElement("Push Notifications", vm.PushNotificationsEnabled); - pushNotifications.Changed.Subscribe(e => vm.PushNotificationsEnabled = e); - accountSection.Add(pushNotifications); - - var source = new StringElement("Source Code"); - source.Clicked.BindCommand(vm.GoToSourceCodeCommand); - - var follow = new StringElement("Follow On Twitter"); - follow.Clicked.Subscribe(_ => UIApplication.SharedApplication.OpenUrl(new NSUrl("https://twitter.com/CodeHubapp"))); - - var rate = new StringElement("Rate This App"); - rate.Clicked.Subscribe(_ => UIApplication.SharedApplication.OpenUrl(new NSUrl("https://itunes.apple.com/us/app/codehub-github-for-ios/id707173885?mt=8"))); - - var aboutSection = new Section("About", "Thank you for downloading. Enjoy!") { source, follow, rate }; - - if (vm.ShouldShowUpgrades) - { - var upgrades = new StringElement("Upgrades"); - upgrades.Clicked.BindCommand(vm.GoToUpgradesCommand); - aboutSection.Add(upgrades); - } - - var appVersion = new StringElement("App Version", UIApplication.SharedApplication.GetVersion()) - { - Accessory = UITableViewCellAccessory.None, - SelectionStyle = UITableViewCellSelectionStyle.None - }; - - aboutSection.Add(appVersion); - - //Assign the root - Root.Reset(accountSection, new Section("Appearance") { showOrganizationsInEvents, showOrganizations, repoDescriptions, startupView }, aboutSection); - } - } -} - - diff --git a/CodeHub.iOS/ViewControllers/Application/StartupViewController.cs b/CodeHub.iOS/ViewControllers/Application/StartupViewController.cs index 162abb9c..eca8c708 100644 --- a/CodeHub.iOS/ViewControllers/Application/StartupViewController.cs +++ b/CodeHub.iOS/ViewControllers/Application/StartupViewController.cs @@ -1,13 +1,8 @@ using System; using UIKit; -using CodeHub.iOS; using SDWebImage; using Foundation; using CodeHub.Core.ViewModels.App; -using CodeHub.iOS.ViewControllers; -using MvvmCross.Platform; -using CodeHub.Core.Factories; -using CodeHub.Core.Services; using MonoTouch.SlideoutNavigation; using CodeHub.iOS.ViewControllers.Accounts; using CodeHub.Core.Utilities; @@ -23,15 +18,7 @@ public class StartupViewController : BaseViewController private UILabel _statusLabel; private UIActivityIndicatorView _activityView; - public StartupViewModel ViewModel { get; } - - public StartupViewController() - { - ViewModel = new StartupViewModel( - Mvx.Resolve(), - Mvx.Resolve(), - Mvx.Resolve()); - } + public StartupViewModel ViewModel { get; } = new StartupViewModel(); public override void ViewWillLayoutSubviews() { @@ -69,9 +56,9 @@ public override void ViewDidLoad() { d(ViewModel.Bind(x => x.ImageUrl).Subscribe(UpdatedImage)); d(ViewModel.Bind(x => x.Status).Subscribe(x => _statusLabel.Text = x)); - d(ViewModel.GoToMenu.Subscribe(GoToMenu)); - d(ViewModel.GoToAccounts.Subscribe(GoToAccounts)); - d(ViewModel.GoToNewAccount.Subscribe(GoToNewAccount)); + d(ViewModel.GoToMenu.Subscribe(_ => GoToMenu())); + d(ViewModel.GoToAccounts.Subscribe(_ => GoToAccounts())); + d(ViewModel.GoToNewAccount.Subscribe(_ => GoToNewAccount())); d(ViewModel.Bind(x => x.IsLoggingIn).Subscribe(x => { if (x) @@ -84,25 +71,67 @@ public override void ViewDidLoad() }); } - private void GoToMenu(object o) + private void GoToMenu() { var vc = new MenuViewController(); var slideoutController = new SlideoutNavigationController(); slideoutController.MenuViewController = new MenuNavigationController(vc, slideoutController); - (UIApplication.SharedApplication.Delegate as AppDelegate).Do(y => y.Presenter.SlideoutNavigationController = slideoutController); - vc.ViewModel.GoToDefaultTopView.Execute(null); + + var appDelegate = UIApplication.SharedApplication.Delegate as AppDelegate; + if (appDelegate != null) + appDelegate.Presenter.SlideoutNavigationController = slideoutController; + + var openButton = new UIBarButtonItem { Image = Images.Buttons.ThreeLinesButton }; + var mainNavigationController = new MainNavigationController(GetInitialMenuViewController(), slideoutController, openButton); + slideoutController.SetMainViewController(mainNavigationController, false); + slideoutController.ModalTransitionStyle = UIModalTransitionStyle.CrossDissolve; - PresentViewController(slideoutController, true, null); + AppDelegate.Instance.TransitionToViewController(slideoutController); + } + + private UIViewController GetInitialMenuViewController() + { + var username = ViewModel.Account.Username; + switch (ViewModel.Account.DefaultStartupView) + { + case "Organizations": + return new Organizations.OrganizationsViewController(username); + case "Trending Repositories": + return new Repositories.TrendingRepositoriesViewController(); + case "Explore Repositories": + return new Search.ExploreViewController(); + case "Owned Repositories": + return Repositories.RepositoriesViewController.CreateMineViewController(); + case "Starred Repositories": + return Repositories.RepositoriesViewController.CreateStarredViewController(); + case "Public Gists": + return Gists.GistsViewController.CreatePublicGistsViewController(); + case "Starred Gists": + return Gists.GistsViewController.CreateStarredGistsViewController(); + case "My Gists": + return Gists.GistsViewController.CreateUserGistsViewController(username); + case "Profile": + return new Users.UserViewController(username); + case "My Events": + return new Events.UserEventsViewController(username); + case "My Issues": + return Views.Issues.MyIssuesView.Create(); + case "Notifications": + return Views.NotificationsView.Create(); + default: + return Events.NewsViewController.Create(); + + } } - private void GoToNewAccount(object o) + private void GoToNewAccount() { var vc = new NewAccountViewController(); var nav = new ThemedNavigationController(vc); PresentViewController(nav, true, null); } - private void GoToAccounts(object o) + private void GoToAccounts() { var vc = new AccountsViewController(); var nav = new ThemedNavigationController(vc); @@ -127,7 +156,7 @@ public void UpdatedImage(Uri uri) _imgView.SetImage(new NSUrl(uri.AbsoluteUri), Images.LoginUserUnknown, (img, err, cache, _) => { _imgView.Image = Images.LoginUserUnknown; - UIView.Transition(_imgView, 0.25f, UIViewAnimationOptions.TransitionCrossDissolve, () => _imgView.Image = img, null); + UIView.Transition(_imgView, 0.25f, UIViewAnimationOptions.TransitionCrossDissolve , () => _imgView.Image = img, null); }); } } diff --git a/CodeHub.iOS/ViewControllers/Application/SupportViewController.cs b/CodeHub.iOS/ViewControllers/Application/SupportViewController.cs new file mode 100644 index 00000000..1a704c0b --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Application/SupportViewController.cs @@ -0,0 +1,111 @@ +using System; +using System.Reactive.Linq; +using CodeHub.Core.ViewModels.App; +using Humanizer; +using UIKit; +using ReactiveUI; +using CodeHub.iOS.DialogElements; +using System.Reactive; +using CodeHub.iOS.ViewControllers.Repositories; +using MessageUI; +using CodeHub.Core.Services; +using Splat; + +namespace CodeHub.iOS.ViewControllers.Application +{ + public class SupportViewController : BaseDialogViewController + { + private readonly IAlertDialogService _alertDialogService = Locator.Current.GetService(); + + public SupportViewModel ViewModel { get; } = new SupportViewModel(); + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var split = new SplitButtonElement(); + var contributors = split.AddButton("Contributors", "-"); + var lastCommit = split.AddButton("Last Commit", "-"); + + var openIssue = new BigButtonElement("Open an Issue on GitHub", Octicon.Bug); + var sendEmail = new BigButtonElement("Email Support", Octicon.Mail); + var openIssues = new BigButtonElement("Existing Issues", Octicon.IssueOpened); + + HeaderView.SubText = "This app is the product of hard work and great suggestions! Thank you to all whom provide feedback!"; + HeaderView.Image = UIImage.FromBundle("AppIcons60x60"); + + NavigationItem.BackBarButtonItem = new UIBarButtonItem { Title = "" }; + + Root.Reset( + new Section { split }, + new Section(null, "Opening an issue on the GitHub project page is the fastest way to get a response.") { openIssue, openIssues }, + new Section(), + new Section(null, "Emails are answered as quickly as possible but there is only one person answering them so expect a delay.") { sendEmail }); + + OnActivation(d => + { + d(openIssue.Clicked + .Select(_ => new FeedbackComposerViewController()) + .Select(viewCtrl => new ThemedNavigationController(viewCtrl)) + .Subscribe(viewCtrl => PresentViewController(viewCtrl, true, null))); + + d(sendEmail.Clicked.Subscribe(_ => SendEmail())); + + d(this.WhenAnyValue(x => x.ViewModel.Title) + .Subscribe(title => Title = title)); + + d(openIssues.Clicked + .Subscribe(_ => this.PushViewController(new FeedbackViewController()))); + + d(HeaderView.Clicked.Subscribe(_ => GoToRepository())); + + d(this.WhenAnyValue(x => x.ViewModel.Contributors) + .Where(x => x.HasValue) + .Subscribe(x => contributors.Text = (x.Value >= 100 ? "100+" : x.Value.ToString()))); + + d(this.WhenAnyValue(x => x.ViewModel.LastCommit) + .Where(x => x.HasValue) + .Subscribe(x => lastCommit.Text = x.Value.UtcDateTime.Humanize())); + }); + + Appearing + .Take(1) + .Select(_ => Unit.Default) + .InvokeReactiveCommand(ViewModel.LoadCommand); + } + + private void SendEmail() + { + if (!MFMailComposeViewController.CanSendMail) + { + _alertDialogService.Alert( + "No Email Setup", + "Looks like you don't have email setup on this device. " + + "Add a mail provider and try again.").ToBackground(); + } + else + { + var ctrl = new MFMailComposeViewController(); + ctrl.SetSubject("CodeHub Support"); + ctrl.SetToRecipients(new[] { "codehubapp@gmail.com" }); + ctrl.Finished += (sender, e) => DismissViewController(true, () => + { + if (e.Result == MFMailComposeResult.Sent) + _alertDialogService.Alert("Sent!", "Thanks for your feedback!"); + }); + PresentViewController(ctrl, true, null); + } + } + + + private void GoToRepository() + => this.PushViewController(RepositoryViewController.CreateCodeHubViewController()); + + private class BigButtonElement : ButtonElement, IElementSizing + { + public BigButtonElement(string name, Octicon img) : base(name, img.ToImage()) { } + public nfloat GetHeight(UITableView tableView, Foundation.NSIndexPath indexPath) => 60f; + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Application/UpgradeViewController.cs b/CodeHub.iOS/ViewControllers/Application/UpgradeViewController.cs index 0fdacee6..be774ba3 100644 --- a/CodeHub.iOS/ViewControllers/Application/UpgradeViewController.cs +++ b/CodeHub.iOS/ViewControllers/Application/UpgradeViewController.cs @@ -5,25 +5,24 @@ using System.Threading.Tasks; using System.Linq; using CodeHub.Core.Services; -using CodeHub.iOS.ViewControllers; using BigTed; using System.Reactive.Disposables; -using CodeHub.iOS.WebViews; -using CodeHub.iOS.Views; -using MvvmCross.Platform; +using CodeHub.WebViews; +using Splat; namespace CodeHub.iOS.ViewControllers.Application { - public class UpgradeViewController : WebView + public class UpgradeViewController : BaseWebViewController { - private readonly IFeaturesService _featuresService = Mvx.Resolve(); - private readonly IInAppPurchaseService _inAppPurchaseService = Mvx.Resolve(); + private readonly IFeaturesService _featuresService + = Locator.Current.GetService(); + private readonly IInAppPurchaseService _inAppPurchaseService + = Locator.Current.GetService(); private UIActivityIndicatorView _activityView; public UpgradeViewController() : base(false, false) { Title = "Pro Upgrade"; - ViewModel = new CodeHub.Core.ViewModels.App.UpgradeViewModel(); } public override void ViewDidLoad() @@ -51,13 +50,15 @@ private async Task Load() try { - var request = _inAppPurchaseService.RequestProductData(FeaturesService.ProEdition).WithTimeout(TimeSpan.FromSeconds(30)); - var productData = (await request).Products.FirstOrDefault(); + var response = await _inAppPurchaseService + .RequestProductData(FeaturesService.ProEdition) + .WithTimeout(TimeSpan.FromSeconds(30)); + + var productData = response.Products.FirstOrDefault(); var enabled = _featuresService.IsProEnabled; - var model = new UpgradeDetailsModel(productData != null ? productData.LocalizedPrice() : null, enabled); - var content = new UpgradeDetailsRazorView { Model = model }.GenerateString(); - LoadContent(content); - Web.UserInteractionEnabled = true; + var model = new UpgradeDetailsModel(productData?.LocalizedPrice(), enabled); + var viewModel = new UpgradeDetailsWebView { Model = model }; + LoadContent(viewModel.GenerateString()); } catch (Exception e) { @@ -65,6 +66,8 @@ private async Task Load() } finally { + Web.UserInteractionEnabled = true; + UIView.Animate(0.2f, 0, UIViewAnimationOptions.BeginFromCurrentState | UIViewAnimationOptions.CurveEaseInOut, () => _activityView.Alpha = 0, () => { @@ -108,9 +111,7 @@ protected override bool ShouldStartLoad(WebKit.WKWebView webView, WebKit.WKNavig if (url.Scheme.Equals("http") || url.Scheme.Equals("https")) { var view = new WebBrowserViewController(url.AbsoluteString); - view.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(Images.Buttons.CancelButton, UIBarButtonItemStyle.Done, - (s, e) => DismissViewController(true, null)); - PresentViewController(new ThemedNavigationController(view), true, null); + PresentViewController(view, true, null); return false; } @@ -131,19 +132,20 @@ private async Task Activate(Func activation) AlertDialogService.ShowAlert("Error", e.Message); } } - } - public static class UpgradeViewControllerExtensions - { - public static UIViewController PresentUpgradeViewController(this UIViewController @this) + public static UpgradeViewController Present(UIViewController parent) { var vc = new UpgradeViewController(); var nav = new ThemedNavigationController(vc); - var navObj = new UIBarButtonItem(Images.Buttons.CancelButton, UIBarButtonItemStyle.Done, (_, __) => @this.DismissViewController(true, null)); - vc.ViewWillAppearCalled += (sender, e) => vc.NavigationItem.LeftBarButtonItem = navObj; - vc.ViewDidDisappearCalled += (sender, e) => vc.NavigationItem.LeftBarButtonItem = null; - @this.PresentViewController(nav, true, null); + var navObj = new UIBarButtonItem( + UIBarButtonSystemItem.Cancel, + (_, __) => parent.DismissViewController(true, null)); + + vc.Appearing.Subscribe(_ => vc.NavigationItem.LeftBarButtonItem = navObj); + vc.Disappeared.Subscribe(_ => vc.NavigationItem.LeftBarButtonItem = null); + + parent.PresentViewController(nav, true, null); return vc; } } diff --git a/CodeHub.iOS/ViewControllers/BaseDialogViewController.cs b/CodeHub.iOS/ViewControllers/BaseDialogViewController.cs new file mode 100644 index 00000000..3cdf07fa --- /dev/null +++ b/CodeHub.iOS/ViewControllers/BaseDialogViewController.cs @@ -0,0 +1,133 @@ +using System; +using UIKit; +using System.Reactive.Linq; +using CodeHub.iOS.TableViewSources; +using CodeHub.iOS.DialogElements; +using CodeHub.iOS.Views; + +namespace CodeHub.iOS.ViewControllers +{ + public abstract class BaseDialogViewController : TableViewController + { + protected readonly SlideUpTitleView SlideUpTitle; + protected readonly ImageAndTitleHeaderView HeaderView; + private readonly UIView _backgroundHeaderView; + private DialogTableViewSource _dialogSource; + + protected DialogTableViewSource DialogSource + { + get { return _dialogSource; } + } + + protected RootElement Root{ + get { return _dialogSource.Root; } + } + + public override string Title + { + get + { + return base.Title; + } + set + { + HeaderView.Text = value; + SlideUpTitle.Text = value; + base.Title = value; + RefreshHeaderView(); + } + } + + public override UIRefreshControl RefreshControl + { + get { return base.RefreshControl; } + set + { + if (value != null) { + value.TintColor = UIColor.White.ColorWithAlpha(0.8f); + } + base.RefreshControl = value; + } + } + + protected BaseDialogViewController() + : base(UITableViewStyle.Grouped) + { + SlideUpTitle = new SlideUpTitleView(44f) { Offset = 100f }; + NavigationItem.TitleView = SlideUpTitle; + HeaderView = new ImageAndTitleHeaderView(); + _backgroundHeaderView = new UIView(); + + Appearing + .Where(x => ToolbarItems != null && NavigationController != null) + .Subscribe(x => NavigationController.SetToolbarHidden(false, x)); + Disappearing + .Where(x => ToolbarItems != null && NavigationController != null) + .Subscribe(x => NavigationController.SetToolbarHidden(true, x)); + Disappearing + .Where(_ => NavigationController != null) + .Subscribe(_ => NavigationController.NavigationBar.ShadowImage = null); + } + + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + HeaderView.BackgroundColor = NavigationController.NavigationBar.BarTintColor; + HeaderView.TextColor = NavigationController.NavigationBar.TintColor; + HeaderView.SubTextColor = NavigationController.NavigationBar.TintColor.ColorWithAlpha(0.8f); + (SlideUpTitle.Subviews[0] as UILabel).TextColor = HeaderView.TextColor; + _backgroundHeaderView.BackgroundColor = HeaderView.BackgroundColor; + TableView.TableHeaderView = HeaderView; + } + + public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) + { + base.DidRotate(fromInterfaceOrientation); + TableView.BeginUpdates(); + TableView.TableHeaderView = HeaderView; + TableView.EndUpdates(); + } + + protected virtual DialogTableViewSource CreateTableViewSource() + { + return new DialogTableViewSource(TableView); + } + + protected void RefreshHeaderView(string text = null, string subtext = null) + { + HeaderView.Text = text ?? HeaderView.Text; + HeaderView.SubText = subtext ?? HeaderView.SubText; + TableView.TableHeaderView = HeaderView; + TableView.ReloadData(); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + _dialogSource = CreateTableViewSource(); + + TableView.SectionHeaderHeight = 0; + TableView.Source = _dialogSource; + + var frame = TableView.Bounds; + frame.Y = -frame.Size.Height; + _backgroundHeaderView.Frame = frame; + _backgroundHeaderView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth; + _backgroundHeaderView.Layer.ZPosition = -1f; + TableView.InsertSubview(_backgroundHeaderView, 0); + + var scrollingObservable = _dialogSource.ScrolledObservable.Select(x => x.Y).StartWith(TableView.ContentOffset.Y); + var shadowObs = scrollingObservable.Where(x => x > 0 && NavigationController != null); + var shadowImgObs = scrollingObservable.Where(x => x <= 0).Where(_ => NavigationController != null) + .Where(_ => NavigationController.NavigationBar.ShadowImage == null); + var slideObs = scrollingObservable.Where(_ => SlideUpTitle != null); + + OnActivation(d => { + d(shadowObs.Subscribe(_ => NavigationController.NavigationBar.ShadowImage = null)); + d(shadowImgObs.Subscribe(_ => NavigationController.NavigationBar.ShadowImage = new UIImage())); + d(slideObs.Subscribe(x => SlideUpTitle.Offset = 108 + 28f - x)); + }); + } + } +} diff --git a/CodeHub.iOS/ViewControllers/BaseViewController.cs b/CodeHub.iOS/ViewControllers/BaseViewController.cs index 9873c155..620b1289 100644 --- a/CodeHub.iOS/ViewControllers/BaseViewController.cs +++ b/CodeHub.iOS/ViewControllers/BaseViewController.cs @@ -2,8 +2,8 @@ using ReactiveUI; using System.Reactive.Subjects; using System.Reactive.Linq; -using System.Collections.Generic; using Foundation; +using System.Reactive; namespace CodeHub.iOS.ViewControllers { @@ -13,38 +13,28 @@ public abstract class BaseViewController : ReactiveViewController, IActivatable private readonly ISubject _appearedSubject = new Subject(); private readonly ISubject _disappearingSubject = new Subject(); private readonly ISubject _disappearedSubject = new Subject(); - private readonly ICollection _activations = new LinkedList(); + private readonly ISubject _loadedSubject = new Subject(); - #if DEBUG +#if DEBUG ~BaseViewController() { Console.WriteLine("All done with " + GetType().Name); } - #endif +#endif - public IObservable Appearing - { - get { return _appearingSubject.AsObservable(); } - } + public IObservable Loaded => _loadedSubject.AsObservable(); - public IObservable Appeared - { - get { return _appearedSubject.AsObservable(); } - } + public IObservable Appearing => _appearingSubject.AsObservable(); - public IObservable Disappearing - { - get { return _disappearingSubject.AsObservable(); } - } + public IObservable Appeared => _appearedSubject.AsObservable(); - public IObservable Disappeared - { - get { return _disappearedSubject.AsObservable(); } - } + public IObservable Disappearing => _disappearingSubject.AsObservable(); + + public IObservable Disappeared => _disappearedSubject.AsObservable(); public void OnActivation(Action> d) { - Appearing.Subscribe(_ => d(x => _activations.Add(x))); + this.WhenActivated(d); } protected BaseViewController() @@ -63,20 +53,9 @@ private void CommonConstructor() this.WhenActivated(_ => { }); } - private void DisposeActivations() - { - foreach (var a in _activations) - a.Dispose(); - _activations.Clear(); - } - public override void ViewWillAppear(bool animated) { - MvvmCross.Platform.Mvx.Resolve() - .LogScreen(GetType().Name); - base.ViewWillAppear(animated); - DisposeActivations(); _appearingSubject.OnNext(animated); } @@ -89,7 +68,6 @@ public override void ViewDidAppear(bool animated) public override void ViewWillDisappear(bool animated) { base.ViewWillDisappear(animated); - DisposeActivations(); _disappearingSubject.OnNext(animated); } @@ -98,6 +76,12 @@ public override void ViewDidDisappear(bool animated) base.ViewDidDisappear(animated); _disappearedSubject.OnNext(animated); } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + _loadedSubject.OnNext(Unit.Default); + } } } diff --git a/CodeHub.iOS/ViewControllers/BaseWebViewController.cs b/CodeHub.iOS/ViewControllers/BaseWebViewController.cs index b0c1bad4..a26bff9c 100644 --- a/CodeHub.iOS/ViewControllers/BaseWebViewController.cs +++ b/CodeHub.iOS/ViewControllers/BaseWebViewController.cs @@ -1,240 +1,253 @@ -using System; -using UIKit; -using Foundation; -using WebKit; -using CodeHub.iOS.Utilities; - -namespace CodeHub.iOS.ViewControllers -{ - public abstract class BaseWebViewController : BaseViewController - { - protected UIBarButtonItem BackButton; - protected UIBarButtonItem RefreshButton; - protected UIBarButtonItem ForwardButton; - - public WKWebView Web { get; private set; } - private readonly bool _navigationToolbar; - private readonly bool _showPageAsTitle; - - protected virtual void GoBack() - { - Web.GoBack(); - } - - protected virtual void Refresh() - { - Web.Reload(); - } - - protected virtual void GoForward() - { - Web.GoForward(); - } - - protected BaseWebViewController() - : this(true, true) - { - } - - protected BaseWebViewController(bool navigationToolbar, bool showPageAsTitle = false) - { - NavigationItem.BackBarButtonItem = new UIBarButtonItem { Title = "" }; - - _navigationToolbar = navigationToolbar; - _showPageAsTitle = showPageAsTitle; - - if (_navigationToolbar) - { - BackButton = new UIBarButtonItem { Image = Images.Web.BackButton, Enabled = false }; - ForwardButton = new UIBarButtonItem { Image = Images.Web.FowardButton, Enabled = false }; - RefreshButton = new UIBarButtonItem(UIBarButtonSystemItem.Refresh) { Enabled = false }; - - BackButton.TintColor = Theme.CurrentTheme.WebButtonTint; - ForwardButton.TintColor = Theme.CurrentTheme.WebButtonTint; - RefreshButton.TintColor = Theme.CurrentTheme.WebButtonTint; - - OnActivation(d => - { - d(BackButton.GetClickedObservable().Subscribe(_ => GoBack())); - d(ForwardButton.GetClickedObservable().Subscribe(_ => GoForward())); - d(RefreshButton.GetClickedObservable().Subscribe(_ => Refresh())); - }); - } - - EdgesForExtendedLayout = UIRectEdge.None; - } - - private class NavigationDelegate : WKNavigationDelegate - { - private readonly WeakReference _webView; - - public NavigationDelegate(BaseWebViewController webView) - { - _webView = new WeakReference(webView); - } - - public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) - { - _webView.Get()?.OnLoadFinished(null, EventArgs.Empty); - } - - public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) - { - _webView.Get()?.OnLoadStarted(null, EventArgs.Empty); - } - - public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) - { - _webView.Get()?.OnLoadError(error); - } - - public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) - { - var ret = _webView.Get()?.ShouldStartLoad(webView, navigationAction) ?? true; - decisionHandler(ret ? WKNavigationActionPolicy.Allow : WKNavigationActionPolicy.Cancel); - } - } - - protected virtual bool ShouldStartLoad (WKWebView webView, WKNavigationAction navigationAction) - { - return true; - } - - protected virtual void OnLoadError (NSError error) - { - NetworkActivity.PopNetworkActive(); - - if (BackButton != null) - { - BackButton.Enabled = Web.CanGoBack; - ForwardButton.Enabled = Web.CanGoForward; - RefreshButton.Enabled = true; - } - } - - protected virtual void OnLoadStarted (object sender, EventArgs e) - { - NetworkActivity.PushNetworkActive(); - - if (RefreshButton != null) - RefreshButton.Enabled = false; - } - - protected virtual void OnLoadFinished(object sender, EventArgs e) - { - NetworkActivity.PopNetworkActive(); - - if (BackButton != null) - { - BackButton.Enabled = Web.CanGoBack; - ForwardButton.Enabled = Web.CanGoForward; - RefreshButton.Enabled = true; - } - - if (_showPageAsTitle) - { - Web.EvaluateJavaScript("document.title", (o, _) => { - Title = o as NSString; - }); - } - } - - public override void ViewWillDisappear(bool animated) - { - base.ViewWillDisappear(animated); - if (ToolbarItems != null) - NavigationController.SetToolbarHidden(true, animated); - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - Web = new WKWebView(View.Bounds, new WKWebViewConfiguration()); - Web.NavigationDelegate = new NavigationDelegate(this); - Add(Web); - } - - public override void ViewWillLayoutSubviews() - { - base.ViewWillLayoutSubviews(); - Web.Frame = View.Bounds; - } - - protected static string JavaScriptStringEncode(string data) - { - return System.Web.HttpUtility.JavaScriptStringEncode(data); - } - - protected static string UrlDecode(string data) - { - return System.Web.HttpUtility.UrlDecode(data); - } - - protected string LoadFile(string path) - { - if (path == null) - return string.Empty; - - var uri = Uri.EscapeUriString("file://" + path) + "#" + Environment.TickCount; - InvokeOnMainThread(() => Web.LoadRequest(new NSUrlRequest(new NSUrl(uri)))); - return uri; - } - - protected void LoadContent(string content) - { - Web.LoadHtmlString(content, NSBundle.MainBundle.BundleUrl); - } - - public override void ViewWillAppear(bool animated) - { - base.ViewWillAppear(animated); - - var bounds = View.Bounds; - if (_navigationToolbar) - bounds.Height -= NavigationController.Toolbar.Frame.Height; - Web.Frame = bounds; - - if (_navigationToolbar) - { - ToolbarItems = new [] - { - BackButton, - new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace) { Width = 40f }, - ForwardButton, - new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace), - RefreshButton - }; - - BackButton.Enabled = Web.CanGoBack; - ForwardButton.Enabled = Web.CanGoForward; - RefreshButton.Enabled = !Web.IsLoading; - } - - if (_showPageAsTitle) - { - Web.EvaluateJavaScript("document.title", (o, _) => { - Title = o as NSString; - }); - } - - if (ToolbarItems != null) - NavigationController.SetToolbarHidden(false, animated); - } - - public override void ViewDidDisappear(bool animated) - { - base.ViewDidDisappear(animated); - - if (_navigationToolbar) - ToolbarItems = null; - } - - public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) - { - base.DidRotate(fromInterfaceOrientation); - Web.Frame = View.Bounds; - } - } -} - +using System; +using UIKit; +using Foundation; +using WebKit; +using CodeHub.iOS.Utilities; + +namespace CodeHub.iOS.ViewControllers +{ + public abstract class BaseWebViewController : BaseViewController + { + protected UIBarButtonItem BackButton; + protected UIBarButtonItem RefreshButton; + protected UIBarButtonItem ForwardButton; + + public WKWebView Web { get; private set; } + private readonly bool _navigationToolbar; + private readonly bool _showPageAsTitle; + private bool _networkActivity; + + protected virtual void GoBack() + { + Web.GoBack(); + } + + protected virtual void Refresh() + { + Web.Reload(); + } + + protected virtual void GoForward() + { + Web.GoForward(); + } + + protected BaseWebViewController() + : this(true, true) + { + } + + protected BaseWebViewController(bool navigationToolbar, bool showPageAsTitle = false) + { + NavigationItem.BackBarButtonItem = new UIBarButtonItem { Title = "" }; + + _navigationToolbar = navigationToolbar; + _showPageAsTitle = showPageAsTitle; + + if (_navigationToolbar) + { + BackButton = new UIBarButtonItem { Image = Images.Web.BackButton, Enabled = false }; + ForwardButton = new UIBarButtonItem { Image = Images.Web.FowardButton, Enabled = false }; + RefreshButton = new UIBarButtonItem(UIBarButtonSystemItem.Refresh) { Enabled = false }; + + BackButton.TintColor = Theme.CurrentTheme.WebButtonTint; + ForwardButton.TintColor = Theme.CurrentTheme.WebButtonTint; + RefreshButton.TintColor = Theme.CurrentTheme.WebButtonTint; + + OnActivation(d => + { + d(BackButton.GetClickedObservable().Subscribe(_ => GoBack())); + d(ForwardButton.GetClickedObservable().Subscribe(_ => GoForward())); + d(RefreshButton.GetClickedObservable().Subscribe(_ => Refresh())); + }); + } + + EdgesForExtendedLayout = UIRectEdge.None; + } + + private class NavigationDelegate : WKNavigationDelegate + { + private readonly WeakReference _webView; + + public NavigationDelegate(BaseWebViewController webView) + { + _webView = new WeakReference(webView); + } + + public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) + { + _webView.Get()?.OnLoadFinished(null, EventArgs.Empty); + } + + public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) + { + _webView.Get()?.OnLoadStarted(null, EventArgs.Empty); + } + + public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) + { + _webView.Get()?.OnLoadError(error); + } + + public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) + { + var ret = _webView.Get()?.ShouldStartLoad(webView, navigationAction) ?? true; + decisionHandler(ret ? WKNavigationActionPolicy.Allow : WKNavigationActionPolicy.Cancel); + } + } + + protected virtual bool ShouldStartLoad (WKWebView webView, WKNavigationAction navigationAction) + { + return true; + } + + private void ActivateLoadingIndicator() + { + if (!_networkActivity) + NetworkActivity.PushNetworkActive(); + _networkActivity = true; + } + + private void DeactivateLoadingIndicator() + { + if (_networkActivity) + NetworkActivity.PopNetworkActive(); + _networkActivity = false; + } + + protected virtual void OnLoadError (NSError error) + { + if (BackButton != null) + { + BackButton.Enabled = Web.CanGoBack; + ForwardButton.Enabled = Web.CanGoForward; + RefreshButton.Enabled = true; + } + } + + protected virtual void OnLoadStarted (object sender, EventArgs e) + { + ActivateLoadingIndicator(); + + if (RefreshButton != null) + RefreshButton.Enabled = false; + } + + protected virtual void OnLoadFinished(object sender, EventArgs e) + { + DeactivateLoadingIndicator(); + + if (BackButton != null) + { + BackButton.Enabled = Web.CanGoBack; + ForwardButton.Enabled = Web.CanGoForward; + RefreshButton.Enabled = true; + } + + if (_showPageAsTitle) + { + Web.EvaluateJavaScript("document.title", (o, _) => { + Title = o as NSString; + }); + } + } + + public override void ViewWillDisappear(bool animated) + { + base.ViewWillDisappear(animated); + if (ToolbarItems != null) + NavigationController.SetToolbarHidden(true, animated); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + Web = new WKWebView(View.Bounds, new WKWebViewConfiguration()); + Web.NavigationDelegate = new NavigationDelegate(this); + Add(Web); + } + + public override void ViewWillLayoutSubviews() + { + base.ViewWillLayoutSubviews(); + Web.Frame = View.Bounds; + } + + protected static string JavaScriptStringEncode(string data) + { + return System.Web.HttpUtility.JavaScriptStringEncode(data); + } + + protected static string UrlDecode(string data) + { + return System.Web.HttpUtility.UrlDecode(data); + } + + protected string LoadFile(string path) + { + if (path == null) + return string.Empty; + + var uri = Uri.EscapeUriString("file://" + path) + "#" + Environment.TickCount; + InvokeOnMainThread(() => Web.LoadRequest(new NSUrlRequest(new NSUrl(uri)))); + return uri; + } + + protected void LoadContent(string content) + { + Web.LoadHtmlString(content, NSBundle.MainBundle.BundleUrl); + } + + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + + var bounds = View.Bounds; + if (_navigationToolbar) + bounds.Height -= NavigationController.Toolbar.Frame.Height; + Web.Frame = bounds; + + if (_navigationToolbar) + { + ToolbarItems = new [] + { + BackButton, + new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace) { Width = 40f }, + ForwardButton, + new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace), + RefreshButton + }; + + BackButton.Enabled = Web.CanGoBack; + ForwardButton.Enabled = Web.CanGoForward; + RefreshButton.Enabled = !Web.IsLoading; + } + + if (_showPageAsTitle) + { + Web.EvaluateJavaScript("document.title", (o, _) => { + Title = o as NSString; + }); + } + + if (ToolbarItems != null) + NavigationController.SetToolbarHidden(false, animated); + } + + public override void ViewDidDisappear(bool animated) + { + base.ViewDidDisappear(animated); + + if (_navigationToolbar) + ToolbarItems = null; + } + + public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation) + { + base.DidRotate(fromInterfaceOrientation); + Web.Frame = View.Bounds; + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Composer.cs b/CodeHub.iOS/ViewControllers/Composer.cs index 4760ed05..4a91f729 100755 --- a/CodeHub.iOS/ViewControllers/Composer.cs +++ b/CodeHub.iOS/ViewControllers/Composer.cs @@ -9,8 +9,7 @@ namespace CodeHub.iOS.ViewControllers { public class Composer : BaseViewController { - protected UIBarButtonItem SendItem; - UIViewController _previousController; + public UIBarButtonItem SendItem; public Action ReturnAction; protected readonly UITextView TextView; protected UIView ScrollingToolbarView; @@ -28,12 +27,10 @@ public Composer () : base (null, null) Title = "New Comment"; EdgesForExtendedLayout = UIRectEdge.None; - var close = new UIBarButtonItem { Image = Images.Buttons.CancelButton }; - NavigationItem.LeftBarButtonItem = close; - SendItem = new UIBarButtonItem { Image = Images.Buttons.SaveButton }; + SendItem = new UIBarButtonItem(UIBarButtonSystemItem.Save); NavigationItem.RightBarButtonItem = SendItem; - TextView = new UITextView(ComputeComposerSize(CGRect.Empty)); + TextView = new UITextView(new CGRect(CGPoint.Empty, View.Bounds.Size)); TextView.Font = UIFont.PreferredBody; TextView.AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth; @@ -45,12 +42,6 @@ public Composer () : base (null, null) _normalButtonImage = ImageFromColor(UIColor.White); _pressedButtonImage = ImageFromColor(UIColor.FromWhiteAlpha(0.0f, 0.4f)); - - OnActivation(d => - { - d(close.GetClickedObservable().Subscribe(_ => CloseComposer())); - d(SendItem.GetClickedObservable().Subscribe(_ => PostCallback())); - }); } private UIImage ImageFromColor(UIColor color) @@ -141,53 +132,30 @@ public string Text set { TextView.Text = value; } } - public void CloseComposer () - { - SendItem.Enabled = true; - _previousController.DismissViewController(true, null); - } - - void PostCallback () - { - SendItem.Enabled = false; - TextView.ResignFirstResponder(); - - try - { - if (ReturnAction != null) - ReturnAction(Text); - } - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine(e.Message + " - " + e.StackTrace); - } - } - - void KeyboardWillShow (NSNotification notification) + void KeyboardChange(NSNotification notification) { - var nsValue = notification.UserInfo.ObjectForKey (UIKeyboard.BoundsUserInfoKey) as NSValue; + var nsValue = notification.UserInfo.ObjectForKey (UIKeyboard.FrameEndUserInfoKey) as NSValue; if (nsValue == null) return; - var kbdBounds = nsValue.RectangleFValue; - UIView.Animate(1.0f, 0, UIViewAnimationOptions.CurveEaseIn, () => TextView.Frame = ComputeComposerSize (kbdBounds), null); - } - void KeyboardWillHide (NSNotification notification) - { - TextView.Frame = ComputeComposerSize(new CGRect(0, 0, 0, 0)); - } + var kbdBounds = nsValue.RectangleFValue; + var keyboard = View.Window.ConvertRectToView(kbdBounds, View); - CGRect ComputeComposerSize (CGRect kbdBounds) - { - var view = View.Bounds; - return new CGRect (0, 0, view.Width, view.Height-kbdBounds.Height); - } + UIView.Animate( + 1.0f, 0, UIViewAnimationOptions.CurveEaseIn, + () => TextView.Frame = new CGRect(0, 0, View.Bounds.Width, keyboard.Top), null); + } NSObject _hideNotification, _showNotification; public override void ViewWillAppear (bool animated) { - base.ViewWillAppear (animated); - _showNotification = NSNotificationCenter.DefaultCenter.AddObserver (new NSString("UIKeyboardWillShowNotification"), KeyboardWillShow); - _hideNotification = NSNotificationCenter.DefaultCenter.AddObserver (new NSString("UIKeyboardWillHideNotification"), KeyboardWillHide); + base.ViewWillAppear (animated); + + _showNotification = NSNotificationCenter.DefaultCenter.AddObserver( + new NSString("UIKeyboardWillShowNotification"), KeyboardChange); + + _hideNotification = NSNotificationCenter.DefaultCenter.AddObserver( + new NSString("UIKeyboardWillHideNotification"), KeyboardChange); + TextView.BecomeFirstResponder (); } @@ -198,16 +166,26 @@ public override void ViewWillDisappear(bool animated) NSNotificationCenter.DefaultCenter.RemoveObserver(_hideNotification); if (_showNotification != null) NSNotificationCenter.DefaultCenter.RemoveObserver(_showNotification); - } - - public void NewComment (UIViewController parent, Action action) + } + + public void PresentAsModal(UIViewController parent, Action onSave, Action onClose = null) { - Title = Title; - ReturnAction = action; - _previousController = parent; - TextView.BecomeFirstResponder (); - var nav = new UINavigationController(this); - parent.PresentViewController(nav, true, null); + onClose = onClose ?? new Action(() => parent.DismissViewController(true, null)); + + var closeButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + NavigationItem.LeftBarButtonItem = closeButton; + + OnActivation(d => + { + d(closeButton.GetClickedObservable() + .Subscribe(_ => onClose?.Invoke())); + + d(SendItem.GetClickedObservable() + .Subscribe(_ => onSave?.Invoke(this.Text))); + }); + + var navigationController = new UINavigationController(this); + parent.PresentViewController(navigationController, true, null); } } } diff --git a/CodeHub.iOS/ViewControllers/DialogViewController.cs b/CodeHub.iOS/ViewControllers/DialogViewController.cs index e2e3f96e..77949496 100644 --- a/CodeHub.iOS/ViewControllers/DialogViewController.cs +++ b/CodeHub.iOS/ViewControllers/DialogViewController.cs @@ -29,15 +29,7 @@ public class DialogViewController : TableViewController private UISearchBar _searchBar; bool pushing; - /// - /// The root element displayed by the DialogViewController, the value can be changed during runtime to update the contents. - /// - public RootElement Root { - get - { - return _rootElement.Value; - } - } + public RootElement Root => _rootElement.Value; public bool EnableSearch { get; set; } @@ -173,12 +165,8 @@ protected virtual void DidScroll(CGPoint p) public class Source : UITableViewSource { private readonly WeakReference _container; - private readonly RootElement _root; - public RootElement Root - { - get { return _root; } - } + public RootElement Root => _container.Get()?.Root; public DialogViewController Container { @@ -188,7 +176,6 @@ public DialogViewController Container public Source (DialogViewController container) { _container = new WeakReference(container); - _root = container.Root; } public override nint RowsInSection (UITableView tableview, nint section) diff --git a/CodeHub.iOS/Views/Events/BaseEventsView.cs b/CodeHub.iOS/ViewControllers/Events/BaseEventsViewController.cs similarity index 83% rename from CodeHub.iOS/Views/Events/BaseEventsView.cs rename to CodeHub.iOS/ViewControllers/Events/BaseEventsViewController.cs index 439bd7ec..816e8980 100644 --- a/CodeHub.iOS/Views/Events/BaseEventsView.cs +++ b/CodeHub.iOS/ViewControllers/Events/BaseEventsViewController.cs @@ -1,165 +1,191 @@ -using System; -using CodeHub.iOS.DialogElements; -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.Events; -using GitHubSharp.Models; -using UIKit; -using System.Collections.Generic; -using CodeHub.Core.Utilities; - -namespace CodeHub.iOS.Views.Events +using System; +using CodeHub.iOS.DialogElements; +using CodeHub.Core.ViewModels.Events; +using GitHubSharp.Models; +using UIKit; +using System.Collections.Generic; +using CodeHub.Core.Utilities; +using CodeHub.iOS.ViewControllers.Source; +using System.Reactive.Linq; + +namespace CodeHub.iOS.ViewControllers.Events { - public abstract class BaseEventsView : ViewModelCollectionDrivenDialogViewController - { - private static IDictionary _eventToImage - = new Dictionary - { - {EventType.Unknown, Octicon.Alert}, - {EventType.Branch, Octicon.GitBranch}, - {EventType.Comment, Octicon.Comment}, - {EventType.Commit, Octicon.GitCommit}, - {EventType.Delete, Octicon.Trashcan}, - {EventType.Follow, Octicon.Person}, - {EventType.Fork, Octicon.RepoForked}, - {EventType.Gist, Octicon.Gist}, - {EventType.Issue, Octicon.IssueOpened}, - {EventType.Organization, Octicon.Organization}, - {EventType.Public, Octicon.Globe}, - {EventType.PullRequest, Octicon.GitPullRequest}, - {EventType.Repository, Octicon.Repo}, - {EventType.Star, Octicon.Star}, - {EventType.Tag, Octicon.Tag}, - {EventType.Wiki, Octicon.Pencil}, - }; + public abstract class BaseEventsViewController : ViewModelCollectionDrivenDialogViewController + { + public new BaseEventsViewModel ViewModel + { + get { return (BaseEventsViewModel)base.ViewModel; } + set { base.ViewModel = value; } + } - protected BaseEventsView() + private static IDictionary _eventToImage + = new Dictionary + { + {EventType.Unknown, Octicon.Alert}, + {EventType.Branch, Octicon.GitBranch}, + {EventType.Comment, Octicon.Comment}, + {EventType.Commit, Octicon.GitCommit}, + {EventType.Delete, Octicon.Trashcan}, + {EventType.Follow, Octicon.Person}, + {EventType.Fork, Octicon.RepoForked}, + {EventType.Gist, Octicon.Gist}, + {EventType.Issue, Octicon.IssueOpened}, + {EventType.Organization, Octicon.Organization}, + {EventType.Public, Octicon.Globe}, + {EventType.PullRequest, Octicon.GitPullRequest}, + {EventType.Repository, Octicon.Repo}, + {EventType.Star, Octicon.Star}, + {EventType.Tag, Octicon.Tag}, + {EventType.Wiki, Octicon.Pencil}, + }; + + protected BaseEventsViewController() { Title = "Events"; EnableSearch = false; - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 64f; - BindCollection(((BaseEventsViewModel)ViewModel).Events, CreateElement); - } - + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 64f; + BindCollection(ViewModel.Events, CreateElement); + + this.OnActivation(d => + { + d(ViewModel.GoToBranchCommand + .Subscribe(branch => + { + var viewController = new SourceTreeViewController( + branch.Item1.Owner, branch.Item1.Name, null, branch.Item2, Utilities.ShaType.Branch); + this.PushViewController(viewController); + })); + + d(ViewModel.GoToTagCommand + .Subscribe(branch => + { + var viewController = new SourceTreeViewController( + branch.Item1.Owner, branch.Item1.Name, null, branch.Item2, Utilities.ShaType.Tag); + this.PushViewController(viewController); + })); + }); + } + private static Element CreateElement(Tuple e) { - try - { - if (e.Item2 == null) - return null; - - var imgKey = ChooseImage(e.Item1); - var img = Octicon.Alert; - if (_eventToImage.ContainsKey(imgKey)) - img = _eventToImage[imgKey]; - - var avatar = e.Item1.Actor != null ? e.Item1.Actor.AvatarUrl : null; - var headerBlocks = new List(); - foreach (var h in e.Item2.Header) - { - Action act = null; - var anchorBlock = h as BaseEventsViewModel.AnchorBlock; - if (anchorBlock != null) - act = anchorBlock.Tapped; - headerBlocks.Add(new NewsFeedElement.TextBlock(h.Text, act)); - } - - var bodyBlocks = new List(); - foreach (var h in e.Item2.Body) - { - Action act = null; - var anchorBlock = h as BaseEventsViewModel.AnchorBlock; - if (anchorBlock != null) - act = anchorBlock.Tapped; - var block = new NewsFeedElement.TextBlock(h.Text, act); - bodyBlocks.Add(block); - } - - var weakTapped = new WeakReference(e.Item2.Tapped); - var githubAvatar = new GitHubAvatar(avatar).ToUri(64)?.AbsoluteUri; + try + { + if (e.Item2 == null) + return null; + + var imgKey = ChooseImage(e.Item1); + var img = Octicon.Alert; + if (_eventToImage.ContainsKey(imgKey)) + img = _eventToImage[imgKey]; + + var avatar = e.Item1.Actor != null ? e.Item1.Actor.AvatarUrl : null; + var headerBlocks = new List(); + foreach (var h in e.Item2.Header) + { + Action act = null; + var anchorBlock = h as BaseEventsViewModel.AnchorBlock; + if (anchorBlock != null) + act = anchorBlock.Tapped; + headerBlocks.Add(new NewsFeedElement.TextBlock(h.Text, act)); + } + + var bodyBlocks = new List(); + foreach (var h in e.Item2.Body) + { + Action act = null; + var anchorBlock = h as BaseEventsViewModel.AnchorBlock; + if (anchorBlock != null) + act = anchorBlock.Tapped; + var block = new NewsFeedElement.TextBlock(h.Text, act); + bodyBlocks.Add(block); + } + + var weakTapped = new WeakReference(e.Item2.Tapped); + var githubAvatar = new GitHubAvatar(avatar).ToUri(64)?.AbsoluteUri; return new NewsFeedElement(githubAvatar, e.Item1.CreatedAt, headerBlocks, bodyBlocks, img.ToImage(), () => weakTapped.Get()?.Invoke(), e.Item2.Multilined); } catch (Exception ex) - { + { System.Diagnostics.Debug.WriteLine("Unable to add event: " + ex.Message); return null; } } - private static EventType ChooseImage(EventModel eventModel) - { - if (eventModel.PayloadObject is EventModel.CommitCommentEvent) - return EventType.Comment; - - var createEvent = eventModel.PayloadObject as EventModel.CreateEvent; - if (createEvent != null) - { - var createModel = createEvent; - if (createModel.RefType.Equals("repository")) - return EventType.Repository; - if (createModel.RefType.Equals("branch")) - return EventType.Branch; - if (createModel.RefType.Equals("tag")) - return EventType.Tag; - } - else if (eventModel.PayloadObject is EventModel.DeleteEvent) - return EventType.Delete; - else if (eventModel.PayloadObject is EventModel.FollowEvent) - return EventType.Follow; - else if (eventModel.PayloadObject is EventModel.ForkEvent) - return EventType.Fork; - else if (eventModel.PayloadObject is EventModel.ForkApplyEvent) - return EventType.Fork; - else if (eventModel.PayloadObject is EventModel.GistEvent) - return EventType.Gist; - else if (eventModel.PayloadObject is EventModel.GollumEvent) - return EventType.Wiki; - else if (eventModel.PayloadObject is EventModel.IssueCommentEvent) - return EventType.Comment; - else if (eventModel.PayloadObject is EventModel.IssuesEvent) - return EventType.Issue; - else if (eventModel.PayloadObject is EventModel.MemberEvent) - return EventType.Organization; - else if (eventModel.PayloadObject is EventModel.PublicEvent) - return EventType.Public; - else if (eventModel.PayloadObject is EventModel.PullRequestEvent) - return EventType.PullRequest; - else if (eventModel.PayloadObject is EventModel.PullRequestReviewCommentEvent) - return EventType.Comment; - else if (eventModel.PayloadObject is EventModel.PushEvent) - return EventType.Commit; - else if (eventModel.PayloadObject is EventModel.TeamAddEvent) - return EventType.Organization; - else if (eventModel.PayloadObject is EventModel.WatchEvent) - return EventType.Star; - else if (eventModel.PayloadObject is EventModel.ReleaseEvent) - return EventType.Tag; - return EventType.Unknown; - } - - public enum EventType - { - Unknown = 0, - Comment, - Repository, - Branch, - Tag, - Delete, - Follow, - Fork, - Gist, - Wiki, - Issue, - Organization, - Public, - PullRequest, - Star, - Commit + private static EventType ChooseImage(EventModel eventModel) + { + if (eventModel.PayloadObject is EventModel.CommitCommentEvent) + return EventType.Comment; + + var createEvent = eventModel.PayloadObject as EventModel.CreateEvent; + if (createEvent != null) + { + var createModel = createEvent; + if (createModel.RefType.Equals("repository")) + return EventType.Repository; + if (createModel.RefType.Equals("branch")) + return EventType.Branch; + if (createModel.RefType.Equals("tag")) + return EventType.Tag; + } + else if (eventModel.PayloadObject is EventModel.DeleteEvent) + return EventType.Delete; + else if (eventModel.PayloadObject is EventModel.FollowEvent) + return EventType.Follow; + else if (eventModel.PayloadObject is EventModel.ForkEvent) + return EventType.Fork; + else if (eventModel.PayloadObject is EventModel.ForkApplyEvent) + return EventType.Fork; + else if (eventModel.PayloadObject is EventModel.GistEvent) + return EventType.Gist; + else if (eventModel.PayloadObject is EventModel.GollumEvent) + return EventType.Wiki; + else if (eventModel.PayloadObject is EventModel.IssueCommentEvent) + return EventType.Comment; + else if (eventModel.PayloadObject is EventModel.IssuesEvent) + return EventType.Issue; + else if (eventModel.PayloadObject is EventModel.MemberEvent) + return EventType.Organization; + else if (eventModel.PayloadObject is EventModel.PublicEvent) + return EventType.Public; + else if (eventModel.PayloadObject is EventModel.PullRequestEvent) + return EventType.PullRequest; + else if (eventModel.PayloadObject is EventModel.PullRequestReviewCommentEvent) + return EventType.Comment; + else if (eventModel.PayloadObject is EventModel.PushEvent) + return EventType.Commit; + else if (eventModel.PayloadObject is EventModel.TeamAddEvent) + return EventType.Organization; + else if (eventModel.PayloadObject is EventModel.WatchEvent) + return EventType.Star; + else if (eventModel.PayloadObject is EventModel.ReleaseEvent) + return EventType.Tag; + return EventType.Unknown; + } + + public enum EventType + { + Unknown = 0, + Comment, + Repository, + Branch, + Tag, + Delete, + Follow, + Fork, + Gist, + Wiki, + Issue, + Organization, + Public, + PullRequest, + Star, + Commit } } } \ No newline at end of file diff --git a/CodeHub.iOS/ViewControllers/Events/NewsViewController.cs b/CodeHub.iOS/ViewControllers/Events/NewsViewController.cs new file mode 100644 index 00000000..5cbd4d09 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Events/NewsViewController.cs @@ -0,0 +1,16 @@ +using CodeHub.Core.ViewModels.Events; + +namespace CodeHub.iOS.ViewControllers.Events +{ + public class NewsViewController : BaseEventsViewController + { + public NewsViewController() + { + Title = "News"; + } + + public static NewsViewController Create() + => new NewsViewController { ViewModel = new NewsViewModel() }; + } +} + diff --git a/CodeHub.iOS/ViewControllers/Events/OrganizationEventsViewController.cs b/CodeHub.iOS/ViewControllers/Events/OrganizationEventsViewController.cs new file mode 100644 index 00000000..a2445042 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Events/OrganizationEventsViewController.cs @@ -0,0 +1,7 @@ +namespace CodeHub.iOS.ViewControllers.Events +{ + public class OrganizationEventsViewController : BaseEventsViewController + { + } +} + diff --git a/CodeHub.iOS/ViewControllers/Events/RepositoryEventsViewController.cs b/CodeHub.iOS/ViewControllers/Events/RepositoryEventsViewController.cs new file mode 100644 index 00000000..6e8535b3 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Events/RepositoryEventsViewController.cs @@ -0,0 +1,23 @@ +using CodeHub.Core.ViewModels.Events; + +namespace CodeHub.iOS.ViewControllers.Events +{ + public class RepositoryEventsViewController : BaseEventsViewController + { + public RepositoryEventsViewController() + { + } + + public RepositoryEventsViewController(string username, string repository) + { + var viewModel = new RepositoryEventsViewModel(); + viewModel.Init(new RepositoryEventsViewModel.NavObject + { + Username = username, + Repository = repository + }); + ViewModel = viewModel; + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Events/UserEventsViewController.cs b/CodeHub.iOS/ViewControllers/Events/UserEventsViewController.cs new file mode 100644 index 00000000..671a9478 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Events/UserEventsViewController.cs @@ -0,0 +1,18 @@ +using CodeHub.Core.ViewModels.Events; + +namespace CodeHub.iOS.ViewControllers.Events +{ + public class UserEventsViewController : BaseEventsViewController + { + public UserEventsViewController() + { + } + + public UserEventsViewController(string username) + { + var viewModel = new UserEventsViewModel(); + viewModel.Init(new UserEventsViewModel.NavObject { Username = username }); + ViewModel = viewModel; + } + } +} \ No newline at end of file diff --git a/CodeHub.iOS/ViewControllers/FilterViewController.cs b/CodeHub.iOS/ViewControllers/FilterViewController.cs index 92d2bce4..cb9f7037 100755 --- a/CodeHub.iOS/ViewControllers/FilterViewController.cs +++ b/CodeHub.iOS/ViewControllers/FilterViewController.cs @@ -14,8 +14,8 @@ protected FilterViewController() { Title = "Filter & Sort"; - var cancel = NavigationItem.LeftBarButtonItem = new UIBarButtonItem { Image = Images.Buttons.CancelButton }; - var save = NavigationItem.RightBarButtonItem = new UIBarButtonItem { Image = Images.Buttons.SaveButton }; + var cancel = NavigationItem.LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + var save = NavigationItem.RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Save); OnActivation(d => { diff --git a/CodeHub.iOS/Views/Filters/IssueMilestonesFilterViewController.cs b/CodeHub.iOS/ViewControllers/Filters/IssueMilestonesFilterViewController.cs similarity index 97% rename from CodeHub.iOS/Views/Filters/IssueMilestonesFilterViewController.cs rename to CodeHub.iOS/ViewControllers/Filters/IssueMilestonesFilterViewController.cs index 91c8bbe4..46691b51 100644 --- a/CodeHub.iOS/Views/Filters/IssueMilestonesFilterViewController.cs +++ b/CodeHub.iOS/ViewControllers/Filters/IssueMilestonesFilterViewController.cs @@ -1,12 +1,11 @@ using System; using GitHubSharp.Models; -using CodeHub.iOS.ViewControllers; using System.Linq; using CodeHub.Core.ViewModels; using CodeHub.iOS.Utilities; using CodeHub.iOS.DialogElements; -namespace CodeHub.iOS.Views.Filters +namespace CodeHub.iOS.ViewControllers.Filters { public class IssueMilestonesFilterViewController : DialogViewController { diff --git a/CodeHub.iOS/Views/Filters/IssuesFilterViewController.cs b/CodeHub.iOS/ViewControllers/Filters/IssuesFilterViewController.cs similarity index 98% rename from CodeHub.iOS/Views/Filters/IssuesFilterViewController.cs rename to CodeHub.iOS/ViewControllers/Filters/IssuesFilterViewController.cs index 3f1cd432..ea600cd4 100644 --- a/CodeHub.iOS/Views/Filters/IssuesFilterViewController.cs +++ b/CodeHub.iOS/ViewControllers/Filters/IssuesFilterViewController.cs @@ -1,11 +1,10 @@ using System; using UIKit; -using CodeHub.iOS.ViewControllers; using CodeHub.Core.ViewModels; using CodeHub.Core.Filters; using CodeHub.iOS.DialogElements; -namespace CodeHub.iOS.Views.Filters +namespace CodeHub.iOS.ViewControllers.Filters { public class IssuesFilterViewController : FilterViewController { diff --git a/CodeHub.iOS/Views/Filters/MyIssuesFilterViewController.cs b/CodeHub.iOS/ViewControllers/Filters/MyIssuesFilterViewController.cs similarity index 97% rename from CodeHub.iOS/Views/Filters/MyIssuesFilterViewController.cs rename to CodeHub.iOS/ViewControllers/Filters/MyIssuesFilterViewController.cs index 6a2048ee..59d2f5af 100644 --- a/CodeHub.iOS/Views/Filters/MyIssuesFilterViewController.cs +++ b/CodeHub.iOS/ViewControllers/Filters/MyIssuesFilterViewController.cs @@ -1,11 +1,10 @@ using System; using UIKit; -using CodeHub.iOS.ViewControllers; using CodeHub.Core.ViewModels; using CodeHub.Core.Filters; using CodeHub.iOS.DialogElements; -namespace CodeHub.iOS.Views.Filters +namespace CodeHub.iOS.ViewControllers.Filters { public class MyIssuesFilterViewController : FilterViewController { diff --git a/CodeHub.iOS/Views/Gists/GistCreateView.cs b/CodeHub.iOS/ViewControllers/Gists/GistCreateViewController.cs similarity index 86% rename from CodeHub.iOS/Views/Gists/GistCreateView.cs rename to CodeHub.iOS/ViewControllers/Gists/GistCreateViewController.cs index c55a7ba3..020032a0 100755 --- a/CodeHub.iOS/Views/Gists/GistCreateView.cs +++ b/CodeHub.iOS/ViewControllers/Gists/GistCreateViewController.cs @@ -1,62 +1,65 @@ -using System; -using CodeHub.iOS; +using System; using UIKit; -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.Gists; -using CodeHub.iOS.Utilities; -using CodeHub.iOS.DialogElements; -using System.Collections.Generic; -using CodeHub.iOS.ViewControllers.Gists; - -namespace CodeHub.iOS.Views.Gists +using CodeHub.Core.ViewModels.Gists; +using CodeHub.iOS.Utilities; +using CodeHub.iOS.DialogElements; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Linq; +using ReactiveUI; + +namespace CodeHub.iOS.ViewControllers.Gists { - public class GistCreateView : DialogViewController - { - public GistCreateViewModel ViewModel { get; } - - public GistCreateView() : base(UITableViewStyle.Grouped) - { - Title = "Create Gist"; - ViewModel = new GistCreateViewModel(); - } - - public static GistCreateView Show(UIViewController parent) - { - var ctrl = new GistCreateView(); - var weakVm = new WeakReference(ctrl.ViewModel); - ctrl.ViewModel.SaveCommand.Subscribe(_ => parent.DismissViewController(true, null)); - ctrl.NavigationItem.LeftBarButtonItem = new UIBarButtonItem { Image = Images.Buttons.CancelButton }; - ctrl.NavigationItem.LeftBarButtonItem.GetClickedObservable().Subscribe(_ => { - weakVm.Get()?.CancelCommand.Execute(null); - parent.DismissViewController(true, null); - }); - parent.PresentViewController(new ThemedNavigationController(ctrl), true, null); - return ctrl; + public class GistCreateViewController : DialogViewController + { + public GistCreateViewModel ViewModel { get; } + + public GistCreateViewController() : base(UITableViewStyle.Grouped) + { + Title = "Create Gist"; + ViewModel = new GistCreateViewModel(); } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44f; - - var saveButton = NavigationItem.RightBarButtonItem = new UIBarButtonItem { Image = Images.Buttons.SaveButton }; - - OnActivation(d => - { - d(saveButton.GetClickedObservable().BindCommand(ViewModel.SaveCommand)); - d(ViewModel.Bind(x => x.Description).Subscribe(_ => UpdateView())); - d(ViewModel.Bind(x => x.Files).Subscribe(_ => UpdateView())); - d(ViewModel.Bind(x => x.Public).Subscribe(_ => UpdateView())); - d(ViewModel.Bind(x => x.IsSaving).SubscribeStatus("Saving...")); - }); + + public static GistCreateViewController Show(UIViewController parent) + { + var ctrl = new GistCreateViewController(); + var weakVm = new WeakReference(ctrl.ViewModel); + ctrl.ViewModel.SaveCommand.Subscribe(_ => parent.DismissViewController(true, null)); + ctrl.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + ctrl.NavigationItem.LeftBarButtonItem.GetClickedObservable().Subscribe(_ => { + weakVm.Get()?.CancelCommand.Execute().Subscribe(); + parent.DismissViewController(true, null); + }); + parent.PresentViewController(new ThemedNavigationController(ctrl), true, null); + return ctrl; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44f; + + var saveButton = NavigationItem.RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Save); + + OnActivation(d => + { + d(ViewModel.Bind(x => x.Description).Subscribe(_ => UpdateView())); + d(ViewModel.Bind(x => x.Files).Subscribe(_ => UpdateView())); + d(ViewModel.Bind(x => x.Public).Subscribe(_ => UpdateView())); + d(ViewModel.Bind(x => x.IsSaving).SubscribeStatus("Saving...")); + + d(saveButton.GetClickedObservable() + .Select(_ => Unit.Default) + .InvokeReactiveCommand(ViewModel.SaveCommand)); + }); } int _gistFileCounter = 0; private void AddFile() { - var createController = new GistFileAddViewController(); + var createController = new GistFileAddViewController(); createController.SaveCommand.Subscribe(_ => NavigationController.PopToViewController(this, true)); createController.Save = (name, content) => { if (string.IsNullOrEmpty(name)) @@ -73,9 +76,9 @@ private void AddFile() if (ViewModel.Files.ContainsKey(name)) throw new InvalidOperationException("A filename by that type already exists"); - ViewModel.Files.Add(name, content); + ViewModel.Files.Add(name, content); ViewModel.Files = ViewModel.Files; - }; + }; NavigationController.PushViewController(createController, true); } @@ -88,15 +91,15 @@ public override void ViewWillAppear(bool animated) protected void UpdateView() { - ICollection
sections = new LinkedList
(); + ICollection
sections = new LinkedList
(); var section = new Section(); sections.Add(section); var desc = new MultilinedElement("Description", ViewModel.Description); desc.Clicked.Subscribe(_ => ChangeDescription()); section.Add(desc); - - var pub = new BooleanElement("Public", ViewModel.Public); + + var pub = new BooleanElement("Public", ViewModel.Public); pub.Changed.Subscribe(x => ViewModel.Public = x); section.Add(pub); @@ -110,11 +113,11 @@ protected void UpdateView() continue; var size = System.Text.Encoding.UTF8.GetByteCount(ViewModel.Files[file]); - var el = new StringElement(file, size + " bytes", UITableViewCellStyle.Subtitle) { Accessory = UITableViewCellAccessory.DisclosureIndicator }; + var el = new StringElement(file, size + " bytes", UITableViewCellStyle.Subtitle) { Accessory = UITableViewCellAccessory.DisclosureIndicator }; el.Clicked.Subscribe(_ => { if (!ViewModel.Files.ContainsKey(key)) return; - var createController = new GistFileEditViewController { Filename = key, Content = ViewModel.Files[key] }; + var createController = new GistFileEditViewController { Filename = key, Content = ViewModel.Files[key] }; createController.SaveCommand.Subscribe(__ => NavigationController.PopToViewController(this, true)); createController.Save = (name, content) => { @@ -126,7 +129,7 @@ protected void UpdateView() throw new InvalidOperationException("A filename by that type already exists"); ViewModel.Files.Remove(key); - ViewModel.Files[name] = content; + ViewModel.Files[name] = content; ViewModel.Files = ViewModel.Files; // Trigger refresh }; @@ -134,8 +137,8 @@ protected void UpdateView() }); fileSection.Add(el); } - - var add = new StringElement("Add New File"); + + var add = new StringElement("Add New File"); add.Clicked.Subscribe(_ => AddFile()); fileSection.Add(add); @@ -144,10 +147,16 @@ protected void UpdateView() private void ChangeDescription() { - var composer = new Composer { Title = "Description", Text = ViewModel.Description }; - composer.NewComment(this, (text) => { + var composer = new Composer + { + Title = "Description", + Text = ViewModel.Description + }; + + composer.PresentAsModal(this, text => + { ViewModel.Description = text; - composer.CloseComposer(); + this.DismissViewController(true, null); }); } @@ -157,15 +166,15 @@ public override DialogViewController.Source CreateSizingSource() } private void Delete(Element element) - { - var e = element as StringElement; - if (e != null) + { + var e = element as StringElement; + if (e != null) ViewModel.Files.Remove(e.Caption); } private class EditSource : Source { - public EditSource(GistCreateView dvc) + public EditSource(GistCreateViewController dvc) : base (dvc) { } @@ -189,7 +198,7 @@ public override void CommitEditingStyle(UITableView tableView, UITableViewCellEd case UITableViewCellEditingStyle.Delete: var section = Root?[indexPath.Section]; var element = section?[indexPath.Row]; - (Container as GistCreateView)?.Delete(element); + (Container as GistCreateViewController)?.Delete(element); section?.Remove(element); break; } diff --git a/CodeHub.iOS/Views/Gists/EditGistController.cs b/CodeHub.iOS/ViewControllers/Gists/GistEditViewController.cs similarity index 79% rename from CodeHub.iOS/Views/Gists/EditGistController.cs rename to CodeHub.iOS/ViewControllers/Gists/GistEditViewController.cs index 3376e127..21c63988 100755 --- a/CodeHub.iOS/Views/Gists/EditGistController.cs +++ b/CodeHub.iOS/ViewControllers/Gists/GistEditViewController.cs @@ -1,37 +1,34 @@ -using System; -using CodeHub.iOS; -using GitHubSharp.Models; +using System; using System.Collections.Generic; using UIKit; using System.Linq; -using CodeHub.iOS.ViewControllers; -using CodeHub.iOS.Utilities; -using CodeHub.iOS.Services; -using CodeHub.iOS.DialogElements; -using System.Threading.Tasks; +using CodeHub.iOS.Utilities; +using CodeHub.iOS.Services; +using CodeHub.iOS.DialogElements; +using System.Threading.Tasks; using CodeHub.iOS.ViewControllers.Gists; +using Octokit; -namespace CodeHub.iOS.Views +namespace CodeHub.iOS.ViewControllers { - public class EditGistController : DialogViewController + public class GistEditViewController : DialogViewController { - private GistEditModel _model; - public Action Created; - private GistModel _originalGist; + private GistUpdate _model; + public Action Created; + private Gist _originalGist; - public EditGistController(GistModel gist) + public GistEditViewController(Gist gist) : base(UITableViewStyle.Grouped, true) { Title = "Edit Gist"; _originalGist = gist; - _model = new GistEditModel(); + _model = new GistUpdate(); _model.Description = gist.Description; - _model.Files = new Dictionary(); if (gist.Files != null) foreach (var f in gist.Files) - _model.Files.Add(f.Key, new GistEditModel.File() { Content = f.Value.Content }); + _model.Files.Add(f.Key, new GistFileUpdate { Content = f.Value.Content }); } private void Discard() @@ -45,31 +42,35 @@ private async Task Save() { AlertDialogService.ShowAlert("No Files", "You cannot modify a Gist without atleast one file"); return; - } - - var app = MvvmCross.Platform.Mvx.Resolve(); - var hud = this.CreateHud(); - NetworkActivity.PushNetworkActive(); - - try - { - hud.Show("Saving..."); - var newGist = await app.Client.ExecuteAsync(app.Client.Gists[_originalGist.Id].EditGist(_model)); - Created?.Invoke(newGist.Data); - DismissViewController(true, null); - } - finally - { - hud.Hide(); - NetworkActivity.PopNetworkActive(); - } + } + + var app = MvvmCross.Platform.Mvx.Resolve(); + var hud = this.CreateHud(); + NetworkActivity.PushNetworkActive(); + + try + { + hud.Show("Saving..."); + var newGist = await app.GitHubClient.Gist.Edit(_originalGist.Id, _model); + Created?.Invoke(newGist); + DismissViewController(true, null); + } + catch (Exception e) + { + AlertDialogService.ShowAlert("Error", "Unable to save gist: " + e.Message); + } + finally + { + hud.Hide(); + NetworkActivity.PopNetworkActive(); + } } private bool IsDuplicateName(string name) { if (_model.Files.Count(x => x.Key.Equals(name) && x.Value != null) > 0) return true; - return _model.Files.Count(x => x.Value != null && name.Equals(x.Value.Filename)) > 0; + return _model.Files.Count(x => x.Value != null && name.Equals(x.Value.NewFileName)) > 0; } int _gistFileCounter = 0; @@ -89,7 +90,7 @@ private string GenerateName() private void AddFile() { - var createController = new GistFileAddViewController(); + var createController = new GistFileAddViewController(); createController.SaveCommand.Subscribe(_ => NavigationController.PopToViewController(this, true)); createController.Save = (name, content) => { if (string.IsNullOrEmpty(name)) @@ -97,39 +98,39 @@ private void AddFile() if (IsDuplicateName(name)) throw new InvalidOperationException("A filename by that type already exists"); - _model.Files[name] = new GistEditModel.File { Content = content }; + _model.Files[name] = new GistFileUpdate { Content = content }; }; NavigationController.PushViewController(createController, true); - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44f; - - var cancelButton = new UIBarButtonItem { Image = Images.Buttons.CancelButton }; - var saveButton = new UIBarButtonItem { Image = Images.Buttons.SaveButton }; - NavigationItem.LeftBarButtonItem = cancelButton; - NavigationItem.RightBarButtonItem = saveButton; - - OnActivation(d => - { - d(cancelButton.GetClickedObservable().Subscribe(_ => Discard())); - d(saveButton.GetClickedObservable().Subscribe(_ => Save())); - d(_descriptionElement.Clicked.Subscribe(_ => ChangeDescription())); - d(_addFileElement.Clicked.Subscribe(_ => AddFile())); - }); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44f; + + var cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel); + var saveButton = new UIBarButtonItem(UIBarButtonSystemItem.Save); + NavigationItem.LeftBarButtonItem = cancelButton; + NavigationItem.RightBarButtonItem = saveButton; + + OnActivation(d => + { + d(cancelButton.GetClickedObservable().Subscribe(_ => Discard())); + d(saveButton.GetClickedObservable().Subscribe(_ => Save().ToBackground())); + d(_descriptionElement.Clicked.Subscribe(_ => ChangeDescription())); + d(_addFileElement.Clicked.Subscribe(_ => AddFile())); + }); } public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); UpdateView(); - } - - private readonly MultilinedElement _descriptionElement = new MultilinedElement("Description"); + } + + private readonly MultilinedElement _descriptionElement = new MultilinedElement("Description"); private readonly StringElement _addFileElement = new StringElement("Add New File"); protected void UpdateView() @@ -138,7 +139,7 @@ protected void UpdateView() var section = new Section(); sections.Add(section); - _descriptionElement.Details = _model.Description; + _descriptionElement.Details = _model.Description; section.Add(_descriptionElement); var fileSection = new Section(); @@ -151,60 +152,66 @@ protected void UpdateView() continue; var elName = key; - if (_model.Files[key].Filename != null) - elName = _model.Files[key].Filename; + if (_model.Files[key].NewFileName != null) + elName = _model.Files[key].NewFileName; - var el = new FileElement(elName, key, _model.Files[key]); + var el = new FileElement(elName, key, _model.Files[key]); el.Clicked.Subscribe(MakeCallback(this, key)); fileSection.Add(el); } - + fileSection.Add(_addFileElement); Root.Reset(sections); - } - - private static Action MakeCallback(EditGistController ctrl, string key) - { - var weakCtrl = new WeakReference(ctrl); - return new Action(_ => - { - var model = weakCtrl.Get()?._model; - if (model == null || !model.Files.ContainsKey(key)) - return; - - var originalGist = weakCtrl.Get()?._originalGist; - - var createController = new GistFileEditViewController { Filename = key, Content = model.Files[key].Content }; - createController.SaveCommand.Subscribe(__ => weakCtrl.Get()?.NavigationController.PopToViewController(weakCtrl.Get(), true)); - createController.Save = (name, content) => - { - if (string.IsNullOrEmpty(name)) - throw new InvalidOperationException("Please enter a name for the file"); - - //If different name & exists somewhere else - if (!name.Equals(key)) - if (weakCtrl.Get()?.IsDuplicateName(name) == true) - throw new InvalidOperationException("A filename by that type already exists"); - - if (originalGist?.Files.ContainsKey(key) == true) - model.Files[key] = new GistEditModel.File { Content = content, Filename = name }; - else - { - model.Files.Remove(key); - model.Files[name] = new GistEditModel.File { Content = content }; - } - }; - - weakCtrl.Get()?.NavigationController.PushViewController(createController, true); - }); + } + + private static Action MakeCallback(GistEditViewController ctrl, string key) + { + var weakCtrl = new WeakReference(ctrl); + return new Action(_ => + { + var model = weakCtrl.Get()?._model; + if (model == null || !model.Files.ContainsKey(key)) + return; + + var originalGist = weakCtrl.Get()?._originalGist; + + var createController = new GistFileEditViewController { Filename = key, Content = model.Files[key].Content }; + createController.SaveCommand.Subscribe(__ => weakCtrl.Get()?.NavigationController.PopToViewController(weakCtrl.Get(), true)); + createController.Save = (name, content) => + { + if (string.IsNullOrEmpty(name)) + throw new InvalidOperationException("Please enter a name for the file"); + + //If different name & exists somewhere else + if (!name.Equals(key)) + if (weakCtrl.Get()?.IsDuplicateName(name) == true) + throw new InvalidOperationException("A filename by that type already exists"); + + if (originalGist?.Files.ContainsKey(key) == true) + model.Files[key] = new GistFileUpdate { Content = content, NewFileName = name }; + else + { + model.Files.Remove(key); + model.Files[name] = new GistFileUpdate { Content = content }; + } + }; + + weakCtrl.Get()?.NavigationController.PushViewController(createController, true); + }); } private void ChangeDescription() { - var composer = new Composer { Title = "Description", Text = _model.Description }; - composer.NewComment(this, (text) => { + var composer = new Composer + { + Title = "Description", + Text = _model.Description + }; + + composer.PresentAsModal(this, text => + { _model.Description = text; - composer.CloseComposer(); + this.DismissViewController(true, null); }); } @@ -230,9 +237,9 @@ private void Delete(Element element, Section section) private class FileElement : StringElement { - public readonly GistEditModel.File File; + public readonly GistFileUpdate File; public readonly string Key; - public FileElement(string name, string key, GistEditModel.File file) + public FileElement(string name, string key, GistFileUpdate file) : base(name, String.Empty, UITableViewCellStyle.Subtitle) { File = file; @@ -244,7 +251,7 @@ public FileElement(string name, string key, GistEditModel.File file) private class EditSource : Source { - public EditSource(EditGistController dvc) + public EditSource(GistEditViewController dvc) : base (dvc) { } @@ -268,7 +275,7 @@ public override void CommitEditingStyle(UITableView tableView, UITableViewCellEd case UITableViewCellEditingStyle.Delete: var section = Root?[indexPath.Section]; var element = section?[indexPath.Row]; - (Container as EditGistController)?.Delete(element, section); + (Container as GistEditViewController)?.Delete(element, section); break; } } diff --git a/CodeHub.iOS/ViewControllers/Gists/GistFileModifyViewController.cs b/CodeHub.iOS/ViewControllers/Gists/GistFileModifyViewController.cs index b945671a..670c307e 100644 --- a/CodeHub.iOS/ViewControllers/Gists/GistFileModifyViewController.cs +++ b/CodeHub.iOS/ViewControllers/Gists/GistFileModifyViewController.cs @@ -35,12 +35,12 @@ public string Content set { this.RaiseAndSetIfChanged(ref _content, value); } } - public IReactiveCommand SaveCommand { get; } + public ReactiveCommand SaveCommand { get; } protected GistFileModifyViewController() : base(UITableViewStyle.Plain) { - SaveCommand = ReactiveCommand.CreateAsyncTask(t => { + SaveCommand = ReactiveCommand.CreateFromTask(t => { if (String.IsNullOrEmpty(Content)) throw new Exception("You cannot save a file without content!"); Save?.Invoke(Filename, Content); @@ -60,18 +60,31 @@ public override void ViewDidLoad() Root.Add(new Section { titleElement, contentElement }); TableView.TableFooterView = new UIView(); - OnActivation(d => { - d(this.Bind(x => x.Filename, true).Subscribe(x => { + var saveButton = new UIBarButtonItem(UIBarButtonSystemItem.Save); + NavigationItem.RightBarButtonItem = saveButton; + + OnActivation(d => + { + d(this.Bind(x => x.Filename, true).Subscribe(x => + { Title = string.IsNullOrEmpty(x) ? "Gist File" : x; titleElement.Value = x; })); + d(titleElement.Changed.Subscribe(x => Filename = x)); d(this.Bind(x => x.Content, true).Subscribe(x => contentElement.Value = x)); + d(contentElement.Changed.Subscribe(x => Content = x)); d(SaveCommand.Subscribe(_ => ResignFirstResponder())); - d(this.Bind(x => x.SaveCommand, true).ToBarButtonItem(Images.Buttons.SaveButton, x => NavigationItem.RightBarButtonItem = x)); + + d(saveButton.GetClickedObservable() + .Select(_ => Unit.Default) + .InvokeReactiveCommand(SaveCommand)); + + d(SaveCommand.IsExecuting + .Subscribe(x => saveButton.Enabled = !x)); }); } } diff --git a/CodeHub.iOS/ViewControllers/Gists/GistFileViewController.cs b/CodeHub.iOS/ViewControllers/Gists/GistFileViewController.cs index d7f94607..79e9ec9a 100755 --- a/CodeHub.iOS/ViewControllers/Gists/GistFileViewController.cs +++ b/CodeHub.iOS/ViewControllers/Gists/GistFileViewController.cs @@ -1,44 +1,202 @@ -using UIKit; using System; -using CodeHub.iOS.WebViews; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; using System.Threading.Tasks; -using MvvmCross.Platform; +using CodeHub.Core; using CodeHub.Core.Services; -using CodeHub.Core.ViewModels.Gists; -using System.Reactive.Linq; +using CodeHub.iOS.Services; +using CodeHub.WebViews; +using Foundation; +using ReactiveUI; +using Splat; +using UIKit; namespace CodeHub.iOS.ViewControllers.Gists { - public class GistFileViewController : CodeHub.iOS.Views.Source.FileSourceView + public class GistFileViewController : BaseWebViewController { - public override void ViewDidLoad() + private readonly IApplicationService _applicationService; + private readonly IAlertDialogService _alertDialogService; + private readonly string _gistId; + private readonly string _filename; + + private Octokit.Gist _gist; + public Octokit.Gist Gist { - base.ViewDidLoad(); + get { return _gist; } + set { this.RaiseAndSetIfChanged(ref _gist, value); } + } + + private string _contentSavePath; + public string ContentSavePath + { + get { return _contentSavePath; } + set { this.RaiseAndSetIfChanged(ref _contentSavePath, value); } + } + + public GistFileViewController( + string gistId, + string filename, + Octokit.Gist gist = null, + IApplicationService applicationService = null, + IAlertDialogService alertDialogService = null, + IMessageService messageService = null) + : base(false) + { + _gistId = gistId; + _filename = filename; + _gist = gist; + _applicationService = applicationService ?? Locator.Current.GetService(); + _alertDialogService = alertDialogService ?? Locator.Current.GetService(); + messageService = messageService ?? Locator.Current.GetService(); + + Title = System.IO.Path.GetFileName(filename); + + var actionButton = new UIBarButtonItem(UIBarButtonSystemItem.Action) { Enabled = false }; + NavigationItem.RightBarButtonItem = actionButton; - var vm = ViewModel as GistFileViewModel; - vm.Bind(x => x.ContentPath).IsNotNull().Subscribe(x => LoadSource(new Uri("file://" + x))); + var loadCommand = ReactiveCommand.CreateFromTask(Load); + + loadCommand + .ThrownExceptions + .Select(HandleLoadError) + .SelectMany(Interactions.Errors.Handle) + .Subscribe(); + + this.OnActivation(d => + { + d(this.WhenAnyValue(x => x.Gist) + .Select(x => x != null) + .Subscribe(x => actionButton.Enabled = x)); + + d(actionButton + .GetClickedObservable() + .Subscribe(CreateActionSheet)); + + d(loadCommand + .IsExecuting + .Subscribe(x => actionButton.Enabled = !x)); + }); + + Appearing + .Take(1) + .Select(_ => Unit.Default) + .InvokeReactiveCommand(loadCommand); } - async Task LoadSource(Uri fileUri) + private UserError HandleLoadError(Exception error) + { + LoadContent(""); + return new UserError("Unable to load selected file.", error); + } + + private async Task Load() + { + if (Gist == null) + Gist = await _applicationService.GitHubClient.Gist.Get(_gistId); + + if (!Gist.Files.ContainsKey(_filename)) + throw new Exception($"This gist does not have a file named {_filename}."); + + var file = Gist.Files[_filename]; + var isMarkdown = string.Equals(file.Language, "Markdown"); + + var filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), _filename); + System.IO.File.WriteAllText(filepath, file.Content, System.Text.Encoding.UTF8); + ContentSavePath = filepath; + await LoadSource(new Uri("file://" + filepath), isMarkdown); + } + + async Task LoadSource(Uri fileUri, bool isMarkdown) { var fontSize = (int)UIFont.PreferredSubheadline.PointSize; - var content = System.IO.File.ReadAllText(fileUri.LocalPath, System.Text.Encoding.UTF8); + var content = await Task.Run(() => System.IO.File.ReadAllText(fileUri.LocalPath, System.Text.Encoding.UTF8)); + await LoadSource(content, fileUri.LocalPath, isMarkdown); + } - if (ViewModel.IsMarkdown) + async Task LoadSource(string content, string filename, bool isMarkdown) + { + var fontSize = (int)UIFont.PreferredSubheadline.PointSize; + + if (isMarkdown) { - var markdownContent = await Mvx.Resolve().Client.Markdown.GetMarkdown(content); - var model = new DescriptionModel(markdownContent, fontSize); - var htmlContent = new MarkdownView { Model = model }; + var markdownContent = await _applicationService.Client.Markdown.GetMarkdown(content); + var model = new MarkdownModel(markdownContent, fontSize); + var htmlContent = new MarkdownWebView { Model = model }; LoadContent(htmlContent.GenerateString()); } else { var zoom = UIDevice.CurrentDevice.UserInterfaceIdiom != UIUserInterfaceIdiom.Phone; - var model = new SourceBrowserModel(content, "idea", fontSize, zoom, fileUri.LocalPath); - var contentView = new SyntaxHighlighterView { Model = model }; + var theme = _applicationService.Account.CodeEditTheme; + var model = new SyntaxHighlighterModel(content, theme, fontSize, zoom, file: filename); + var contentView = new SyntaxHighlighterWebView { Model = model }; LoadContent(contentView.GenerateString()); } } + + private void PresentOpenIn(UIBarButtonItem barButtonItem) + { + if (ContentSavePath == null) + return; + + var ctrl = new UIDocumentInteractionController(); + ctrl.Url = NSUrl.FromFilename(ContentSavePath); + ctrl.PresentOpenInMenu(barButtonItem, true); + } + + private void Share(UIBarButtonItem barButtonItem) + { + var url = Gist?.HtmlUrl; + if (url == null) + return; + + AlertDialogService.Share( + Title, + url: Gist?.HtmlUrl, + barButtonItem: barButtonItem); + } + + private void ShowInBrowser() + { + var url = Gist?.HtmlUrl; + if (url == null) + return; + + var viewController = new WebBrowserViewController(url); + PresentViewController(viewController, true, null); + } + + private void CreateActionSheet(UIBarButtonItem barButtonItem) + { + var sheet = new UIActionSheet(); + sheet.Dismissed += (sender, e) => sheet.Dispose(); + + var openButton = ContentSavePath != null ? sheet.AddButton("Open In") : -1; + var shareButton = Gist?.HtmlUrl != null ? sheet.AddButton("Share") : -1; + var showButton = Gist?.HtmlUrl != null ? sheet.AddButton("Show in GitHub") : -1; + var cancelButton = sheet.AddButton("Cancel"); + + sheet.CancelButtonIndex = cancelButton; + sheet.Dismissed += (sender, e) => BeginInvokeOnMainThread(() => { + try + { + if (e.ButtonIndex == openButton) + PresentOpenIn(barButtonItem); + else if (e.ButtonIndex == shareButton) + Share(barButtonItem); + else if (e.ButtonIndex == showButton) + ShowInBrowser(); + } + catch + { + } + }); + + sheet.ShowFrom(barButtonItem, true); + } } } + diff --git a/CodeHub.iOS/Views/Gists/GistView.cs b/CodeHub.iOS/ViewControllers/Gists/GistViewController.cs similarity index 80% rename from CodeHub.iOS/Views/Gists/GistView.cs rename to CodeHub.iOS/ViewControllers/Gists/GistViewController.cs index 5c3c493f..67b72cf8 100644 --- a/CodeHub.iOS/Views/Gists/GistView.cs +++ b/CodeHub.iOS/ViewControllers/Gists/GistViewController.cs @@ -1,208 +1,233 @@ -using System; -using CodeHub.iOS.ViewControllers; -using CodeHub.iOS.Views; -using CodeHub.Core.ViewModels.Gists; -using GitHubSharp.Models; -using UIKit; -using CodeHub.iOS.Utilities; -using System.Linq; -using System.Threading.Tasks; -using CodeHub.iOS.DialogElements; -using CodeHub.Core.Services; -using MvvmCross.Platform; -using System.Collections.Generic; -using CodeHub.iOS.Services; -using System.Reactive.Linq; -using ReactiveUI; - -namespace CodeHub.iOS.Views.Gists +using System; +using CodeHub.Core.ViewModels.Gists; +using UIKit; +using CodeHub.iOS.Utilities; +using System.Linq; +using System.Threading.Tasks; +using CodeHub.iOS.DialogElements; +using CodeHub.Core.Services; +using MvvmCross.Platform; +using System.Collections.Generic; +using CodeHub.iOS.Services; +using System.Reactive.Linq; +using ReactiveUI; +using Octokit; +using Humanizer; + +namespace CodeHub.iOS.ViewControllers.Gists { - public class GistView : PrettyDialogViewController - { - private SplitViewElement _splitRow1, _splitRow2; + public class GistViewController : PrettyDialogViewController + { + private SplitViewElement _splitRow1, _splitRow2; private StringElement _ownerElement; - private SplitButtonElement _split; - private readonly IAlertDialogService _alertDialogService = Mvx.Resolve(); + private SplitButtonElement _split; + private readonly IAlertDialogService _alertDialogService = Mvx.Resolve(); public new GistViewModel ViewModel { get { return (GistViewModel)base.ViewModel; } set { base.ViewModel = value; } } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - Title = "Gist"; - - var editButton = NavigationItem.RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Action); - - HeaderView.SetImage(null, Images.Avatar); - HeaderView.Text = "Gist #" + ViewModel.Id; - HeaderView.SubImageView.TintColor = UIColor.FromRGB(243, 156, 18); - - Appeared.Take(1) - .Select(_ => Observable.Timer(TimeSpan.FromSeconds(0.35f)).Take(1)) - .Switch() - .Select(_ => ViewModel.Bind(x => x.IsStarred, true)) - .Switch() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(x => HeaderView.SetSubImage(x ? Octicon.Star.ToImage() : null)); - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44f; - - _split = new SplitButtonElement(); - var files = _split.AddButton("Files", "-"); - var comments = _split.AddButton("Comments", "-"); - var forks = _split.AddButton("Forks", "-"); - - _splitRow1 = new SplitViewElement(Octicon.Lock.ToImage(), Octicon.Package.ToImage()); - _splitRow2 = new SplitViewElement(Octicon.Calendar.ToImage(), Octicon.Star.ToImage()); - _ownerElement = new StringElement("Owner", string.Empty, UITableViewCellStyle.Value1) { - Image = Octicon.Person.ToImage(), - Accessory = UITableViewCellAccessory.DisclosureIndicator - }; - - OnActivation(d => - { - d(editButton.GetClickedObservable().Subscribe(_ => ShareButtonTap(editButton))); - d(_ownerElement.Clicked.BindCommand(ViewModel.GoToUserCommand)); - - d(ViewModel.Bind(x => x.IsStarred, true).Subscribe(isStarred => _splitRow2.Button2.Text = isStarred ? "Starred" : "Not Starred")); - - d(ViewModel.BindCollection(x => x.Comments, true).Subscribe(_ => RenderGist())); - d(HeaderView.Clicked.BindCommand(ViewModel.GoToUserCommand)); - - d(ViewModel.Bind(x => x.Gist, true).Where(x => x != null).Subscribe(gist => - { - _splitRow1.Button1.Text = (gist.Public ?? true) ? "Public" : "Private"; - _splitRow1.Button2.Text = (gist.History?.Count ?? 0) + " Revisions"; - _splitRow2.Button1.Text = gist.CreatedAt.Day + " Days Old"; - _ownerElement.Value = gist.Owner?.Login ?? "Unknown"; - files.Text = gist.Files.Count.ToString(); - comments.Text = gist.Comments.ToString(); - forks.Text = gist.Forks.Count.ToString(); - HeaderView.SubText = gist.Description; - HeaderView.Text = gist.Files?.Select(x => x.Key).FirstOrDefault() ?? HeaderView.Text; - HeaderView.SetImage(gist.Owner?.AvatarUrl, Images.Avatar); - RenderGist(); - RefreshHeaderView(); - })); - }); - } + + public static GistViewController FromGist(Gist gist) + { + return new GistViewController + { + ViewModel = GistViewModel.FromGist(gist) + }; + } + + public GistViewController() + { + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + Title = "Gist"; + + var editButton = NavigationItem.RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Action); + + HeaderView.SetImage(null, Images.Avatar); + HeaderView.Text = "Gist #" + ViewModel.Id; + HeaderView.SubImageView.TintColor = UIColor.FromRGB(243, 156, 18); + + Appeared.Take(1) + .Select(_ => Observable.Timer(TimeSpan.FromSeconds(0.35f)).Take(1)) + .Switch() + .Select(_ => ViewModel.Bind(x => x.IsStarred, true)) + .Switch() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => HeaderView.SetSubImage(x ? Octicon.Star.ToImage() : null)); + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44f; + + _split = new SplitButtonElement(); + var files = _split.AddButton("Files", "-"); + var comments = _split.AddButton("Comments", "-"); + var forks = _split.AddButton("Forks", "-"); + + _splitRow1 = new SplitViewElement(Octicon.Lock.ToImage(), Octicon.Package.ToImage()); + _splitRow2 = new SplitViewElement(Octicon.Calendar.ToImage(), Octicon.Star.ToImage()); + _ownerElement = new StringElement("Owner", string.Empty, UITableViewCellStyle.Value1) { + Image = Octicon.Person.ToImage(), + Accessory = UITableViewCellAccessory.DisclosureIndicator + }; + + OnActivation(d => + { + d(editButton.GetClickedObservable().Subscribe(ShareButtonTap)); + d(_ownerElement.Clicked.BindCommand(ViewModel.GoToUserCommand)); + + d(ViewModel.Bind(x => x.IsStarred, true).Subscribe(isStarred => _splitRow2.Button2.Text = isStarred ? "Starred" : "Not Starred")); + + d(ViewModel.BindCollection(x => x.Comments, true).Subscribe(_ => RenderGist())); + d(HeaderView.Clicked.BindCommand(ViewModel.GoToUserCommand)); + + d(ViewModel.Bind(x => x.Gist, true).Where(x => x != null).Subscribe(gist => + { + var daysOld = gist.CreatedAt.TotalDaysAgo(); + + _splitRow1.Button1.Text = gist.Public ? "Public" : "Private"; + _splitRow1.Button2.Text = (gist.History?.Count ?? 0) + " Revisions"; + _splitRow2.Button1.Text = daysOld == 0 ? "Created today" : "day".ToQuantity(daysOld) + " old"; + _ownerElement.Value = gist.Owner?.Login ?? "Unknown"; + files.Text = gist.Files?.Count.ToString() ?? "-"; + comments.Text = gist.Comments.ToString(); + forks.Text = gist.Forks?.Count.ToString() ?? "-"; + HeaderView.SubText = gist.Description; + HeaderView.Text = gist.Files?.Select(x => x.Key).FirstOrDefault() ?? HeaderView.Text; + HeaderView.SetImage(gist.Owner?.AvatarUrl, Images.Avatar); + RenderGist(); + RefreshHeaderView(); + })); + }); + } public void RenderGist() - { - if (ViewModel.Gist == null) return; - var model = ViewModel.Gist; - - ICollection
sections = new LinkedList
(); - sections.Add(new Section { _split }); - sections.Add(new Section { _splitRow1, _splitRow2, _ownerElement }); - var sec2 = new Section(); - sections.Add(sec2); - + { + if (ViewModel.Gist == null) return; + var model = ViewModel.Gist; + + ICollection
sections = new LinkedList
(); + sections.Add(new Section { _split }); + sections.Add(new Section { _splitRow1, _splitRow2, _ownerElement }); + var sec2 = new Section(); + sections.Add(sec2); + var weakVm = new WeakReference(ViewModel); foreach (var file in model.Files.Keys) { - var sse = new ButtonElement(file, Octicon.FileCode.ToImage()) + var sse = new ButtonElement(file, Octicon.FileCode.ToImage()) { LineBreakMode = UILineBreakMode.TailTruncation, }; var fileSaved = file; var gistFileModel = model.Files[fileSaved]; - sse.Clicked.Subscribe(MakeCallback(weakVm, gistFileModel)); + sse.Clicked.Subscribe(_ => GoToGist(gistFileModel)); sec2.Add(sse); - } - - if (ViewModel.Comments.Items.Count > 0) - { - var sec3 = new Section("Comments"); - sec3.AddAll(ViewModel.Comments.Select(x => new CommentElement(x.User?.Login ?? "Anonymous", x.Body, x.CreatedAt, x.User?.AvatarUrl))); - sections.Add(sec3); - } + } + + if (ViewModel.Comments.Items.Count > 0) + { + var sec3 = new Section("Comments"); + sec3.AddAll(ViewModel.Comments.Select(x => new CommentElement(x.User?.Login ?? "Anonymous", x.Body, x.CreatedAt, x.User?.AvatarUrl))); + sections.Add(sec3); + } Root.Reset(sections); - } - - private static Action MakeCallback(WeakReference weakVm, GistFileModel model) - { - return new Action(_ => weakVm.Get()?.GoToFileSourceCommand.Execute(model)); - } - - private async Task Fork() - { - try - { - await this.DoWorkAsync("Forking...", ViewModel.ForkGist); - } - catch (Exception ex) - { - _alertDialogService.Alert("Error", ex.Message).ToBackground(); - } - } - - private async Task Compose() - { - try - { - var app = Mvx.Resolve(); - var data = await this.DoWorkAsync("Loading...", () => app.Client.ExecuteAsync(app.Client.Gists[ViewModel.Id].Get())); - var gistController = new EditGistController(data.Data); - gistController.Created = editedGist => ViewModel.Gist = editedGist; - var navController = new UINavigationController(gistController); - PresentViewController(navController, true, null); - } - catch (Exception ex) - { - _alertDialogService.Alert("Error", ex.Message).ToBackground(); - } } - - void ShareButtonTap (object sender) - { - if (ViewModel.Gist == null) - return; - - var app = Mvx.Resolve(); - var isOwner = string.Equals(app.Account.Username, ViewModel.Gist.Owner.Login, StringComparison.OrdinalIgnoreCase); - - var sheet = new UIActionSheet(); - var editButton = sheet.AddButton(isOwner ? "Edit" : "Fork"); - var starButton = sheet.AddButton(ViewModel.IsStarred ? "Unstar" : "Star"); - var shareButton = sheet.AddButton("Share"); - var showButton = sheet.AddButton("Show in GitHub"); - var cancelButton = sheet.AddButton("Cancel"); - sheet.CancelButtonIndex = cancelButton; - sheet.DismissWithClickedButtonIndex(cancelButton, true); - sheet.Dismissed += (s, e) => - { - BeginInvokeOnMainThread(() => - { - try - { - if (e.ButtonIndex == shareButton) - AlertDialogService.ShareUrl(ViewModel.Gist?.HtmlUrl, sender as UIBarButtonItem); - else if (e.ButtonIndex == showButton) - ViewModel.GoToHtmlUrlCommand.Execute(null); - else if (e.ButtonIndex == starButton) - ViewModel.ToggleStarCommand.Execute(null); - else if (e.ButtonIndex == editButton) - Compose().ToBackground(); - } - catch - { - } - }); - - sheet.Dispose(); - }; - - sheet.ShowFromToolbar(NavigationController.Toolbar); - } + + private void GoToGist(GistFile model) + { + var viewCtrl = new GistFileViewController( + ViewModel.Gist.Id, + model.Filename, + ViewModel.Gist); + + this.PushViewController(viewCtrl); + } + + private async Task Fork() + { + try + { + await this.DoWorkAsync("Forking...", ViewModel.ForkGist); + } + catch (Exception ex) + { + _alertDialogService.Alert("Error", ex.Message).ToBackground(); + } + } + + private async Task Compose() + { + try + { + var app = Mvx.Resolve(); + var data = await this.DoWorkAsync("Loading...", () => app.GitHubClient.Gist.Get(ViewModel.Id)); + var gistController = new GistEditViewController(data); + gistController.Created = editedGist => ViewModel.Gist = editedGist; + var navController = new UINavigationController(gistController); + PresentViewController(navController, true, null); + } + catch (Exception ex) + { + _alertDialogService.Alert("Error", ex.Message).ToBackground(); + } + } + + void ShareButtonTap (object sender) + { + if (ViewModel.Gist == null) + return; + + var app = Mvx.Resolve(); + var isOwner = string.Equals(app.Account.Username, ViewModel.Gist?.Owner?.Login, StringComparison.OrdinalIgnoreCase); + var gist = ViewModel.Gist; + + var sheet = new UIActionSheet(); + var editButton = sheet.AddButton(isOwner ? "Edit" : "Fork"); + var starButton = sheet.AddButton(ViewModel.IsStarred ? "Unstar" : "Star"); + var shareButton = sheet.AddButton("Share"); + var showButton = sheet.AddButton("Show in GitHub"); + var cancelButton = sheet.AddButton("Cancel"); + sheet.CancelButtonIndex = cancelButton; + sheet.DismissWithClickedButtonIndex(cancelButton, true); + sheet.Dismissed += (s, e) => + { + BeginInvokeOnMainThread(() => + { + try + { + if (e.ButtonIndex == shareButton) + { + AlertDialogService.Share( + $"Gist {gist.Files?.Select(x => x.Key).FirstOrDefault() ?? gist.Id}", + gist.Description, + gist.HtmlUrl, + sender as UIBarButtonItem); + } + else if (e.ButtonIndex == showButton) + ViewModel.GoToHtmlUrlCommand.Execute(null); + else if (e.ButtonIndex == starButton) + ViewModel.ToggleStarCommand.Execute(null); + else if (e.ButtonIndex == editButton) + Compose().ToBackground(); + } + catch + { + } + }); + + sheet.Dispose(); + }; + + sheet.ShowFromToolbar(NavigationController.Toolbar); + } } } diff --git a/CodeHub.iOS/ViewControllers/Gists/GistsViewController.cs b/CodeHub.iOS/ViewControllers/Gists/GistsViewController.cs new file mode 100644 index 00000000..77fc59a5 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/Gists/GistsViewController.cs @@ -0,0 +1,151 @@ +using System; +using System.Reactive.Linq; +using CodeHub.iOS.Views; +using CoreGraphics; +using UIKit; +using ReactiveUI; +using CodeHub.iOS.TableViewSources; +using CodeHub.Core.ViewModels.Gists; +using System.Reactive; +using Splat; +using CodeHub.Core.Services; + +namespace CodeHub.iOS.ViewControllers.Gists +{ + public class GistsViewController : TableViewController + { + private readonly Lazy _repositorySearchBar + = new Lazy(() => new UISearchBar(new CGRect(0, 0, 320, 44))); + + private readonly LoadingIndicatorView _loading = new LoadingIndicatorView(); + + private readonly Lazy emptyView = new Lazy((() => + new EmptyListView(Octicon.Gist.ToEmptyListImage(), "There are no gists."))); + + public GistsViewModel ViewModel { get; } + + public static GistsViewController CreatePublicGistsViewController() + { + var viewModel = GistsViewModel.CreatePublicGistsViewModel(); + return new GistsViewController(viewModel) + { + Title = "Public Gists", + ShowSearchBar = false + }; + } + + public static GistsViewController CreateStarredGistsViewController() + { + var viewModel = GistsViewModel.CreateStarredGistsViewModel(); + return new GistsViewController(viewModel) { Title = "Starred Gists" }; + } + + public static GistsViewController CreateUserGistsViewController(string username) + { + var applicationService = Locator.Current.GetService(); + var mine = applicationService.Account.Username.ToLower().Equals(username.ToLower()); + + if (mine) + { + var viewModel = new CurrentUserGistsViewModel(username); + var vc = new GistsViewController(viewModel) { Title = "My Gists" }; + + var button = new UIBarButtonItem(UIBarButtonSystemItem.Add); + vc.NavigationItem.RightBarButtonItem = button; + vc.WhenActivated(d => + { + d(button.GetClickedObservable() + .Subscribe(_ => GistCreateViewController.Show(vc))); + }); + + return vc; + } + else + { + var viewModel = GistsViewModel.CreateUserGistsViewModel(username); + var vc = new GistsViewController(viewModel) { Title = $"{username}'s Gists" }; + return vc; + } + } + + public bool ShowSearchBar { get; private set; } = true; + + public GistsViewController(GistsViewModel viewModel) + : base(UITableViewStyle.Plain) + { + ViewModel = viewModel; + Title = "Gists"; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var tableViewSource = new GistTableViewSource(TableView, ViewModel.Items); + TableView.Source = tableViewSource; + + Appearing + .Take(1) + .Select(_ => ViewModel.LoadCommand.Execute()) + .Switch() + .Take(1) + .Catch(Observable.Return(false)) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(SetItemsPresent); + + this.WhenActivated(d => + { + if (ShowSearchBar) { + d(_repositorySearchBar.Value.GetChangedObservable() + .Subscribe(x => ViewModel.SearchText = x)); + } + + d(ViewModel.ItemSelected + .Select(x => GistViewController.FromGist(x.Gist)) + .Subscribe(x => NavigationController.PushViewController(x, true))); + + d(ViewModel.WhenAnyValue(x => x.HasMore) + .Subscribe(x => TableView.TableFooterView = x ? _loading : null)); + + d(tableViewSource.RequestMore + .InvokeReactiveCommand(ViewModel.LoadMoreCommand)); + + d(ViewModel.LoadCommand + .Select(_ => ViewModel.Items.Changed) + .Switch() + .Select(_ => Unit.Default) + .Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) + .Where(_ => TableView.LastItemVisible()) + .InvokeReactiveCommand(ViewModel.LoadMoreCommand)); + + d(ViewModel.LoadCommand.Merge(ViewModel.LoadMoreCommand) + .Select(_ => Unit.Default) + .Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) + .Where(_ => TableView.LastItemVisible()) + .InvokeReactiveCommand(ViewModel.LoadMoreCommand)); + }); + } + + private void SetItemsPresent(bool hasItems) + { + if (ShowSearchBar) + TableView.TableHeaderView = hasItems ? _repositorySearchBar.Value : null; + + TableView.SeparatorStyle = hasItems + ? UITableViewCellSeparatorStyle.SingleLine + : UITableViewCellSeparatorStyle.None; + + if (hasItems) + { + TableView.BackgroundView = null; + } + else + { + emptyView.Value.Alpha = 0; + TableView.BackgroundView = emptyView.Value; + UIView.Animate(0.8, 0, UIViewAnimationOptions.CurveEaseIn, + () => emptyView.Value.Alpha = 1, null); + } + } + } +} diff --git a/CodeHub.iOS/ViewControllers/LiteComposer.cs b/CodeHub.iOS/ViewControllers/LiteComposer.cs deleted file mode 100644 index fb1cb162..00000000 --- a/CodeHub.iOS/ViewControllers/LiteComposer.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using CoreGraphics; -using Foundation; -using UIKit; - -namespace CodeHub.iOS.ViewControllers -{ - public class LiteComposer : BaseViewController - { - private readonly UITextView _textView; - private readonly UIBarButtonItem _sendButton; - - public event EventHandler ReturnAction; - - public bool EnableSendButton - { - get { return _sendButton.Enabled; } - set { _sendButton.Enabled = value; } - } - - public LiteComposer () : base (null, null) - { - Title = "New Comment"; - EdgesForExtendedLayout = UIRectEdge.None; - - _textView = new UITextView() - { - Font = UIFont.PreferredBody - }; - - var close = new UIBarButtonItem { Image = Images.Buttons.BackButton }; - NavigationItem.LeftBarButtonItem = close; - _sendButton = new UIBarButtonItem { Image = Images.Buttons.SaveButton }; - NavigationItem.RightBarButtonItem = _sendButton; - - OnActivation(d => - { - d(close.GetClickedObservable().Subscribe(_ => CloseComposer())); - d(_sendButton.GetClickedObservable().Subscribe(_ => PostCallback())); - }); - } - - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - _textView.Frame = ComputeComposerSize(CGRect.Empty); - View.AddSubview (_textView); - } - - public string Text - { - get { return _textView.Text; } - set { _textView.Text = value; } - } - - public string ActionButtonText - { - get { return NavigationItem.RightBarButtonItem.Title; } - set { NavigationItem.RightBarButtonItem.Title = value; } - } - - public void CloseComposer () - { - _sendButton.Enabled = true; - NavigationController.PopViewController(true); - } - - void PostCallback () - { - _sendButton.Enabled = false; - var handler = ReturnAction; - if (handler != null) - handler(this, Text); - } - - void KeyboardWillShow (NSNotification notification) - { - var nsValue = notification.UserInfo.ObjectForKey (UIKeyboard.BoundsUserInfoKey) as NSValue; - if (nsValue == null) return; - var kbdBounds = nsValue.RectangleFValue; - _textView.Frame = ComputeComposerSize (kbdBounds); - } - - CGRect ComputeComposerSize (CGRect kbdBounds) - { - var view = View.Bounds; - return new CGRect (0, 0, view.Width, view.Height-kbdBounds.Height); - } - - NSObject _showNotification; - public override void ViewWillAppear (bool animated) - { - base.ViewWillAppear (animated); - _showNotification = NSNotificationCenter.DefaultCenter.AddObserver (new NSString("UIKeyboardWillShowNotification"), KeyboardWillShow); - _textView.BecomeFirstResponder (); - } - - public override void ViewWillDisappear(bool animated) - { - base.ViewWillDisappear(animated); - if (_showNotification != null) - NSNotificationCenter.DefaultCenter.RemoveObserver(_showNotification); - } - } -} diff --git a/CodeHub.iOS/ViewControllers/MarkdownComposerViewController.cs b/CodeHub.iOS/ViewControllers/MarkdownComposerViewController.cs index 7f159f23..3fd2172b 100644 --- a/CodeHub.iOS/ViewControllers/MarkdownComposerViewController.cs +++ b/CodeHub.iOS/ViewControllers/MarkdownComposerViewController.cs @@ -1,199 +1,43 @@ using System; -using CodeHub.iOS.ViewControllers; using UIKit; -using MvvmCross.Platform; using CodeHub.Core.Services; using Foundation; -using System.Net; -using System.Collections.Specialized; -using System.Threading.Tasks; -using System.Linq; -using CodeHub.iOS.Services; -using CodeHub.iOS.WebViews; +using CodeHub.WebViews; using WebKit; +using CodeHub.iOS.Views; +using ReactiveUI; +using System.Threading.Tasks; +using Splat; namespace CodeHub.iOS.ViewControllers { public class MarkdownComposerViewController : Composer { private readonly UISegmentedControl _viewSegment; + private readonly IMarkdownService _markdownService; private WKWebView _previewView; - public MarkdownComposerViewController() + public MarkdownComposerViewController(IMarkdownService markdownService = null) { - _viewSegment = new UISegmentedControl(new [] { "Compose", "Preview" }); - _viewSegment.SelectedSegment = 0; - NavigationItem.TitleView = _viewSegment; - _viewSegment.ValueChanged += SegmentValueChanged; - - var cameraImage = Octicon.DeviceCamera.ToImage(25, false); - var linkImage = Octicon.Link.ToImage(25, false); - var pictureImage = Octicon.FileMedia.ToImage(25, false); + _markdownService = markdownService ?? Locator.Current.GetService(); - var buttons = new [] + _viewSegment = new UISegmentedControl(new[] { "Compose", "Preview" }) { - CreateAccessoryButton("@", () => TextView.InsertText("@")), - CreateAccessoryButton("#", () => TextView.InsertText("#")), - CreateAccessoryButton("*", () => TextView.InsertText("*")), - CreateAccessoryButton("`", () => TextView.InsertText("`")), - CreateAccessoryButton(pictureImage, () => { - var range = TextView.SelectedRange; - TextView.InsertText("![]()"); - TextView.SelectedRange = new NSRange(range.Location + 4, 0); - }), - CreateAccessoryButton(cameraImage, SelectImage), - CreateAccessoryButton(linkImage, () => { - var range = TextView.SelectedRange; - TextView.InsertText("[]()"); - TextView.SelectedRange = new NSRange(range.Location + 1, 0); - }), - CreateAccessoryButton("~", () => TextView.InsertText("~")), - CreateAccessoryButton("=", () => TextView.InsertText("=")), - CreateAccessoryButton("-", () => TextView.InsertText("-")), - CreateAccessoryButton("+", () => TextView.InsertText("+")), - CreateAccessoryButton("_", () => TextView.InsertText("_")), - CreateAccessoryButton("[", () => TextView.InsertText("[")), - CreateAccessoryButton("]", () => TextView.InsertText("]")), - CreateAccessoryButton("<", () => TextView.InsertText("<")), - CreateAccessoryButton(">", () => TextView.InsertText(">")), + SelectedSegment = 0 }; - SetAccesoryButtons(buttons); - } - - private class ImagePickerDelegate : UINavigationControllerDelegate - { - public override void WillShowViewController(UINavigationController navigationController, UIViewController viewController, bool animated) - { - UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; - } - } - - private class ImgurModel - { - public ImgurDataModel Data { get; set; } - public bool Success { get; set; } - - public class ImgurDataModel - { - public string Link { get; set; } - } - } - - private async void UploadImage(UIImage img) - { - var hud = new CodeHub.iOS.Utilities.Hud(null); - hud.Show("Uploading..."); - - try - { - var returnData = await Task.Run(() => - { - using (var w = new WebClient()) - { - var data = img.AsJPEG(); - byte[] dataBytes = new byte[data.Length]; - System.Runtime.InteropServices.Marshal.Copy(data.Bytes, dataBytes, 0, Convert.ToInt32(data.Length)); - - w.Headers.Set("Authorization", "Client-ID aa5d7d0bc1dffa6"); - - var values = new NameValueCollection - { - { "image", Convert.ToBase64String(dataBytes) } - }; - - return w.UploadValues("https://api.imgur.com/3/image", values); - } - }); - - - var json = Mvx.Resolve(); - var imgurModel = json.Deserialize(System.Text.Encoding.UTF8.GetString(returnData)); - TextView.InsertText("![](" + imgurModel.Data.Link + ")"); - } - catch (Exception e) - { - AlertDialogService.ShowAlert("Error", "Unable to upload image: " + e.Message); - } - finally - { - hud.Hide(); - } - } - - private class RotatableUIImagePickerController : UIImagePickerController - { - public override bool ShouldAutorotate() - { - return true; - } - - public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations() - { - return UIInterfaceOrientationMask.All; - } - - public override UIInterfaceOrientation PreferredInterfaceOrientationForPresentation() - { - return UIApplication.SharedApplication.StatusBarOrientation; - } - } - - private void SelectImage() - { - var imagePicker = new RotatableUIImagePickerController(); - imagePicker.NavigationControllerDelegate = new ImagePickerDelegate(); - imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary; - imagePicker.MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary); - imagePicker.MediaTypes = imagePicker.MediaTypes.Where(x => !(x.Contains("movie") || x.Contains("video"))).ToArray(); - - imagePicker.FinishedPickingMedia += (sender, e) => - { - // determine what was selected, video or image - bool isImage = false; - switch(e.Info[UIImagePickerController.MediaType].ToString()) { - case "public.image": - isImage = true; - break; - } - - // if it was an image, get the other image info - if(isImage) - { - // get the original image - UIImage originalImage = e.Info[UIImagePickerController.OriginalImage] as UIImage; - if(originalImage != null) { - // do something with the image - try - { - UploadImage(originalImage); - } - catch - { - } - } - } - else - { // if it's a video - AlertDialogService.ShowAlert("Not supported!", "Video upload is currently not supported."); - } - - // dismiss the picker - imagePicker.DismissViewController(true, null); - UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; - }; + NavigationItem.TitleView = _viewSegment; + TextView.InputAccessoryView = new MarkdownAccessoryView(TextView); - imagePicker.Canceled += (sender, e) => + this.WhenActivated(d => { - imagePicker.DismissViewController(true, null); - UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; - }; - - NavigationController.PresentViewController(imagePicker, true, null); + d(_viewSegment.GetChangedObservable() + .Subscribe(_ => SegmentValueChanged())); + }); } - void SegmentValueChanged (object sender, EventArgs e) + void SegmentValueChanged() { if (_viewSegment.SelectedSegment == 0) { @@ -215,13 +59,17 @@ void SegmentValueChanged (object sender, EventArgs e) TextView.RemoveFromSuperview(); Add(_previewView); - var markdownService = Mvx.Resolve(); - var markdownText = markdownService.Convert(Text); - var model = new DescriptionModel(markdownText, (int)UIFont.PreferredSubheadline.PointSize); - var view = new MarkdownView { Model = model }.GenerateString(); - _previewView.LoadHtmlString(view, NSBundle.MainBundle.BundleUrl); + LoadPreview(_previewView).ToBackground(); } } + + private async Task LoadPreview(WKWebView previewView) + { + var markdownText = await _markdownService.Convert(Text); + var model = new MarkdownModel(markdownText, (int)UIFont.PreferredSubheadline.PointSize); + var view = new MarkdownWebView { Model = model }.GenerateString(); + previewView.LoadHtmlString(view, NSBundle.MainBundle.BundleUrl); + } } } diff --git a/CodeHub.iOS/ViewControllers/MessageComposerViewController.cs b/CodeHub.iOS/ViewControllers/MessageComposerViewController.cs new file mode 100644 index 00000000..58d956e8 --- /dev/null +++ b/CodeHub.iOS/ViewControllers/MessageComposerViewController.cs @@ -0,0 +1,127 @@ +using CoreGraphics; +using Foundation; +using UIKit; +using ReactiveUI; +using CodeHub.iOS.Views; + +namespace CodeHub.iOS.ViewControllers +{ + public class MessageComposerViewController : BaseViewController, IActivatable + { + private CGRect _keyboardBounds = CGRect.Empty; + private NSObject _keyboardHideObserver; + private NSObject _keyboardShowObserver; + + public ExtendedUITextView TextView { get; } + + public MessageComposerViewController() + { + EdgesForExtendedLayout = UIRectEdge.None; + TextView = new ExtendedUITextView(); + TextView.Font = UIFont.PreferredBody; + + // Work around an Apple bug in the UITextView that crashes + if (ObjCRuntime.Runtime.Arch == ObjCRuntime.Arch.SIMULATOR) + TextView.AutocorrectionType = UITextAutocorrectionType.No; + + NavigationItem.BackBarButtonItem = new UIBarButtonItem { Title = string.Empty }; + } + + public string Text + { + get { return TextView.Text; } + set + { + if (string.Equals(Text, value)) + return; + + TextView.Text = value; + TextView.SelectedRange = new NSRange(0, 0); + } + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + View.BackgroundColor = UIColor.White; + } + + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + ResizeTextView(); + } + + void KeyboardWillShow (NSNotification notification) + { + var nsValue = notification.UserInfo.ObjectForKey (UIKeyboard.BoundsUserInfoKey) as NSValue; + if (nsValue == null) return; + _keyboardBounds = nsValue.RectangleFValue; + UIView.Animate(0.25f, 0, UIViewAnimationOptions.BeginFromCurrentState | UIViewAnimationOptions.CurveEaseIn, ResizeTextView, null); + } + + void KeyboardWillHide (NSNotification notification) + { + _keyboardBounds = CGRect.Empty; + UIView.Animate(0.2, 0, UIViewAnimationOptions.BeginFromCurrentState | UIViewAnimationOptions.CurveEaseIn, ResizeTextView, null); + } + + private void ResizeTextView() + { + TextView.Frame = new CGRect(0, 0, View.Bounds.Width, View.Bounds.Height - _keyboardBounds.Height); + } + + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + _keyboardShowObserver = NSNotificationCenter.DefaultCenter.AddObserver(new NSString("UIKeyboardWillShowNotification"), KeyboardWillShow); + _keyboardHideObserver = NSNotificationCenter.DefaultCenter.AddObserver(new NSString("UIKeyboardWillHideNotification"), KeyboardWillHide); + ResizeTextView(); + Add(TextView); + } + + public override void ViewWillDisappear(bool animated) + { + base.ViewWillDisappear(animated); + TextView.ResignFirstResponder(); + } + + public override void ViewDidDisappear(bool animated) + { + base.ViewDidDisappear(animated); + NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardHideObserver); + NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardShowObserver); + TextView.RemoveFromSuperview(); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + TextView.BecomeFirstResponder(); + } + + private static float CalculateHeight(UIInterfaceOrientation orientation) + { + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) + return 44; + if (orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown) + return 64; + return 88f; + } + + public override void WillRotate(UIInterfaceOrientation toInterfaceOrientation, double duration) + { + base.WillRotate(toInterfaceOrientation, duration); + + if (TextView.InputAccessoryView != null) + { + UIView.Animate(duration, 0, UIViewAnimationOptions.BeginFromCurrentState, () => + { + var frame = TextView.InputAccessoryView.Frame; + frame.Height = CalculateHeight(toInterfaceOrientation); + TextView.InputAccessoryView.Frame = frame; + }, null); + } + } + } +} diff --git a/CodeHub.iOS/ViewControllers/Organizations/OrganizationViewController.cs b/CodeHub.iOS/ViewControllers/Organizations/OrganizationViewController.cs index 2999f5e6..1d27d216 100644 --- a/CodeHub.iOS/ViewControllers/Organizations/OrganizationViewController.cs +++ b/CodeHub.iOS/ViewControllers/Organizations/OrganizationViewController.cs @@ -1,23 +1,25 @@ -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.Organizations; -using UIKit; -using CoreGraphics; -using CodeHub.iOS.DialogElements; -using System; -using System.Reactive.Linq; - +using CodeHub.iOS.ViewControllers; +using CodeHub.Core.ViewModels.Organizations; +using UIKit; +using CoreGraphics; +using CodeHub.iOS.DialogElements; +using System; +using System.Reactive.Linq; +using CodeHub.iOS.ViewControllers.Users; +using CodeHub.iOS.ViewControllers.Gists; + namespace CodeHub.iOS.ViewControllers.Organizations { public class OrganizationViewController : PrettyDialogViewController { public override void ViewDidLoad() { - base.ViewDidLoad(); - - var vm = (OrganizationViewModel) ViewModel; - - HeaderView.SetImage(null, Images.Avatar); - Title = vm.Name; + base.ViewDidLoad(); + + var vm = (OrganizationViewModel) ViewModel; + + HeaderView.SetImage(null, Images.Avatar); + Title = vm.Name; HeaderView.Text = vm.Name; var members = new StringElement("Members", Octicon.Person.ToImage()); @@ -26,25 +28,38 @@ public override void ViewDidLoad() var events = new StringElement("Events", Octicon.Rss.ToImage()); var repos = new StringElement("Repositories", Octicon.Repo.ToImage()); var gists = new StringElement("Gists", Octicon.Gist.ToImage()); - Root.Reset(new Section(new UIView(new CGRect(0, 0, 0, 20f))) { members, teams }, new Section { events, followers }, new Section { repos, gists }); - - OnActivation(d => - { - d(members.Clicked.BindCommand(vm.GoToMembersCommand)); - d(teams.Clicked.BindCommand(vm.GoToTeamsCommand)); - d(followers.Clicked.BindCommand(vm.GoToFollowersCommand)); - d(events.Clicked.BindCommand(vm.GoToEventsCommand)); - d(repos.Clicked.BindCommand(vm.GoToRepositoriesCommand)); - d(gists.Clicked.BindCommand(vm.GoToGistsCommand)); - - d(vm.Bind(x => x.Organization, true).Where(x => x != null).Subscribe(x => - { - HeaderView.SubText = string.IsNullOrWhiteSpace(x.Name) ? x.Login : x.Name; - HeaderView.SetImage(x.AvatarUrl, Images.Avatar); - RefreshHeaderView(); - })); + Root.Reset(new Section(new UIView(new CGRect(0, 0, 0, 20f))) { members, teams }, new Section { events, followers }, new Section { repos, gists }); + + OnActivation(d => + { + d(teams.Clicked.BindCommand(vm.GoToTeamsCommand)); + d(events.Clicked.BindCommand(vm.GoToEventsCommand)); + + d(members.Clicked + .Select(_ => UsersViewController.CreateOrganizationMembersViewController(vm.Name)) + .Subscribe(x => NavigationController.PushViewController(x, true))); + + d(followers.Clicked + .Select(_ => UsersViewController.CreateFollowersViewController(vm.Name)) + .Subscribe(x => NavigationController.PushViewController(x, true))); + + d(repos.Clicked.Subscribe(_ => { + var vc = Repositories.RepositoriesViewController.CreateOrganizationViewController(vm.Name); + NavigationController?.PushViewController(vc, true); + })); + + d(gists.Clicked + .Select(x => GistsViewController.CreateUserGistsViewController(vm.Name)) + .Subscribe(x => NavigationController.PushViewController(x, true))); + + d(vm.Bind(x => x.Organization, true).Where(x => x != null).Subscribe(x => + { + HeaderView.SubText = string.IsNullOrWhiteSpace(x.Name) ? x.Login : x.Name; + HeaderView.SetImage(x.AvatarUrl, Images.Avatar); + RefreshHeaderView(); + })); }); - } + } } } diff --git a/CodeHub.iOS/ViewControllers/Organizations/OrganizationsViewController.cs b/CodeHub.iOS/ViewControllers/Organizations/OrganizationsViewController.cs index aefea5aa..50126a37 100644 --- a/CodeHub.iOS/ViewControllers/Organizations/OrganizationsViewController.cs +++ b/CodeHub.iOS/ViewControllers/Organizations/OrganizationsViewController.cs @@ -1,36 +1,42 @@ -using CodeHub.iOS.ViewControllers; -using CodeHub.Core.ViewModels.Organizations; -using CodeHub.iOS.DialogElements; -using System; -using UIKit; -using CodeHub.iOS.Views; -using CodeHub.Core.Utilities; - +using CodeHub.Core.ViewModels.Organizations; +using CodeHub.iOS.DialogElements; +using System; +using UIKit; +using CodeHub.iOS.Views; +using CodeHub.Core.Utilities; + namespace CodeHub.iOS.ViewControllers.Organizations { public class OrganizationsViewController : ViewModelCollectionDrivenDialogViewController - { - public OrganizationsViewController() - { - Title = "Organizations"; - EmptyView = new Lazy(() => - new EmptyListView(Octicon.Organization.ToEmptyListImage(), "There are no organizations.")); - } + { + public OrganizationsViewController() + { + Title = "Organizations"; + EmptyView = new Lazy(() => + new EmptyListView(Octicon.Organization.ToEmptyListImage(), "There are no organizations.")); + } + + public OrganizationsViewController(string username) : this() + { + var viewModel = new OrganizationsViewModel(); + viewModel.Init(new OrganizationsViewModel.NavObject { Username = username }); + ViewModel = viewModel; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - var vm = (OrganizationsViewModel) ViewModel; - var weakVm = new WeakReference(vm); - BindCollection(vm.Organizations, x => - { - var avatar = new GitHubAvatar(x.AvatarUrl); - var e = new UserElement(x.Login, string.Empty, string.Empty, avatar); - e.Clicked.Subscribe(_ => weakVm.Get()?.GoToOrganizationCommand.Execute(x)); - return e; - }); - } + var vm = (OrganizationsViewModel) ViewModel; + var weakVm = new WeakReference(vm); + BindCollection(vm.Organizations, x => + { + var avatar = new GitHubAvatar(x.AvatarUrl); + var e = new UserElement(x.Login, string.Empty, string.Empty, avatar); + e.Clicked.Subscribe(_ => weakVm.Get()?.GoToOrganizationCommand.Execute(x)); + return e; + }); + } } } diff --git a/CodeHub.iOS/ViewControllers/Organizations/TeamMembersViewController.cs b/CodeHub.iOS/ViewControllers/Organizations/TeamMembersViewController.cs deleted file mode 100644 index b9006e4d..00000000 --- a/CodeHub.iOS/ViewControllers/Organizations/TeamMembersViewController.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CodeHub.iOS.ViewControllers.Users; - -namespace CodeHub.iOS.ViewControllers.Organizations -{ - public class TeamMembersViewController : BaseUserCollectionViewController - { - public TeamMembersViewController() - : base("There are no team members.") - { - Title = "Members"; - } - } -} \ No newline at end of file diff --git a/CodeHub.iOS/ViewControllers/Organizations/TeamsViewController.cs b/CodeHub.iOS/ViewControllers/Organizations/TeamsViewController.cs index d67c7fb6..b5a25262 100644 --- a/CodeHub.iOS/ViewControllers/Organizations/TeamsViewController.cs +++ b/CodeHub.iOS/ViewControllers/Organizations/TeamsViewController.cs @@ -1,32 +1,37 @@ -using System; -using CodeHub.Core.ViewModels.Organizations; -using CodeHub.iOS.DialogElements; -using CodeHub.iOS.ViewControllers; -using CodeHub.iOS.Views; -using UIKit; - +using System; +using CodeHub.Core.ViewModels.Organizations; +using CodeHub.iOS.DialogElements; +using CodeHub.iOS.ViewControllers.Users; +using CodeHub.iOS.Views; +using UIKit; + namespace CodeHub.iOS.ViewControllers.Organizations { public class TeamsViewController : ViewModelCollectionDrivenDialogViewController - { - public TeamsViewController() - { - Title = "Teams"; - EmptyView = new Lazy(() => - new EmptyListView(Octicon.Organization.ToEmptyListImage(), "There are no teams.")); - } + { + public TeamsViewController() + { + Title = "Teams"; + EmptyView = new Lazy(() => + new EmptyListView(Octicon.Organization.ToEmptyListImage(), "There are no teams.")); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var vm = (TeamsViewModel)ViewModel; + var weakVm = new WeakReference(this); - public override void ViewDidLoad() - { - base.ViewDidLoad(); - - var vm = (TeamsViewModel) ViewModel; - var weakVm = new WeakReference(vm); - this.BindCollection(vm.Teams, x => { - var e = new StringElement(x.Name); - e.Clicked.Subscribe(_ => weakVm.Get()?.GoToTeamCommand.Execute(x)); - return e; - }); - } - } + this.BindCollection(vm.Teams, x => + { + var e = new StringElement(x.Name); + e.Clicked.Subscribe(_ => { + var vc = UsersViewController.CreateTeamMembersViewController((int)x.Id); + weakVm.Get()?.NavigationController.PushViewController(vc, true); + }); + return e; + }); + } + } } \ No newline at end of file diff --git a/CodeHub.iOS/ViewControllers/PullRequests/PullRequestDiffViewController.cs b/CodeHub.iOS/ViewControllers/PullRequests/PullRequestDiffViewController.cs new file mode 100644 index 00000000..8d6b3b2b --- /dev/null +++ b/CodeHub.iOS/ViewControllers/PullRequests/PullRequestDiffViewController.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using CodeHub.Core; +using CodeHub.Core.Services; +using CodeHub.iOS.Services; +using CodeHub.iOS.Utilities; +using CodeHub.WebViews; +using Humanizer; +using Newtonsoft.Json; +using ReactiveUI; +using Splat; +using UIKit; +using WebKit; + +namespace CodeHub.iOS.ViewControllers.PullRequests +{ + public class PullRequestDiffViewController : BaseWebViewController + { + private readonly IApplicationService _applicationService; + private readonly INetworkActivityService _networkActivityService; + private readonly IMarkdownService _markdownService; + private readonly string _username; + private readonly string _repository; + private readonly int _pullRequestId; + private readonly string _path; + private readonly string _patch; + private readonly string _commit; + + private readonly ReactiveList _comments + = new ReactiveList(); + + public PullRequestDiffViewController( + string username, + string repository, + int pullRequestId, + string path, + string patch, + string commit, + IApplicationService applicationService = null, + INetworkActivityService networkActivityService = null, + IMarkdownService markdownService = null) + : base(false) + { + _applicationService = applicationService ?? Locator.Current.GetService(); + _networkActivityService = networkActivityService ?? Locator.Current.GetService(); + _markdownService = markdownService ?? Locator.Current.GetService(); + _username = username; + _repository = repository; + _pullRequestId = pullRequestId; + _path = path; + _patch = patch; + _commit = commit; + + Title = string.IsNullOrEmpty(_path) ? "Diff" : System.IO.Path.GetFileName(_path); + + var loadComments = ReactiveCommand.CreateFromTask( + _ => _applicationService.GitHubClient.PullRequest.ReviewComment.GetAll(_username, _repository, _pullRequestId)); + + loadComments + .ThrownExceptions + .Select(error => new UserError("Unable to load comments.", error)) + .SelectMany(Interactions.Errors.Handle) + .Subscribe(); + + loadComments + .Subscribe(comments => _comments.Reset(comments)); + + var loadAll = ReactiveCommand.CreateCombined(new[] { loadComments }); + + Appearing + .Take(1) + .Select(_ => Unit.Default) + .InvokeReactiveCommand(loadAll); + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + Observable + .Return(Unit.Default) + .Merge(_comments.Changed.Select(_ => Unit.Default)) + .Do(_ => Render().ToBackground()) + .Subscribe(); + } + + private async Task Render() + { + var comments = new List(); + foreach (var comment in _comments.Where(x => string.Equals(x.Path, _path))) + { + comments.Add(new DiffCommentModel + { + Id = comment.Id, + GroupId = comment.Id, + Username = comment.User.Login, + AvatarUrl = comment.User.AvatarUrl, + LineTo = comment.Position, + LineFrom = comment.Position, + Body = await _markdownService.Convert(comment.Body), + Date = comment.CreatedAt.Humanize() + }); + } + + var diffModel = new DiffModel( + _patch.Split('\n'), + comments, + (int)UIFont.PreferredSubheadline.PointSize); + + var diffView = new DiffWebView { Model = diffModel }; + LoadContent(diffView.GenerateString()); + } + + private class JavascriptComment + { + public int PatchLine { get; set; } + public int FileLine { get; set; } + } + + private class JavascriptReplyComment + { + public int Id { get; set; } + } + + protected override bool ShouldStartLoad(WKWebView webView, WKNavigationAction navigationAction) + { + var url = navigationAction.Request.Url; + if (url.Scheme.Equals("app")) + { + var func = url.Host; + if (func.Equals("comment")) + { + var commentModel = JsonConvert.DeserializeObject(UrlDecode(url.Fragment)); + PromptForComment(commentModel); + } + else if (func.Equals("reply-to")) + { + var commentModel = JsonConvert.DeserializeObject(UrlDecode(url.Fragment)); + ShowReplyCommentComposer(commentModel.Id); + } + + return false; + } + + return base.ShouldStartLoad(webView, navigationAction); + } + + private void PromptForComment(JavascriptComment model) + { + var title = "Line " + model.PatchLine; + var sheet = new UIActionSheet(title); + var addButton = sheet.AddButton("Add Comment"); + var cancelButton = sheet.AddButton("Cancel"); + sheet.CancelButtonIndex = cancelButton; + sheet.Dismissed += (sender, e) => + { + BeginInvokeOnMainThread(() => + { + if (e.ButtonIndex == addButton) + ShowCommentComposer(model.FileLine); + }); + + sheet.Dispose(); + }; + + sheet.ShowInView(this.View); + } + + private void ShowCommentComposer(int line) + { + ShowComposer(async text => + { + var commentOptions = new Octokit.PullRequestReviewCommentCreate(text, _commit, _path, line); + var comment = await _applicationService.GitHubClient.PullRequest.ReviewComment.Create( + _username, _repository, _pullRequestId, commentOptions); + _comments.Add(comment); + }); + } + + private void ShowReplyCommentComposer(int replyToId) + { + ShowComposer(async text => + { + var commentOptions = new Octokit.PullRequestReviewCommentReplyCreate(text, replyToId); + var comment = await _applicationService.GitHubClient.PullRequest.ReviewComment.CreateReply( + _username, _repository, _pullRequestId, commentOptions); + _comments.Add(comment); + }); + } + + private void ShowComposer(Func workFn) + { + var composer = new MarkdownComposerViewController(); + composer.PresentAsModal(this, async text => + { + var hud = composer.CreateHud(); + + using (UIApplication.SharedApplication.DisableInteraction()) + using (_networkActivityService.ActivateNetwork()) + using (hud.Activate("Commenting...")) + { + try + { + await workFn(text); + composer.DismissViewController(true, null); + } + catch (Exception e) + { + AlertDialogService.ShowAlert("Unable to Comment", e.Message); + } + } + }); + } + } +} + diff --git a/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.cs b/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.cs index c00d4fa8..18614bbc 100644 --- a/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.cs +++ b/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.cs @@ -2,6 +2,7 @@ using UIKit; using CodeHub.iOS.ViewControllers.Application; using System.Reactive.Disposables; +using System.Reactive.Linq; namespace CodeHub.iOS.ViewControllers.Repositories { @@ -14,13 +15,16 @@ public PrivateRepositoryViewController() : base("PrivateRepositoryViewController public override void ViewDidLoad() { base.ViewDidLoad(); - // Perform any additional setup after loading the view, typically from a nib. Button.BackgroundColor = UIColor.FromRGB(0x27, 0xae, 0x60); Button.SetTitleColor(UIColor.White, UIControlState.Normal); Button.Layer.CornerRadius = 6f; - OnActivation(d => d(Button.GetClickedObservable().Subscribe(_ => this.PresentUpgradeViewController()))); + OnActivation(d => + { + d(Button.GetClickedObservable() + .Subscribe(_ => UpgradeViewController.Present(this))); + }); } } @@ -39,7 +43,8 @@ public static IDisposable ShowPrivateView(this UIViewController @this) vc.View.BackgroundColor = Theme.CurrentTheme.PrimaryColor; - @this.NavigationItem.RightBarButtonItem.Do(x => x.Enabled = false); + if (@this.NavigationItem.RightBarButtonItem != null) + @this.NavigationItem.RightBarButtonItem.Enabled = false; return Disposable.Create(() => { diff --git a/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.xib b/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.xib index 2c4c5d6b..58362b4f 100644 --- a/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.xib +++ b/CodeHub.iOS/ViewControllers/Repositories/PrivateRepositoryViewController.xib @@ -37,7 +37,7 @@ -