From d76367b39037be275a08383f9267589717b95c2e Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 14 Sep 2019 20:13:42 +0100 Subject: [PATCH 01/16] fixed WelcomePage xaml + cs not linked in Solution Explorer issue --- .../CarouselViewChallenge.csproj | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index 198ec7f..9d5cb43 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -15,12 +15,9 @@ - - WelcomePage.xaml - - - - + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml From 33bf89f2d388017bd0370c1b18262e70ac245c83 Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 14 Sep 2019 22:58:04 +0100 Subject: [PATCH 02/16] initial UI layout --- .../CarouselViewChallenge/App.xaml | 7 +++ .../CarouselViewChallengeViewModel.cs | 50 +++++++++++++++++++ .../Views/CarouselViewChallengePage.xaml | 45 ++++++++++++++--- .../Views/CarouselViewChallengePage.xaml.cs | 10 ++-- 4 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs diff --git a/CarouselViewChallenge/CarouselViewChallenge/App.xaml b/CarouselViewChallenge/CarouselViewChallenge/App.xaml index f3469f1..a495c26 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/App.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/App.xaml @@ -6,5 +6,12 @@ mc:Ignorable="d" x:Class="CarouselViewChallenge.App"> + + #2F3136 + #484B51 + LightGray + White + #2196F3 + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs new file mode 100644 index 0000000..d63850e --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Text; +using System.Windows.Input; +using Xamarin.Forms; + +namespace CarouselViewChallenge.ViewModels +{ + public class CarouselViewChallengeViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged(string propertyName) + { + var changed = PropertyChanged; + if (changed != null) + { + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public ObservableCollection GalNetNewsList { get; set; } + + private DateTime _galnetLastUpdated; + public DateTime GalNetLastUpdated + { + get { return _galnetLastUpdated; } + private set + { + if (_galnetLastUpdated != value) + { + _galnetLastUpdated = value; + OnPropertyChanged(nameof(GalNetLastUpdated)); + } + } + } + + public CarouselViewChallengeViewModel() + { + GalNetNewsList = new ObservableCollection + { + "First Item", + "Second Item", + "Third Item" + }; + GalNetLastUpdated = DateTime.Now; + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml index 1a7cc0d..743d1e4 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml @@ -3,11 +3,44 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="clr-namespace:CarouselViewChallenge.ViewModels" mc:Ignorable="d" - x:Class="CarouselViewChallenge.Views.CarouselViewChallengePage"> - - - - + x:Class="CarouselViewChallenge.Views.CarouselViewChallengePage" + x:DataType="vm:CarouselViewChallengeViewModel" + BackgroundColor="{StaticResource pageBackground}"> + + + + + + + + + + + + + + + + + + + + + Horizontal + + + + + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml.cs b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml.cs index 38f2e9f..c42482d 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - +using CarouselViewChallenge.ViewModels; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -12,9 +7,12 @@ namespace CarouselViewChallenge.Views [XamlCompilation(XamlCompilationOptions.Compile)] public partial class CarouselViewChallengePage : ContentPage { + private readonly CarouselViewChallengeViewModel vm = new CarouselViewChallengeViewModel(); + public CarouselViewChallengePage() { InitializeComponent(); + BindingContext = vm; } } } \ No newline at end of file From 990ac811586b4fd29afd23f885a7758970d0f4fa Mon Sep 17 00:00:00 2001 From: irongut Date: Sun, 15 Sep 2019 01:07:00 +0100 Subject: [PATCH 03/16] download + parse GalNet news --- CarouselViewChallenge.sln | 2 +- .../CarouselViewChallenge.Android.csproj | 3 + .../CarouselViewChallenge.csproj | 11 ++ .../Converters/NewsItemConverter.cs | 73 +++++++++++ .../GlobalSuppressions.cs | 8 ++ .../Helpers/HttpHelper.cs | 39 ++++++ .../CarouselViewChallenge/Models/NewsItem.cs | 124 ++++++++++++++++++ .../CarouselViewChallenge/Models/Topic.cs | 22 ++++ .../Models/TopicsList.cs | 63 +++++++++ .../Resources/NewsBoW.csv | 50 +++++++ .../Services/APIException.cs | 37 ++++++ .../Services/DownloadService.cs | 67 ++++++++++ .../Services/GalNetService.cs | 24 ++++ .../Services/NoNetworkNoCacheException.cs | 25 ++++ .../CarouselViewChallengeViewModel.cs | 62 +++++++-- .../Views/CarouselViewChallengePage.xaml | 10 +- 16 files changed, 606 insertions(+), 14 deletions(-) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Converters/NewsItemConverter.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Helpers/HttpHelper.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Models/NewsItem.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Models/Topic.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Models/TopicsList.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Resources/NewsBoW.csv create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Services/APIException.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Services/DownloadService.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Services/GalNetService.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Services/NoNetworkNoCacheException.cs diff --git a/CarouselViewChallenge.sln b/CarouselViewChallenge.sln index ca08a3c..426a2aa 100644 --- a/CarouselViewChallenge.sln +++ b/CarouselViewChallenge.sln @@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarouselViewChallenge.Andro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarouselViewChallenge.iOS", "CarouselViewChallenge\CarouselViewChallenge.iOS\CarouselViewChallenge.iOS.csproj", "{E981DF49-12EE-46EC-9F08-D3FC5F4FF693}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarouselViewChallenge", "CarouselViewChallenge\CarouselViewChallenge\CarouselViewChallenge.csproj", "{A9F8B9B3-719F-4F40-9044-924E7B921409}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarouselViewChallenge", "CarouselViewChallenge\CarouselViewChallenge\CarouselViewChallenge.csproj", "{A9F8B9B3-719F-4F40-9044-924E7B921409}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj index 82e6221..e1aa540 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj @@ -53,6 +53,9 @@ + + 1.3.0 + diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index 9d5cb43..b0e24e5 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -6,6 +6,17 @@ + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Converters/NewsItemConverter.cs b/CarouselViewChallenge/CarouselViewChallenge/Converters/NewsItemConverter.cs new file mode 100644 index 0000000..5de38f3 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Converters/NewsItemConverter.cs @@ -0,0 +1,73 @@ +using CarouselViewChallenge.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace CarouselViewChallenge.Converters +{ + public class NewsItemConverter : JsonConverter + { + public static readonly NewsItemConverter Instance = new NewsItemConverter(); + + private static readonly Type NewsItemType = typeof(ICollection); + + public override bool CanConvert(Type objectType) + { + return NewsItemType.IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var collection = new List(); + NewsItem item = null; + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.StartObject: + item = new NewsItem(); + collection.Add(item); + break; + case JsonToken.PropertyName: + SetProperty(reader, item); + break; + case JsonToken.EndArray: + return collection; + } + } + return collection; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + private static void SetProperty(JsonReader reader, NewsItem item) + { + var name = (string)reader.Value; + reader.Read(); + switch (name) + { + case "title": + item.Title = (string)reader.Value; + break; + case "body": + item.Body = (string)reader.Value; + break; + case "date": + item.PublishDateTime = Convert.ToDateTime(reader.Value); + break; + case "nid": + item.Id = Convert.ToInt32(reader.Value); + break; + case "image": + item.FDImageName = (string)reader.Value; + break; + case "slug": + item.Slug = (string)reader.Value; + break; + } + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs new file mode 100644 index 0000000..08014c2 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("General", "RCS1079:Throwing of new NotImplementedException.", Scope = "member", Target = "~M:CarouselViewChallenge.Converters.NewsItemConverter.WriteJson(Newtonsoft.Json.JsonWriter,System.Object,Newtonsoft.Json.JsonSerializer)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Formatting", "RCS1057:Add empty line between declarations.", Scope = "type", Target = "~T:CarouselViewChallenge.Models.NewsItem")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Formatting", "RCS1057:Add empty line between declarations.", Scope = "type", Target = "~T:CarouselViewChallenge.ViewModels.CarouselViewChallengeViewModel")] diff --git a/CarouselViewChallenge/CarouselViewChallenge/Helpers/HttpHelper.cs b/CarouselViewChallenge/CarouselViewChallenge/Helpers/HttpHelper.cs new file mode 100644 index 0000000..8fe3e2a --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Helpers/HttpHelper.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Threading.Tasks; + +namespace CarouselViewChallenge.Helpers +{ + public static class HttpHelper + { + public static async Task ReadContentAsync(HttpResponseMessage response) + { + string content; + + if (response.Content.Headers.ContentEncoding.Contains("gzip")) + { + Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using (GZipStream gzipStream = new GZipStream(stream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream)) + { + content = reader.ReadToEnd(); + } + } + else if (response.Content.Headers.ContentEncoding.Contains("deflate")) + { + Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using (DeflateStream gzipStream = new DeflateStream(stream, CompressionMode.Decompress)) + using (StreamReader reader = new StreamReader(gzipStream)) + { + content = reader.ReadToEnd(); + } + } + else + { + content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + return content; + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Models/NewsItem.cs b/CarouselViewChallenge/CarouselViewChallenge/Models/NewsItem.cs new file mode 100644 index 0000000..7a398da --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Models/NewsItem.cs @@ -0,0 +1,124 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace CarouselViewChallenge.Models +{ + public class NewsItem + { + #region Properties + + private string _title; + [JsonProperty(PropertyName = "title")] + public string Title + { + get { return _title; } + internal set + { + if (_title != value) + { + _title = value.Trim(); + } + } + } + + private string _body; + [JsonProperty(PropertyName = "body")] + public string Body + { + get { return _body; } + internal set + { + if (_body != value) + { + _body = value.Replace("

", "").Replace("

", "\n").Replace("
", "\n").Replace("'", "'").Replace(""", "\u201d").Trim(); + } + } + } + + [JsonProperty(PropertyName = "date")] + public DateTime PublishDateTime { get; internal set; } + + [JsonProperty(PropertyName = "nid")] + public int Id { get; internal set; } + + [JsonProperty(PropertyName = "image")] + public string FDImageName { get; internal set; } + + [JsonProperty(PropertyName = "slug")] + public string Slug { get; internal set; } + + public string Topic { get; private set; } + public List Tags { get; private set; } + + public string PublishDate + { + get + { + // return the date part of PublishDateTime + string date = PublishDateTime.ToString(); + return date.Substring(0, date.IndexOf(" ")); + } + } + + #endregion + + public override string ToString() + { + return String.Format("{0}: {1}", Title, Body); + } + + private List SplitSentences() + { + List sentences = new List + { + Title.Trim().ToLower(), + Slug.Replace("-", " ").Trim().ToLower() + }; + foreach (string sentence in Regex.Split(Body, @"(?<=[\w\s](?:[\.\!\? ]+[\x20]*[\x22\xBB]*))(?:\s+(?![\x22\xBB](?!\w)))")) + { + sentences.Add(sentence.Trim().ToLower()); + } + return sentences; + } + + public void ClassifyArticle() + { + Tags = new List(); + // analyse article using Bag of Words technique + TopicsList topicsList = new TopicsList("CarouselViewChallenge.Resources.NewsBoW.csv"); + foreach (string sentence in SplitSentences()) + { + foreach (Topic topic in topicsList.Topics) + { + foreach (string term in topic.Terms) + { + if (sentence.Contains(term.ToLower())) + { + topic.Count++; + } + } + } + } + // select topic + tags + Topic tempTopic = topicsList.Topics.OrderByDescending(o => o.Count).First(); + if (tempTopic.Count < 2 || string.Equals(Title, "week in review", StringComparison.OrdinalIgnoreCase)) + { + Topic = "Unclassified"; + } + else + { + Topic = tempTopic.Name; + } + foreach (Topic topic in topicsList.Topics.OrderByDescending(o => o.Count).Take(5)) + { + if (topic.Count > 0) + { + Tags.Add(topic.Name); + } + } + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Models/Topic.cs b/CarouselViewChallenge/CarouselViewChallenge/Models/Topic.cs new file mode 100644 index 0000000..ff9ac4b --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Models/Topic.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace CarouselViewChallenge.Models +{ + public class Topic + { + #region Properties + + public string Name { get; } + public List Terms { get; } + public int Count { get; set; } + + #endregion + + public Topic(string name, List terms) + { + Name = name; + Terms = terms; + Count = 0; + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Models/TopicsList.cs b/CarouselViewChallenge/CarouselViewChallenge/Models/TopicsList.cs new file mode 100644 index 0000000..db25174 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Models/TopicsList.cs @@ -0,0 +1,63 @@ +using CsvHelper; +using CsvHelper.Configuration; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace CarouselViewChallenge.Models +{ + public class TopicsList + { + private readonly string BoWFilename; + + #region Properties + + public List Topics { get; set; } + + #endregion + + public TopicsList(string filename) + { + BoWFilename = filename; + Topics = new List(); + GetBagOfWords(); + } + + private void GetBagOfWords() + { + try + { + Assembly assembly = GetType().GetTypeInfo().Assembly; + using (Stream stream = assembly.GetManifestResourceStream(BoWFilename)) + { + using (StreamReader reader = new StreamReader(stream)) + { + Configuration csvConfig = new Configuration + { + Delimiter = ",", + IgnoreQuotes = true + }; + using (CsvReader csv = new CsvReader(reader, csvConfig)) + { + while (csv.Read()) + { + string name = csv.GetField(0); + List terms = new List(); + for (int i = 1; csv.TryGetField(i, out string value); i++) + { + terms.Add(value); + } + Topics.Add(new Topic(name, terms)); + } + } + } + } + } + catch (Exception ex) + { + throw new Exception("Unable to load Bag of Words for linguistic analysis.", ex); + } + } + } +} \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/NewsBoW.csv b/CarouselViewChallenge/CarouselViewChallenge/Resources/NewsBoW.csv new file mode 100644 index 0000000..d0786e0 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/NewsBoW.csv @@ -0,0 +1,50 @@ +Aliens,alien,thargoid,guardian,aegis,meta-alloy,incursion,xenological +Crime,crime,criminal,illegal,theft,steal,stole,murder,homicide,assassin,arrest,hijack,blackmail,hack,kidnap,corrupt,prosecution,guilt,unlicensed,regulation,police,cartel,narcotic,autopsy,black market +Combat,combat,conflict,battle,navy,naval,armada,military,bounty,bounties,insurrection,kill order,hostilities,mercenar,violent,raid,attack,defence +Culture,culture,fine art,sculpture,painting,literature,artist,author,festival +Economy,economy,economic,trade,deliver,construct,investor,supplier,logistics,merger,conglomerate,commodities,business analyst,investment,wealth,fortune,ambrose foundation,industry,industrial,manufacture,engineering,bank of zaonce,megaship,resources,homeless,immigra +Exploration,exploration,explorers,distant worlds,mapping,cartographics,unexplored,sagittarius,a*,beagle point,galactic core,codex +Health,health,medical,medicine,meds,pharmaceutical,outbreak,disease,illness,injury,injuries,clinic,immune,patient,addict,narcotic +Mining,mining,miner,mineral,prospector +Mystery,mystery,mysteri,vanish,phenomenon,missing,disappear,anomaly,inexplicabl,dream journal,visions,gan romero,halsey,starship one,raxxla,dark wheel,omphalos rift,salomé,salome,metadrive +Politics,politic,election,senate,senator,congress,president,rebellion,diplomacy,corrupt,emancipat,homeless,immigra,activist,anti-slavery +Religion,religion,god,church,worship,faith,theology,thelogian +Science,scientific,technolog,professor,engineer,hyperspace,research,canonn,anomaly,codex +Alliance,alliance,mahon,yamamoto,kincaid,interpol,bank of zaonce +Empire,empire,imperial,emperor,arissa,lavigny,duval,patreus,torval,senate,senator,chancellor,blaine,slave,achenar,capitol,imperium +Federation,federation,federal,hudson,winters,halsey,congress,olympus village +AD,aisling duval +Delaine,archon delaine +ALD,arissa lavigny-duval,lavigny-duval,emperor +Patreus,denton patreus,patreus +Mahon,edmund mahon,prime minister mahon +Winters,felicia winters,shadow president winters +LYR,li yong-rui,yong-rui,sirius corp +Antal,pranav antal,simguru,utopia +Grom,yuri grom,eg pilots +Hudson,zachary hudson,president hudson +Torval,zemina torval,senator torval +Turner,bill turner,turner metallics inc +Tarquin,broo tarquin,broo's legacy +Dekker,colonel bris dekker,dekker's yard +Vatermann,didi vatermann,vatermann llc +Martuuk,elvira martuuk,long sight base +Dorn,etienne dorn,kraken's retreat +Farseer,felicity farseer,farseer inc +Tani,hera tani,the jet's hole +Ishmaak,juri ishmaak,pater's memorial +Cheung,lei cheung,trader's rest +Ryder,liz ryder,demolition unlimited +Jameson,lori jameson,jameson base +Qwent,marco qwent,qwent research base +Hicks,marsha hicks,the watchtower +Brandon,mel brandon,the brig +Olmanova,petra olmanova,asura +Palin,professor palin,palin research centre +Ram Tah,ram tah,phoenix base +Jean,selene jean,prospector's rest +Dweller,the dweller,black hide +Sarge,the sarge,beta-3 tucani +Fortune,tiana fortune,fortune's loss +Blaster,tod "the blaster" mcquinn,trophy camp +Nemo,zacariah nemo,nemo cyber party base \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Services/APIException.cs b/CarouselViewChallenge/CarouselViewChallenge/Services/APIException.cs new file mode 100644 index 0000000..fbac808 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Services/APIException.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.Serialization; + +namespace CarouselViewChallenge.Services +{ + [Serializable] + public class APIException : Exception + { + public int? StatusCode { get; } + + public APIException() + { + } + + public APIException(string message) : base(message) + { + } + + public APIException(string message, Exception innerException) : base(message, innerException) + { + } + + protected APIException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public APIException(string message, int? code) : base(message) + { + StatusCode = code; + } + + public APIException(string message, Exception innerException, int? code) : base(message, innerException) + { + StatusCode = code; + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Services/DownloadService.cs b/CarouselViewChallenge/CarouselViewChallenge/Services/DownloadService.cs new file mode 100644 index 0000000..0d2df93 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Services/DownloadService.cs @@ -0,0 +1,67 @@ +using CarouselViewChallenge.Helpers; +using MonkeyCache.FileStore; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Xamarin.Essentials; + +namespace CarouselViewChallenge.Services +{ + public sealed class DownloadService + { + private static readonly DownloadService instance = new DownloadService(); + + private readonly HttpClient client; + + private DownloadService() + { + client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Requested-With", "CarouselViewChallenge"); + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate")); + client.Timeout = TimeSpan.FromSeconds(40); + } + + public static DownloadService Instance() + { + return instance; + } + + public async Task<(string data, DateTime updated)> GetData(string url, string dataKey, string lastUpdatedKey, TimeSpan expiry, bool ignoreCache = false) + { + string data; + DateTime lastUpdated; + + if (!ignoreCache && Barrel.Current.Exists(dataKey) && !Barrel.Current.IsExpired(dataKey)) + { + // use cached data + data = Barrel.Current.Get(dataKey); + lastUpdated = DateTime.Parse(Barrel.Current.Get(lastUpdatedKey)); + } + else if (Connectivity.NetworkAccess != NetworkAccess.Internet) + { + throw new NoNetworkNoCacheException("No Internet available and no data cached."); + } + else + { + // download data + var uri = new Uri(url); + HttpResponseMessage response = await client.GetAsync(uri).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + throw new APIException(String.Format("{0} - {1}", response.StatusCode, response.ReasonPhrase), (int)response.StatusCode); + } + else + { + data = await HttpHelper.ReadContentAsync(response).ConfigureAwait(false); + lastUpdated = DateTime.Now; + // cache data + Barrel.Current.Add(dataKey, data, expiry); + Barrel.Current.Add(lastUpdatedKey, lastUpdated.ToString(), expiry); + } + } + return (data, lastUpdated); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Services/GalNetService.cs b/CarouselViewChallenge/CarouselViewChallenge/Services/GalNetService.cs new file mode 100644 index 0000000..7ab457f --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Services/GalNetService.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; + +namespace CarouselViewChallenge.Services +{ + public class GalNetService + { + private const string GalNetURL = "https://elitedangerous-website-backend-production.elitedangerous.com/api/galnet?_format=json"; + private const string dataKey = "NewsFeed"; + private const string lastUpdatedKey = "NewsLastUpdated"; + + public async Task<(string data, DateTime updated)> GetData(bool ignoreCache = false) + { + DateTime lastUpdated; + string json = String.Empty; + TimeSpan expiry = TimeSpan.FromHours(1); + + // download the json feed + DownloadService downloadService = DownloadService.Instance(); + (json, lastUpdated) = await downloadService.GetData(GalNetURL, dataKey, lastUpdatedKey, expiry, ignoreCache).ConfigureAwait(false); + return (json, lastUpdated); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Services/NoNetworkNoCacheException.cs b/CarouselViewChallenge/CarouselViewChallenge/Services/NoNetworkNoCacheException.cs new file mode 100644 index 0000000..04f8017 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Services/NoNetworkNoCacheException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace CarouselViewChallenge.Services +{ + [Serializable] + public class NoNetworkNoCacheException : Exception + { + public NoNetworkNoCacheException() + { + } + + public NoNetworkNoCacheException(string message) : base(message) + { + } + + public NoNetworkNoCacheException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NoNetworkNoCacheException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs index d63850e..04b2be5 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs @@ -1,9 +1,14 @@ -using System; +using CarouselViewChallenge.Converters; +using CarouselViewChallenge.Models; +using CarouselViewChallenge.Services; +using MonkeyCache.FileStore; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Text; -using System.Windows.Input; +using System.Linq; +using System.Threading.Tasks; using Xamarin.Forms; namespace CarouselViewChallenge.ViewModels @@ -20,7 +25,7 @@ protected virtual void OnPropertyChanged(string propertyName) } } - public ObservableCollection GalNetNewsList { get; set; } + public ObservableCollection GalNetNewsList { get; set; } private DateTime _galnetLastUpdated; public DateTime GalNetLastUpdated @@ -36,15 +41,52 @@ private set } } + private string _message; + public string Message + { + get { return _message; } + set + { + if (_message != value) + { + _message = value; + OnPropertyChanged(nameof(Message)); + } + } + } + public CarouselViewChallengeViewModel() { - GalNetNewsList = new ObservableCollection + Barrel.ApplicationId = "com.carouselchallenge.galnet"; + GalNetNewsList = new ObservableCollection(); + GetGalNetNewsAsync(); + } + + private async void GetGalNetNewsAsync(bool ignoreCache = false) + { + try { - "First Item", - "Second Item", - "Third Item" - }; - GalNetLastUpdated = DateTime.Now; + // get the news feed + string json = String.Empty; + GalNetService news = new GalNetService(); + (json, GalNetLastUpdated) = await news.GetData(ignoreCache).ConfigureAwait(false); + + // parse the json data + GalNetNewsList.Clear(); + await Task.Run(() => + { + List fullNews = JsonConvert.DeserializeObject>(json, NewsItemConverter.Instance); + foreach (NewsItem item in fullNews.Where(o => !String.IsNullOrEmpty(o.Body)).OrderByDescending(o => o.PublishDateTime).Take(20)) + { + item.ClassifyArticle(); + Device.BeginInvokeOnMainThread(() => GalNetNewsList.Add(item)); + } + }).ConfigureAwait(false); + } + catch (Exception ex) + { + Message = String.Format("Error: {0}", ex.Message); + } } } } diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml index 743d1e4..f491332 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="clr-namespace:CarouselViewChallenge.Models" xmlns:vm="clr-namespace:CarouselViewChallenge.ViewModels" mc:Ignorable="d" x:Class="CarouselViewChallenge.Views.CarouselViewChallengePage" @@ -11,10 +12,10 @@ - + - @@ -27,7 +28,10 @@
+
\ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge.UWP/CarouselViewChallenge.UWP.csproj b/CarouselViewChallenge/CarouselViewChallenge.UWP/CarouselViewChallenge.UWP.csproj index 4b6b2c9..55db78b 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.UWP/CarouselViewChallenge.UWP.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge.UWP/CarouselViewChallenge.UWP.csproj @@ -146,8 +146,8 @@ - - + + @@ -159,4 +159,4 @@ 14.0 - + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge.iOS/CarouselViewChallenge.iOS.csproj b/CarouselViewChallenge/CarouselViewChallenge.iOS/CarouselViewChallenge.iOS.csproj index 6241d2c..f5738b9 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.iOS/CarouselViewChallenge.iOS.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge.iOS/CarouselViewChallenge.iOS.csproj @@ -130,7 +130,7 @@ - + @@ -139,4 +139,4 @@ CarouselViewChallenge - + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index 7f32b01..f7f6c3e 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -75,7 +75,7 @@ - + From 80e7461b8e6b1fbaa7f790aa59e7919dcf3b5f37 Mon Sep 17 00:00:00 2001 From: irongut Date: Fri, 20 Sep 2019 21:34:53 +0100 Subject: [PATCH 11/16] fix change to CarouselView.ItemsLayout --- .../Views/CarouselViewChallengePage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml index 95b86e8..93e4eb0 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml @@ -88,11 +88,11 @@ PeekAreaInsets="80" VerticalOptions="StartAndExpand"> - + Horizontal - + From 6177cfd0fb3abf40c47590f477a84fde2272a3bf Mon Sep 17 00:00:00 2001 From: irongut Date: Fri, 20 Sep 2019 23:50:09 +0100 Subject: [PATCH 12/16] initial UI layout for Fortify page --- .../CarouselViewChallenge/AppShell.xaml | 3 + .../CarouselViewChallenge.csproj | 3 + .../GlobalSuppressions.cs | 2 + .../ViewModels/CarouselViewTwoViewModel.cs | 98 +++++++++++++++++++ .../Views/CarouselViewTwoPage.xaml | 88 +++++++++++++++++ .../Views/CarouselViewTwoPage.xaml.cs | 18 ++++ 6 files changed, 212 insertions(+) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml.cs diff --git a/CarouselViewChallenge/CarouselViewChallenge/AppShell.xaml b/CarouselViewChallenge/CarouselViewChallenge/AppShell.xaml index 7f8fdee..32dd03a 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/AppShell.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/AppShell.xaml @@ -39,6 +39,9 @@ + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index f7f6c3e..52a6234 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -83,6 +83,9 @@ + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml diff --git a/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs index c93ffd7..35e42c2 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs @@ -7,3 +7,5 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Formatting", "RCS1057:Add empty line between declarations.", Scope = "type", Target = "~T:CarouselViewChallenge.Models.NewsItem")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Formatting", "RCS1057:Add empty line between declarations.", Scope = "type", Target = "~T:CarouselViewChallenge.ViewModels.CarouselViewChallengeViewModel")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("General", "RCS1079:Throwing of new NotImplementedException.", Scope = "member", Target = "~M:CarouselViewChallenge.Converters.NewsTopicToImage.ConvertBack(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)~System.Object")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Formatting", "RCS1057:Add empty line between declarations.", Justification = "", Scope = "type", Target = "~T:CarouselViewChallenge.ViewModels.CarouselViewTwoViewModel")] + diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs new file mode 100644 index 0000000..92bea77 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; + +namespace CarouselViewChallenge.ViewModels +{ + public class CarouselViewTwoViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged(string propertyName) + { + var changed = PropertyChanged; + if (changed != null) + { + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public ObservableCollection LargeShipList { get; set; } + + public ObservableCollection SmallShipList { get; set; } + + public string _ccBalance; + public string CCBalance + { + get { return _ccBalance; } + private set + { + if (_ccBalance != value) + { + _ccBalance = value; + OnPropertyChanged(nameof(CCBalance)); + } + } + } + + private string _cycle; + public string Cycle + { + get { return _cycle; } + protected set + { + if (_cycle != value) + { + _cycle = value; + OnPropertyChanged(nameof(Cycle)); + } + } + } + + private DateTime _lastUpdated; + public DateTime LastUpdated + { + get { return _lastUpdated; } + private set + { + if (_lastUpdated != value) + { + _lastUpdated = value; + OnPropertyChanged(nameof(LastUpdated)); + } + } + } + + public CarouselViewTwoViewModel() + { + LargeShipList = new ObservableCollection + { + "First Large Item", + "Second Large Item", + "Third Large Item", + "Fourth Large Item", + "Fifth Large Item", + "Sixth Large Item", + "Seventh Large Item", + "Eighth Large Item", + "Ninth Large Item", + "Tenth Large Item" + }; + SmallShipList = new ObservableCollection + { + "First Small Item", + "Second Small Item", + "Third Small Item", + "Fourth Small Item", + "Fifth Small Item", + "Sixth Small Item", + "Seventh Small Item", + "Eighth Small Item", + "Ninth Small Item", + "Tenth Small Item" + }; + Cycle = "Cycle 150"; + CCBalance = "853 CC"; + LastUpdated = DateTime.Now; + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml new file mode 100644 index 0000000..167ea78 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml.cs b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml.cs new file mode 100644 index 0000000..8936126 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml.cs @@ -0,0 +1,18 @@ +using CarouselViewChallenge.ViewModels; +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace CarouselViewChallenge.Views +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class CarouselViewTwoPage : ContentPage + { + private readonly CarouselViewTwoViewModel vm = new CarouselViewTwoViewModel(); + + public CarouselViewTwoPage() + { + InitializeComponent(); + BindingContext = vm; + } + } +} \ No newline at end of file From 38230bf129cddbd24014c7747ed37c9b1e9c6355 Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 21 Sep 2019 00:40:12 +0100 Subject: [PATCH 13/16] added mock data for Fort targets --- .../CarouselViewChallenge/Models/FortItem.cs | 27 ++++++++++ .../ViewModels/CarouselViewTwoViewModel.cs | 52 ++++++++++--------- .../Views/CarouselViewTwoPage.xaml | 5 +- 3 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Models/FortItem.cs diff --git a/CarouselViewChallenge/CarouselViewChallenge/Models/FortItem.cs b/CarouselViewChallenge/CarouselViewChallenge/Models/FortItem.cs new file mode 100644 index 0000000..60fb353 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Models/FortItem.cs @@ -0,0 +1,27 @@ +using System; + +namespace CarouselViewChallenge.Models +{ + public class FortItem + { + public string SystemName { get; set; } + public string DistanceToHQ { get; } + public string StationPad { get; } + public string StationDistance { get; } + public string MajorFaction { get; } + + public FortItem(string systemName, string distanceToHQ, string stationPad, string stationDistance, string majorFaction) + { + SystemName = systemName; + DistanceToHQ = distanceToHQ; + StationPad = stationPad; + StationDistance = stationDistance; + MajorFaction = majorFaction; + } + + public override string ToString() + { + return String.Format("{0} ({1}) - {2} ({3})", SystemName, DistanceToHQ, StationPad, StationDistance); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs index 92bea77..b589c65 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs @@ -1,4 +1,5 @@ -using System; +using CarouselViewChallenge.Models; +using System; using System.Collections.ObjectModel; using System.ComponentModel; @@ -16,9 +17,9 @@ protected virtual void OnPropertyChanged(string propertyName) } } - public ObservableCollection LargeShipList { get; set; } + public ObservableCollection LargeShipList { get; set; } - public ObservableCollection SmallShipList { get; set; } + public ObservableCollection SmallShipList { get; set; } public string _ccBalance; public string CCBalance @@ -64,31 +65,32 @@ private set public CarouselViewTwoViewModel() { - LargeShipList = new ObservableCollection + // fake data + LargeShipList = new ObservableCollection { - "First Large Item", - "Second Large Item", - "Third Large Item", - "Fourth Large Item", - "Fifth Large Item", - "Sixth Large Item", - "Seventh Large Item", - "Eighth Large Item", - "Ninth Large Item", - "Tenth Large Item" + new FortItem("Xinca", "101 ly", "station", "343 ls", "independent"), + new FortItem("Ngarawe", "86 ly", "station", "35 ls", "empire"), + new FortItem("Yanerones", "115 ly", "planetary", "42 ls", "empire"), + new FortItem("Damoorai", "86 ly", "station", "1,324 ls", "empire"), + new FortItem("Ostyat", "117 ly", "station", "41 ls", "independent"), + new FortItem("Itzamni", "64 ly", "planetary", "158,992 ls", "empire"), + new FortItem("HIP 20524", "53 ly", "planetary", "2,192 ls", "empire"), + new FortItem("Amenta", "47 ly", "station", "236 ls", "empire"), + new FortItem("27 G. Caeli", "54 ly", "outpost", "3,145 ls", "independent"), + new FortItem("Kappa Reticuli", "81 ly", "planetary", "350 ls", "federation") }; - SmallShipList = new ObservableCollection + SmallShipList = new ObservableCollection { - "First Small Item", - "Second Small Item", - "Third Small Item", - "Fourth Small Item", - "Fifth Small Item", - "Sixth Small Item", - "Seventh Small Item", - "Eighth Small Item", - "Ninth Small Item", - "Tenth Small Item" + new FortItem("Lutni", "30 ly", "planetary", "21 ls", "empire"), + new FortItem("AB Pictoris", "41 ly", "station", "1,739 ls", "empire"), + new FortItem("HIP 21778", "66 ly", "outpost", "181 ls", "independent"), + new FortItem("Biliri", "68 ly", "outpost", "181 ls", "empire"), + new FortItem("Ngarawe", "86 ly", "station", "35 ls", "empire"), + new FortItem("Lopocares", "125 ly", "outpost", "329 ls", "federation"), + new FortItem("Carverda", "41 ly", "station", "727 ls", "empire"), + new FortItem("HIP 32812", "62 ly", "outpost", "1,753 ls", "federation"), + new FortItem("Tiburnat", "53 ly", "planetary", "2,192 ls", "empire"), + new FortItem("Ju Shiva", "47 ly", "station", "236 ls", "independent"), }; Cycle = "Cycle 150"; CCBalance = "853 CC"; diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml index 167ea78..223c772 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="clr-namespace:CarouselViewChallenge.Models" xmlns:vm="clr-namespace:CarouselViewChallenge.ViewModels" mc:Ignorable="d" x:Class="CarouselViewChallenge.Views.CarouselViewTwoPage" @@ -11,10 +12,10 @@ - + - From 282df22fa4ec4b5187d2937fc643373b7ca704cd Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 21 Sep 2019 01:17:15 +0100 Subject: [PATCH 14/16] implement new Fortify UI --- .../CarouselViewChallenge.csproj | 8 +++ .../Converters/FactionToLogo.cs | 38 ++++++++++ .../Converters/StationPadToImage.cs | 39 +++++++++++ .../Resources/coriolis.orange.svg | 34 +++++++++ .../Resources/independent.orange.svg | 58 ++++++++++++++++ .../Resources/outpost.orange.svg | 69 +++++++++++++++++++ .../Resources/surface-port.orange.svg | 62 +++++++++++++++++ .../ViewModels/CarouselViewTwoViewModel.cs | 40 +++++------ .../Views/CarouselViewTwoPage.xaml | 54 ++++++++++++--- 9 files changed, 374 insertions(+), 28 deletions(-) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Converters/FactionToLogo.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Converters/StationPadToImage.cs create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Resources/coriolis.orange.svg create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Resources/independent.orange.svg create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Resources/outpost.orange.svg create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Resources/surface-port.orange.svg diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index 52a6234..97463be 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -12,6 +12,7 @@ + @@ -23,15 +24,18 @@ + + + @@ -44,6 +48,7 @@ + @@ -55,15 +60,18 @@ + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Converters/FactionToLogo.cs b/CarouselViewChallenge/CarouselViewChallenge/Converters/FactionToLogo.cs new file mode 100644 index 0000000..d8cfbdb --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Converters/FactionToLogo.cs @@ -0,0 +1,38 @@ +using System; +using System.Globalization; +using Xamarin.Forms; + +namespace CarouselViewChallenge.Converters +{ + internal class FactionToLogo : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return String.Empty; + } + + string faction = ((string)value).Trim().ToLower(); + switch (faction) + { + case "alliance": + return "resource://CarouselViewChallenge.Resources.alliance.green.svg"; + case "empire": + return "resource://CarouselViewChallenge.Resources.empire.blue.svg"; + case "federation": + return "resource://CarouselViewChallenge.Resources.federation.red.svg"; + case "independent": + case "independant": // Uranius can't spell + return "resource://CarouselViewChallenge.Resources.independent.orange.svg"; + default: + return String.Empty; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Converters/StationPadToImage.cs b/CarouselViewChallenge/CarouselViewChallenge/Converters/StationPadToImage.cs new file mode 100644 index 0000000..3d35559 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Converters/StationPadToImage.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using Xamarin.Forms; + +namespace CarouselViewChallenge.Converters +{ + internal class StationPadToImage : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return String.Empty; + } + + string pad = ((string)value).Trim().ToLower(); + switch (pad) + { + case "large": + case "station": + return "resource://CarouselViewChallenge.Resources.coriolis.orange.svg"; + case "large (planet)": + case "large(planet)": + case "planetary": + return "resource://CarouselViewChallenge.Resources.surface-port.orange.svg"; + case "medium": + case "outpost": + return "resource://CarouselViewChallenge.Resources.outpost.orange.svg"; + default: + return String.Empty; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/coriolis.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/coriolis.orange.svg new file mode 100644 index 0000000..789499d --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/coriolis.orange.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/independent.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/independent.orange.svg new file mode 100644 index 0000000..a0f2a28 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/independent.orange.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/outpost.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/outpost.orange.svg new file mode 100644 index 0000000..251dceb --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/outpost.orange.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/surface-port.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/surface-port.orange.svg new file mode 100644 index 0000000..a1e3aef --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/surface-port.orange.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs index b589c65..1372ca0 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs @@ -68,29 +68,29 @@ public CarouselViewTwoViewModel() // fake data LargeShipList = new ObservableCollection { - new FortItem("Xinca", "101 ly", "station", "343 ls", "independent"), - new FortItem("Ngarawe", "86 ly", "station", "35 ls", "empire"), - new FortItem("Yanerones", "115 ly", "planetary", "42 ls", "empire"), - new FortItem("Damoorai", "86 ly", "station", "1,324 ls", "empire"), - new FortItem("Ostyat", "117 ly", "station", "41 ls", "independent"), - new FortItem("Itzamni", "64 ly", "planetary", "158,992 ls", "empire"), - new FortItem("HIP 20524", "53 ly", "planetary", "2,192 ls", "empire"), - new FortItem("Amenta", "47 ly", "station", "236 ls", "empire"), - new FortItem("27 G. Caeli", "54 ly", "outpost", "3,145 ls", "independent"), - new FortItem("Kappa Reticuli", "81 ly", "planetary", "350 ls", "federation") + new FortItem("Xinca", "101 ly", "Station", "343 ls", "independent"), + new FortItem("Ngarawe", "86 ly", "Station", "35 ls", "empire"), + new FortItem("Yanerones", "115 ly", "Planetary", "42 ls", "empire"), + new FortItem("Damoorai", "86 ly", "Station", "1,324 ls", "empire"), + new FortItem("Ostyat", "117 ly", "Station", "41 ls", "independent"), + new FortItem("Itzamni", "64 ly", "Planetary", "158,992 ls", "empire"), + new FortItem("HIP 20524", "53 ly", "Planetary", "2,192 ls", "empire"), + new FortItem("Amenta", "47 ly", "Station", "236 ls", "empire"), + new FortItem("27 G. Caeli", "54 ly", "Outpost", "3,145 ls", "independent"), + new FortItem("Kappa Reticuli", "81 ly", "Planetary", "350 ls", "federation") }; SmallShipList = new ObservableCollection { - new FortItem("Lutni", "30 ly", "planetary", "21 ls", "empire"), - new FortItem("AB Pictoris", "41 ly", "station", "1,739 ls", "empire"), - new FortItem("HIP 21778", "66 ly", "outpost", "181 ls", "independent"), - new FortItem("Biliri", "68 ly", "outpost", "181 ls", "empire"), - new FortItem("Ngarawe", "86 ly", "station", "35 ls", "empire"), - new FortItem("Lopocares", "125 ly", "outpost", "329 ls", "federation"), - new FortItem("Carverda", "41 ly", "station", "727 ls", "empire"), - new FortItem("HIP 32812", "62 ly", "outpost", "1,753 ls", "federation"), - new FortItem("Tiburnat", "53 ly", "planetary", "2,192 ls", "empire"), - new FortItem("Ju Shiva", "47 ly", "station", "236 ls", "independent"), + new FortItem("Lutni", "30 ly", "Planetary", "21 ls", "empire"), + new FortItem("AB Pictoris", "41 ly", "Station", "1,739 ls", "empire"), + new FortItem("HIP 21778", "66 ly", "Outpost", "181 ls", "independent"), + new FortItem("Biliri", "68 ly", "Outpost", "181 ls", "empire"), + new FortItem("Ngarawe", "86 ly", "Station", "35 ls", "empire"), + new FortItem("Lopocares", "125 ly", "Outpost", "329 ls", "federation"), + new FortItem("Carverda", "41 ly", "Station", "727 ls", "empire"), + new FortItem("HIP 32812", "62 ly", "Outpost", "1,753 ls", "federation"), + new FortItem("Tiburnat", "53 ly", "Planetary", "2,192 ls", "empire"), + new FortItem("Ju Shiva", "47 ly", "Station", "236 ls", "independent"), }; Cycle = "Cycle 150"; CCBalance = "853 CC"; diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml index 223c772..d8d53dd 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml @@ -3,6 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ffsvg="clr-namespace:FFImageLoading.Svg.Forms;assembly=FFImageLoading.Svg.Forms" + xmlns:c="clr-namespace:CarouselViewChallenge.Converters" xmlns:m="clr-namespace:CarouselViewChallenge.Models" xmlns:vm="clr-namespace:CarouselViewChallenge.ViewModels" mc:Ignorable="d" @@ -12,10 +14,43 @@ + + + - - @@ -24,7 +59,7 @@ - + - - + From bc65863cbe617343bb04a9b6c046aea2dda9b6c5 Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 21 Sep 2019 20:50:03 +0100 Subject: [PATCH 15/16] show errors with a Toast because EmptyView not working --- .../CarouselViewChallenge.Android.csproj | 1 + .../CarouselViewChallenge.Android/MainActivity.cs | 4 +++- .../CarouselViewChallenge.csproj | 1 + .../CarouselViewChallenge/Helpers/ToastHelper.cs | 14 ++++++++++++++ .../ViewModels/CarouselViewChallengeViewModel.cs | 2 ++ 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 CarouselViewChallenge/CarouselViewChallenge/Helpers/ToastHelper.cs diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj index 0086af1..49627f7 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj @@ -58,6 +58,7 @@ + diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs b/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs index ab9977b..70e0123 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs @@ -1,4 +1,5 @@ -using Android.App; +using Acr.UserDialogs; +using Android.App; using Android.Content.PM; using Android.OS; using Android.Runtime; @@ -21,6 +22,7 @@ protected override void OnCreate(Bundle savedInstanceState) CachedImageRenderer.Init(true); var ignore = typeof(SvgCachedImage); + UserDialogs.Init(this); Xamarin.Essentials.Platform.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); diff --git a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj index 97463be..08642af 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -78,6 +78,7 @@ + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Helpers/ToastHelper.cs b/CarouselViewChallenge/CarouselViewChallenge/Helpers/ToastHelper.cs new file mode 100644 index 0000000..0692e09 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Helpers/ToastHelper.cs @@ -0,0 +1,14 @@ +using Acr.UserDialogs; + +namespace CarouselViewChallenge.Helpers +{ + public static class ToastHelper + { + public static void Toast(string message) + { + ToastConfig toastConfig = new ToastConfig(message); + toastConfig.SetDuration(2500); + UserDialogs.Instance.Toast(toastConfig); + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs index 85ad3dc..488c39c 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs @@ -1,4 +1,5 @@ using CarouselViewChallenge.Converters; +using CarouselViewChallenge.Helpers; using CarouselViewChallenge.Models; using CarouselViewChallenge.Services; using MonkeyCache.FileStore; @@ -88,6 +89,7 @@ await Task.Run(() => catch (Exception ex) { Message = String.Format("Error: {0}", ex.Message); + ToastHelper.Toast(Message); } } } From 73e3b0e8b5d4447188ad89074f84eabc960e7575 Mon Sep 17 00:00:00 2001 From: irongut Date: Sat, 21 Sep 2019 21:19:01 +0100 Subject: [PATCH 16/16] copy system name on tap --- .../ViewModels/CarouselViewTwoViewModel.cs | 15 ++++++++++++++- .../Views/CarouselViewTwoPage.xaml | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs index 1372ca0..9c7c656 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs @@ -1,7 +1,11 @@ -using CarouselViewChallenge.Models; +using CarouselViewChallenge.Helpers; +using CarouselViewChallenge.Models; using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Windows.Input; +using Xamarin.Essentials; +using Xamarin.Forms; namespace CarouselViewChallenge.ViewModels { @@ -17,6 +21,8 @@ protected virtual void OnPropertyChanged(string propertyName) } } + public ICommand CopySystemNameCommand { get; } + public ObservableCollection LargeShipList { get; set; } public ObservableCollection SmallShipList { get; set; } @@ -95,6 +101,13 @@ public CarouselViewTwoViewModel() Cycle = "Cycle 150"; CCBalance = "853 CC"; LastUpdated = DateTime.Now; + CopySystemNameCommand = new Command(CopySystemNameAsync); + } + + private async void CopySystemNameAsync(string name) + { + await Clipboard.SetTextAsync(name).ConfigureAwait(false); + ToastHelper.Toast(String.Format("{0} copied", name)); } } } diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml index d8d53dd..85e225f 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml @@ -10,6 +10,7 @@ mc:Ignorable="d" x:Class="CarouselViewChallenge.Views.CarouselViewTwoPage" x:DataType="vm:CarouselViewTwoViewModel" + x:Name="CarouselViewTwo" BackgroundColor="{StaticResource pageBackground}"> @@ -29,6 +30,10 @@ + + +