diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj index 1e33499..49627f7 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/CarouselViewChallenge.Android.csproj @@ -32,6 +32,11 @@ prompt 4 None + false + false + false + false + d8 true @@ -53,11 +58,15 @@ + + + - - + + + @@ -101,4 +110,4 @@ - + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/GlobalSuppressions.cs b/CarouselViewChallenge/CarouselViewChallenge.Android/GlobalSuppressions.cs new file mode 100644 index 0000000..21bad7a --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/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("Style", "IDE0059:Unnecessary assignment of a value", Scope = "member", Target = "~M:CarouselViewChallenge.Droid.MainActivity.OnCreate(Android.OS.Bundle)")] + diff --git a/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs b/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs index fd4e22a..70e0123 100644 --- a/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs +++ b/CarouselViewChallenge/CarouselViewChallenge.Android/MainActivity.cs @@ -1,11 +1,10 @@ -using System; - +using Acr.UserDialogs; using Android.App; using Android.Content.PM; -using Android.Runtime; -using Android.Views; -using Android.Widget; using Android.OS; +using Android.Runtime; +using FFImageLoading.Forms.Platform; +using FFImageLoading.Svg.Forms; namespace CarouselViewChallenge.Droid { @@ -20,10 +19,16 @@ protected override void OnCreate(Bundle savedInstanceState) base.OnCreate(savedInstanceState); global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental"); + + CachedImageRenderer.Init(true); + var ignore = typeof(SvgCachedImage); + UserDialogs.Init(this); Xamarin.Essentials.Platform.Init(this, savedInstanceState); + global::Xamarin.Forms.Forms.Init(this, savedInstanceState); LoadApplication(new App()); } + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) { Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 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/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/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 a58ec88..08642af 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj +++ b/CarouselViewChallenge/CarouselViewChallenge/CarouselViewChallenge.csproj @@ -6,21 +6,98 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - WelcomePage.xaml - + + + + + + + + + + + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml 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/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/Converters/NewsTopicToImage.cs b/CarouselViewChallenge/CarouselViewChallenge/Converters/NewsTopicToImage.cs new file mode 100644 index 0000000..69da8d9 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Converters/NewsTopicToImage.cs @@ -0,0 +1,107 @@ +using System; +using System.Globalization; +using Xamarin.Forms; + +namespace CarouselViewChallenge.Converters +{ + internal class NewsTopicToImage : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return String.Empty; + } + string topic = ((string)value).Trim().ToLower(); + switch (topic) + { + case "unclassified": + return "resource://CarouselViewChallenge.Resources.galnet.orange.svg"; + case "aliens": + return "resource://CarouselViewChallenge.Resources.aliens-ufo1.svg"; + case "crime": + return "resource://CarouselViewChallenge.Resources.crime.svg"; + case "combat": + return "resource://CarouselViewChallenge.Resources.combat-fighter2.svg"; + case "culture": + return "resource://CarouselViewChallenge.Resources.theatre.svg"; + case "economy": + return "resource://CarouselViewChallenge.Resources.economy-chart.svg"; + case "exploration": + return "resource://CarouselViewChallenge.Resources.exploration-planet.svg"; + case "health": + return "resource://CarouselViewChallenge.Resources.health.svg"; + case "mining": + return "resource://CarouselViewChallenge.Resources.mining-pick.svg"; + case "mystery": + return "resource://CarouselViewChallenge.Resources.mystery.svg"; + case "politics": + return "resource://CarouselViewChallenge.Resources.politics.svg"; + case "religion": + return "resource://CarouselViewChallenge.Resources.religion-ankh.svg"; + case "science": + return "resource://CarouselViewChallenge.Resources.science-atom.svg"; + 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 "ad": + return "resource://CarouselViewChallenge.Resources.aisling.logo.svg"; + case "delaine": + return "resource://CarouselViewChallenge.Resources.delaine.logo.svg"; + case "ald": + return "resource://CarouselViewChallenge.Resources.ald.logo.svg"; + case "patreus": + return "resource://CarouselViewChallenge.Resources.patreus.logo.svg"; + case "mahon": + return "resource://CarouselViewChallenge.Resources.mahon.logo.svg"; + case "winters": + return "resource://CarouselViewChallenge.Resources.winters.logo.svg"; + case "lyr": + return "resource://CarouselViewChallenge.Resources.lyr.logo.svg"; + case "antal": + return "resource://CarouselViewChallenge.Resources.antal.logo.svg"; + case "grom": + return "resource://CarouselViewChallenge.Resources.grom.logo.svg"; + case "hudson": + return "resource://CarouselViewChallenge.Resources.hudson.logo.svg"; + case "torval": + return "resource://CarouselViewChallenge.Resources.torval.logo.svg"; + case "turner": + case "tarquin": + case "dekker": + case "vatermann": + case "martuuk": + case "dorn": + case "farseer": + case "tani": + case "ishmaak": + case "cheung": + case "ryder": + case "jameson": + case "qwent": + case "hicks": + case "brandon": + case "olmanova": + case "palin": + case "ram tah": + case "jean": + case "dweller": + case "sarge": + case "fortune": + case "blaster": + case "nemo": + return "resource://CarouselViewChallenge.Resources.engineer.orange.svg"; + default: + return "resource://CarouselViewChallenge.Resources.galnet.orange.svg"; + } + } + + 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/GlobalSuppressions.cs b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs new file mode 100644 index 0000000..35e42c2 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/GlobalSuppressions.cs @@ -0,0 +1,11 @@ +// 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")] +[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/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/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/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/Models/NewsItem.cs b/CarouselViewChallenge/CarouselViewChallenge/Models/NewsItem.cs new file mode 100644 index 0000000..4a1e6c5 --- /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(4)) + { + 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..c995277 --- /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,fine art,artwork,sculpture,painting,literature,masterpiece,artist,author,theatre,theatrical,festival,exhibition,culinary,cuisine,delicacy,delicacies,connoisseur,orchestra +Economy,economy,economic,trade,deliver,construct,investor,supplier,logistics,merger,conglomerate,commodities,consumer,business analyst,investment,wealth,fortune,ambrose foundation,industry,industrial,manufacture,engineering,bank of zaonce,megaship,resources,homeless,immigra,agricultur,crop yield,harvest +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,fertility +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,synthetic treatment,fertility,sustainability +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/Resources/aisling.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/aisling.logo.svg new file mode 100644 index 0000000..8096428 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/aisling.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/ald.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/ald.logo.svg new file mode 100644 index 0000000..df854a1 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/ald.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/aliens-ufo1.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/aliens-ufo1.svg new file mode 100644 index 0000000..9c2da90 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/aliens-ufo1.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/alliance.green.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/alliance.green.svg new file mode 100644 index 0000000..d18512c --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/alliance.green.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/antal.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/antal.logo.svg new file mode 100644 index 0000000..1163883 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/antal.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/combat-fighter2.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/combat-fighter2.svg new file mode 100644 index 0000000..ad10984 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/combat-fighter2.svg @@ -0,0 +1,25 @@ + + + + + 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/crime.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/crime.svg new file mode 100644 index 0000000..67805c2 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/crime.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/delaine.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/delaine.logo.svg new file mode 100644 index 0000000..266738f --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/delaine.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/economy-chart.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/economy-chart.svg new file mode 100644 index 0000000..f2fd9df --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/economy-chart.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/empire.blue.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/empire.blue.svg new file mode 100644 index 0000000..c21590b --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/empire.blue.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/engineer.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/engineer.orange.svg new file mode 100644 index 0000000..a4cc0d3 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/engineer.orange.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/exploration-planet.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/exploration-planet.svg new file mode 100644 index 0000000..da90670 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/exploration-planet.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/federation.red.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/federation.red.svg new file mode 100644 index 0000000..c030f18 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/federation.red.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/galnet.orange.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/galnet.orange.svg new file mode 100644 index 0000000..2c4ba33 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/galnet.orange.svg @@ -0,0 +1,26 @@ + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/grom.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/grom.logo.svg new file mode 100644 index 0000000..ab28148 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/grom.logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/health.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/health.svg new file mode 100644 index 0000000..4509b66 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/health.svg @@ -0,0 +1,18 @@ + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/hudson.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/hudson.logo.svg new file mode 100644 index 0000000..df84885 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/hudson.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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/lyr.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/lyr.logo.svg new file mode 100644 index 0000000..a5993e3 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/lyr.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/mahon.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/mahon.logo.svg new file mode 100644 index 0000000..66a947a --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/mahon.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/mining-pick.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/mining-pick.svg new file mode 100644 index 0000000..df7359b --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/mining-pick.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/mystery.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/mystery.svg new file mode 100644 index 0000000..74da07d --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/mystery.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + 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/patreus.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/patreus.logo.svg new file mode 100644 index 0000000..f03ceab --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/patreus.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/politics.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/politics.svg new file mode 100644 index 0000000..241e16f --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/politics.svg @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/religion-ankh.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/religion-ankh.svg new file mode 100644 index 0000000..f33a213 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/religion-ankh.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/science-atom.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/science-atom.svg new file mode 100644 index 0000000..31ba6b5 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/science-atom.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file 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/Resources/theatre.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/theatre.svg new file mode 100644 index 0000000..14f6b09 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/theatre.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/torval.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/torval.logo.svg new file mode 100644 index 0000000..da8dbe4 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/torval.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CarouselViewChallenge/CarouselViewChallenge/Resources/winters.logo.svg b/CarouselViewChallenge/CarouselViewChallenge/Resources/winters.logo.svg new file mode 100644 index 0000000..624730a --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Resources/winters.logo.svg @@ -0,0 +1,6 @@ + + + + + + \ 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 new file mode 100644 index 0000000..488c39c --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewChallengeViewModel.cs @@ -0,0 +1,96 @@ +using CarouselViewChallenge.Converters; +using CarouselViewChallenge.Helpers; +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.Linq; +using System.Threading.Tasks; +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)); + } + } + } + + private string _message; + public string Message + { + get { return _message; } + set + { + if (_message != value) + { + _message = value; + OnPropertyChanged(nameof(Message)); + } + } + } + + public CarouselViewChallengeViewModel() + { + Barrel.ApplicationId = "com.carouselchallenge.galnet"; + GalNetNewsList = new ObservableCollection(); + GetGalNetNewsAsync(); + } + + private async void GetGalNetNewsAsync(bool ignoreCache = false) + { + Message = "Loading..."; + try + { + // 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); + Message = null; + } + catch (Exception ex) + { + Message = String.Format("Error: {0}", ex.Message); + ToastHelper.Toast(Message); + } + } + } +} diff --git a/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs new file mode 100644 index 0000000..9c7c656 --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/ViewModels/CarouselViewTwoViewModel.cs @@ -0,0 +1,113 @@ +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 +{ + 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 ICommand CopySystemNameCommand { get; } + + 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() + { + // 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") + }; + 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"), + }; + 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/CarouselViewChallengePage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml index 1a7cc0d..93e4eb0 100644 --- a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewChallengePage.xaml @@ -3,11 +3,103 @@ 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" - 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 diff --git a/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml new file mode 100644 index 0000000..85e225f --- /dev/null +++ b/CarouselViewChallenge/CarouselViewChallenge/Views/CarouselViewTwoPage.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal + + + + + + + + + + + + + Horizontal + + + + + + + + + + + \ 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