diff --git a/.gitignore b/.gitignore index 51de314..3746275 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +Source/PocketX/Package.StoreAssociation.xml Source/PocketX/Handlers/Keys.cs \ No newline at end of file diff --git a/Source/CacheManager/CacheManager.cs b/Source/CacheManager/CacheManager.cs new file mode 100644 index 0000000..1f36524 --- /dev/null +++ b/Source/CacheManager/CacheManager.cs @@ -0,0 +1,22 @@ +using System.Reactive.Linq; +using System.Threading.Tasks; +using Akavache; +using static Akavache.BlobCache; + +namespace CacheManager +{ + public static class CacheManager + { + public static void Kill() + { + LocalMachine.InvalidateAll(); + LocalMachine.Vacuum(); + } + + public static async Task GetObject(string key, T defaultValue) + => await LocalMachine.GetObject(key).Catch(Observable.Return(defaultValue)); + + public static async Task InsertObject(string key, T value) + => await LocalMachine.InsertObject(key, value); + } +} \ No newline at end of file diff --git a/Source/CacheManager/CacheManager.csproj b/Source/CacheManager/CacheManager.csproj new file mode 100644 index 0000000..770ffba --- /dev/null +++ b/Source/CacheManager/CacheManager.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Source/CacheManager/ILru.cs b/Source/CacheManager/ILru.cs new file mode 100644 index 0000000..e3a36f3 --- /dev/null +++ b/Source/CacheManager/ILru.cs @@ -0,0 +1,10 @@ +namespace CacheManager +{ + internal interface ILru + { + V TryToGet(K key); + void Put(K key, V value); + void InsertAtHead(Node node); + void MoveToHead(Node node); + } +} \ No newline at end of file diff --git a/Source/CacheManager/Lru.cs b/Source/CacheManager/Lru.cs new file mode 100644 index 0000000..de55a4c --- /dev/null +++ b/Source/CacheManager/Lru.cs @@ -0,0 +1,22 @@ +using System.Collections; +using System.Threading.Tasks; + +namespace CacheManager +{ + public class Lru + { + private static LruCache _lruCache; + + public static bool IsOpen => _lruCache != null; + + public static void Init(int capacity, IDictionary oldDictionary) => _lruCache = new LruCache(capacity, oldDictionary); + + public static async Task SaveAllToCache(string key) + => await CacheManager.InsertObject(key, _lruCache.GetAll()); + + public static void Put(K key, V valueTuple) => _lruCache.Put(key, valueTuple); + + public static V Get(K key) => _lruCache.TryToGet(key); + + } +} \ No newline at end of file diff --git a/Source/CacheManager/LruCache.cs b/Source/CacheManager/LruCache.cs new file mode 100644 index 0000000..c06e98f --- /dev/null +++ b/Source/CacheManager/LruCache.cs @@ -0,0 +1,111 @@ +using System.Collections; +using System.Collections.Generic; + +namespace CacheManager +{ + internal class LruCache : ILru + { + private readonly Dictionary> _memory; + private readonly int _capacity; + private int _currentMemoryInUse; + private Node _head; + private Node _tail; + + public LruCache(int capacity, IDictionary oldMemory = null) + { + _capacity = capacity; + _memory = new Dictionary>(); + if (oldMemory != null) + foreach (var k in oldMemory.Keys) + Put((K)k, ((Node) oldMemory[k]).Value); + _currentMemoryInUse = (int)_memory?.Count; + } + + public IDictionary GetAll() => _memory; + + public V TryToGet(K key) + { + if (!_memory.ContainsKey(key)) return default; + var result = _memory[key]; + //Move node to head + MoveToHead(result); + return result.Value; + } + + public void Put(K key, V value) + { + Node node; + if (_memory.ContainsKey(key)) + { + //Parameter key exists in hash-map + node = _memory[key]; + node.Value = value; + MoveToHead(node); + return; + } + + node = new Node(key, value) + { + Key = key, + Value = value + }; + + //Parameter key is new and there is capacity + if (_currentMemoryInUse < _capacity) + { + if (_head == null) + _head = _tail = node; + else + InsertAtHead(node); + _memory[key] = node; + _currentMemoryInUse++; + } + else //Parameter key is new and there is no capacity. + { + var keyToRemove = _tail.Key; + + if (_head != _tail) + { + _tail.Previous.Next = null; + _tail = _tail.Previous; + } + _memory.Remove(keyToRemove); + _currentMemoryInUse--; + InsertAtHead(node); + _memory[key] = node; + _currentMemoryInUse++; + } + } + + public void InsertAtHead(Node node) + { + node.Previous = null; + node.Next = _head; + _head.Previous = node; + _head = node; + } + + public void MoveToHead(Node node) + { + if (node.Previous == null) return; + if (node.Next == null) + _tail = node.Previous; + else + node.Next.Previous = node.Previous; + node.Previous.Next = node.Next; + InsertAtHead(node); + } + + public string Log() + { + var headReference = _head; + var items = new List(); + while (headReference != null) + { + items.Add($"[{headReference.Key}: {headReference.Value}]"); + headReference = headReference.Next; + } + return string.Join(",", items); + } + } +} \ No newline at end of file diff --git a/Source/CacheManager/Node.cs b/Source/CacheManager/Node.cs new file mode 100644 index 0000000..cfbf340 --- /dev/null +++ b/Source/CacheManager/Node.cs @@ -0,0 +1,20 @@ +namespace CacheManager +{ + public class Node + { + public TV Value; + public TK Key; + [Newtonsoft.Json.JsonIgnore] + public Node Next; + [Newtonsoft.Json.JsonIgnore] + public Node Previous; + + public Node(TK key, TV value) + { + Key = key; + Value = value; + Next = null; + Previous = null; + } + } +} \ No newline at end of file diff --git a/Source/Logger/Logger.cs b/Source/Logger/Logger.cs index 5851ae2..85c9ed0 100644 --- a/Source/Logger/Logger.cs +++ b/Source/Logger/Logger.cs @@ -1,35 +1,34 @@ -using Microsoft.AppCenter; +using static System.Diagnostics.Debug; +using Microsoft.AppCenter; using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; -using static System.Console; - namespace Logger { public static class Logger { - private static bool DebugEnabled = false; - private static bool AppCenterEnabled = false; + private static bool _debugEnabled = false; + private static bool _appCenterEnabled = false; public static void InitOnlineLogger(string token) { AppCenter.Start(token, typeof(Analytics), typeof(Crashes)); AppCenter.LogLevel = LogLevel.Error; - AppCenterEnabled = true; + _appCenterEnabled = true; } - public static void SetDebugMode(bool debug) => DebugEnabled = debug; + public static void SetDebugMode(bool debug) => _debugEnabled = debug; public static void L(string message) { - var TAG = "[LOGGER] "; - if (DebugEnabled) WriteLine(TAG + message); - if (AppCenterEnabled) Analytics.TrackEvent(TAG + message); + const string TAG = "[LOGGER] "; + if (_debugEnabled) WriteLine(TAG + message); + if (_appCenterEnabled) Analytics.TrackEvent(TAG + message); } public static void E(System.Exception e) { - if (DebugEnabled) WriteLine("[LOGGER-ERR] " + e.Message); - if (AppCenterEnabled) Crashes.TrackError(e); + if (_debugEnabled) WriteLine("[LOGGER-ERR] " + e.Message); + if (_appCenterEnabled) Crashes.TrackError(e); } } } \ No newline at end of file diff --git a/Source/PocketX.sln b/Source/PocketX.sln index 902f3ae..e71daa5 100644 --- a/Source/PocketX.sln +++ b/Source/PocketX.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logger", "Logger\Logger.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PocketX.UnitTest", "PocketX.UnitTest\PocketX.UnitTest.csproj", "{30FDD3F9-AADC-478E-9FF4-0D1890605A09}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheManager", "CacheManager\CacheManager.csproj", "{00A36BBD-1B69-4449-8EC5-82D2DB0110AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,6 +93,26 @@ Global {30FDD3F9-AADC-478E-9FF4-0D1890605A09}.Release|x86.ActiveCfg = Release|x86 {30FDD3F9-AADC-478E-9FF4-0D1890605A09}.Release|x86.Build.0 = Release|x86 {30FDD3F9-AADC-478E-9FF4-0D1890605A09}.Release|x86.Deploy.0 = Release|x86 + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|ARM.ActiveCfg = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|ARM.Build.0 = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|ARM64.Build.0 = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|x64.Build.0 = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Debug|x86.Build.0 = Debug|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|Any CPU.Build.0 = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|ARM.ActiveCfg = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|ARM.Build.0 = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|ARM64.ActiveCfg = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|ARM64.Build.0 = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|x64.ActiveCfg = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|x64.Build.0 = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|x86.ActiveCfg = Release|Any CPU + {00A36BBD-1B69-4449-8EC5-82D2DB0110AD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/PocketX/App.xaml b/Source/PocketX/App.xaml index 277a3d6..4377eee 100644 --- a/Source/PocketX/App.xaml +++ b/Source/PocketX/App.xaml @@ -17,6 +17,8 @@ + /Assets/Fonts/GothamPro.ttf#Gotham Pro + #ED243B #fefefe #eee diff --git a/Source/PocketX/Assets/Fonts/GothamPro.ttf b/Source/PocketX/Assets/Fonts/GothamPro.ttf new file mode 100644 index 0000000..a7300b5 Binary files /dev/null and b/Source/PocketX/Assets/Fonts/GothamPro.ttf differ diff --git a/Source/PocketX/Assets/Icons/ChangeLog.md b/Source/PocketX/Assets/Icons/ChangeLog.md deleted file mode 100644 index 6989e86..0000000 --- a/Source/PocketX/Assets/Icons/ChangeLog.md +++ /dev/null @@ -1,9 +0,0 @@ -* **[VERSION]** -* Accept URL Share to Save -* Touch Swipe on Article Items to DELETE or ARCHIVE -* Add ChangeLog Section -* Fix loading Image from medium.com -* Richer Tutorial in Welcome Page -* Reload Button -* Minor UI Changes -* Improved Audio Player Controller \ No newline at end of file diff --git a/Source/PocketX/Assets/Icons/Thumbnail.png b/Source/PocketX/Assets/Icons/Thumbnail.png new file mode 100644 index 0000000..50b74e5 Binary files /dev/null and b/Source/PocketX/Assets/Icons/Thumbnail.png differ diff --git a/Source/PocketX/Assets/Icons/Home.md b/Source/PocketX/Assets/Markdown/Home.md similarity index 100% rename from Source/PocketX/Assets/Icons/Home.md rename to Source/PocketX/Assets/Markdown/Home.md diff --git a/Source/PocketX/Controls/MarkdownControl.xaml b/Source/PocketX/Controls/MarkdownControl.xaml deleted file mode 100644 index 2895d84..0000000 --- a/Source/PocketX/Controls/MarkdownControl.xaml +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Source/PocketX/Controls/TagsListControl.xaml b/Source/PocketX/Controls/TagsListControl.xaml deleted file mode 100644 index 0f5be2f..0000000 --- a/Source/PocketX/Controls/TagsListControl.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Source/PocketX/Controls/TagsListControl.xaml.cs b/Source/PocketX/Controls/TagsListControl.xaml.cs deleted file mode 100644 index 841e68a..0000000 --- a/Source/PocketX/Controls/TagsListControl.xaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using Windows.UI.Xaml.Controls; -using PocketX.Handlers; - -namespace PocketX.Controls -{ - public sealed partial class TagsListControl : UserControl - { - private IEnumerable Tags => PocketHandler.GetInstance().Tags; - public Func SearchAsync { get; set; } - public TagsListControl() => InitializeComponent(); - private async void ItemClick(object sender, ItemClickEventArgs e) => await SearchAsync("#" + e.ClickedItem); - } -} diff --git a/Source/PocketX/Converter/ArrayToStringConverter.cs b/Source/PocketX/Converter/ArrayToStringConverter.cs new file mode 100644 index 0000000..b4a8475 --- /dev/null +++ b/Source/PocketX/Converter/ArrayToStringConverter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PocketSharp.Models; + +namespace PocketX.Converter +{ + public class ArrayToStringConverter + { + public static string ConvertTagsToString(IEnumerable tags) + => tags == null ? "" : "#" + string.Join(" #", tags.Select(_ => _.Name).ToArray()); + } +} diff --git a/Source/PocketX/Converter/HideIfEmptyConverter.cs b/Source/PocketX/Converter/HideIfEmptyConverter.cs new file mode 100644 index 0000000..3183d1b --- /dev/null +++ b/Source/PocketX/Converter/HideIfEmptyConverter.cs @@ -0,0 +1,16 @@ +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace PocketX.Converter +{ + public class HideIfEmptyConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) => + value == null || value is string str && string.IsNullOrEmpty(str) || value is Array arr && arr.Length == 0 + ? Visibility.Collapsed + : Visibility.Visible; + + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); + } +} diff --git a/Source/PocketX/Handlers/AudioHandler.cs b/Source/PocketX/Handlers/AudioHandler.cs index 87b8460..ffa504f 100644 --- a/Source/PocketX/Handlers/AudioHandler.cs +++ b/Source/PocketX/Handlers/AudioHandler.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Linq; using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -11,7 +10,7 @@ internal class AudioHandler { private readonly MediaElement _media; - public AudioHandler(MediaElement media, Func> textProviderFunc) + public AudioHandler(MediaElement media, Func textProviderFunc) { _media = media; _media.MediaFailed += OnMediaFailed; @@ -37,7 +36,7 @@ public void OnMediaEnded(object sender, RoutedEventArgs e) public Action MediaEndAction { get; set; } public Action MediaStartAction { get; set; } - public Func> TextProvider { get; set; } + public Func TextProvider { get; set; } public async Task Start(string text) { @@ -57,7 +56,7 @@ public async Task Toggle() if (_media.CurrentState == MediaElementState.Playing) _media.Stop(); else { - var text = await TextProvider(); + var text = TextProvider(); if (!string.IsNullOrEmpty(text)) await Start(text); else await UiUtils.ShowDialogAsync("No Content to Read"); } diff --git a/Source/PocketX/Handlers/PocketHandler.cs b/Source/PocketX/Handlers/PocketHandler.cs index dc06942..6708542 100644 --- a/Source/PocketX/Handlers/PocketHandler.cs +++ b/Source/PocketX/Handlers/PocketHandler.cs @@ -1,5 +1,4 @@ -using Akavache; -using Microsoft.Toolkit.Uwp.Helpers; +using Microsoft.Toolkit.Uwp.Helpers; using PocketSharp; using PocketSharp.Models; using ReadSharp; @@ -8,20 +7,29 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; +using static Logger.Logger; +using Cache = CacheManager.CacheManager; +using Lru = CacheManager.Lru; namespace PocketX.Handlers { internal class PocketHandler : INotifyPropertyChanged { public PocketClient Client; - private static PocketHandler _pocketHandler; - private PocketItem _currentPocketItem; + public PocketUser User { get; set; } public ObservableCollection Tags { set; get; } = new ObservableCollection(); public event PropertyChangedEventHandler PropertyChanged; - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + private static PocketHandler _pocketHandler; + private PocketItem _currentPocketItem; + private Reader _reader; + private const string LruKey = "ArticlesContent"; + private const int LruCapacity = 20; + private readonly LocalObjectStorageHelper _localCache = new LocalObjectStorageHelper(); + protected virtual void OnPropertyChanged(string propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + public PocketItem CurrentPocketItem { get => _currentPocketItem; @@ -32,43 +40,39 @@ public PocketItem CurrentPocketItem } } - public PocketUser User { get; set; } - - #region Login\Logout - public static PocketHandler GetInstance() => _pocketHandler ?? (_pocketHandler = new PocketHandler()); - public async void LoadCacheClient() + #region Login-Logout + + public void LoadCacheClient() { - var cache = new LocalObjectStorageHelper().Read(Keys.PocketClientCache, ""); + var cache = _localCache.Read(Keys.PocketClientCache, ""); Client = cache == "" ? null : new PocketClient(Keys.Pocket, cache); - try { if (Client != null) User = await Client.GetUser(); } - catch{ } + User = _localCache.Read(Keys.PocketClientCache + "user"); } - private void SaveCacheUser(PocketUser user) - => new LocalObjectStorageHelper().Save(Keys.PocketClientCache, user.Code); - internal void Logout() { - Logger.Logger.L("Logout"); + L("Logout"); Client = null; + User = null; _pocketHandler = null; SettingsHandler.Clear(); - BlobCache.LocalMachine.InvalidateAll(); - BlobCache.LocalMachine.Vacuum(); - new LocalObjectStorageHelper().Save(Keys.PocketClientCache, ""); + Cache.Kill(); + _localCache.Save(Keys.PocketClientCache, ""); + _localCache.Save(Keys.PocketClientCache + "user", ""); } - internal async Task LoginAsync() + public async Task LoginAsync() { User = await Client.GetUser(); if (User == null) return false; - SaveCacheUser(User); + _localCache.Save(Keys.PocketClientCache, User.Code); + _localCache.Save(Keys.PocketClientCache + "user", User); return true; } - internal async Task LoginUriAsync() + public async Task LoginUriAsync() { Client = new PocketClient(Keys.Pocket, callbackUri: App.Protocol); await Client.GetRequestCode(); @@ -77,53 +81,11 @@ internal async Task LoginUriAsync() #endregion Login\Logout - internal async Task<(string, string)> AddFromShare(Uri url) - { - var SUCCESS = "Successfully Saved to Pocket"; - var FAILED = "FAILED (Be Sure You Are Logged In)"; - if (Client != null) - { - await Client.Add(url); - return (SUCCESS, url.AbsoluteUri); - } - try - { - _pocketHandler.LoadCacheClient(); - await _pocketHandler.Client.Add(url); - return (SUCCESS, url.AbsoluteUri); - } - catch (Exception e) { return (FAILED, e.Message); } - } - - internal async Task> GetListAsync( - State state, bool? favorite, string tag, string search, int count, int offset) - { - try - { - if (!Microsoft.Toolkit.Uwp.Connectivity.NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable) - throw new Exception(); - var pocketItems = await Client.Get( - state: state, favorite: favorite, - tag: tag, contentType: null, - sort: Sort.newest, search: search, - domain: null, since: null, - count: count, offset: offset); - - if (offset == 0) await SetItemsCache(pocketItems); - return pocketItems; - } - catch - { - if (offset == 0) return await GetItemsCache(); - else return null; - } - } - #region Cache Items internal async Task> GetItemsCache() { - var ls = await BlobCache.LocalMachine.GetObject>(Keys.MainList).Catch(Observable.Return(new List())); + var ls = await Cache.GetObject(Keys.MainList, new List()); var pls = new List(); foreach (var item in ls) { @@ -135,13 +97,18 @@ internal async Task> GetItemsCache() pi.Title = item[2]; pi.LeadImage.Uri = new Uri(item[3]); } - catch { } + catch (Exception e) + { + E(e); + } + pls.Add(pi); } + return pls; } - private async Task SetItemsCache(IEnumerable get) + private static async Task PutItemsInCache(IEnumerable get) { var ls = new List(); var lsget = get.ToList(); @@ -152,72 +119,172 @@ private async Task SetItemsCache(IEnumerable get) if (i == 60) break; } - await BlobCache.LocalMachine.InsertObject(Keys.MainList, ls); + await Cache.InsertObject(Keys.MainList, ls); } - internal async Task SetItemCache(int index, PocketItem item) + internal async Task PutItemInCache(int index, PocketItem item) { - var ls = await BlobCache.LocalMachine.GetObject>(Keys.MainList).Catch(Observable.Return(new List())); + var ls = await Cache.GetObject(Keys.MainList, new List()); var itemGen = new[] { item.ID, item.Uri.AbsoluteUri, item.Title }; ls.Insert(index, itemGen); - await BlobCache.LocalMachine.InsertObject(Keys.MainList, ls); + await Cache.InsertObject(Keys.MainList, ls); } #endregion Cache Items - internal async Task Read(Uri url, bool force) + #region Tags + + public async Task FetchOnlineTagsAsync() { - var cache = await BlobCache.LocalMachine.GetObject(url?.AbsoluteUri) - .Catch(Observable.Return("")); - if (!force && cache?.Trim()?.Length > 0) return cache; - var r = await new Reader().Read(url, new ReadOptions { PrettyPrint = true, PreferHTMLEncoding = false }); - var content = BFound.HtmlToMarkdown.MarkDownDocument.FromHtml(r?.Content); + var tags = (await Client.GetTags()).ToArray().Select(o => o.Name).ToArray(); + if (!tags.Any()) return; + Tags.Clear(); + foreach (var t in tags) Tags?.Add(t); + if (Tags?.Count > 0) + { + OnPropertyChanged(nameof(Tags)); + await Cache.InsertObject("tags", Tags); + } + } + + public async Task FetchOfflineTagsAsync() + { + foreach (var t in await Cache.GetObject>("tags", null)) + if (t != null) + Tags?.Add(t); + if (Tags?.Count > 0) OnPropertyChanged(nameof(Tags)); + } + + public async Task FetchTagsAsync() + { + if (Tags.Count > 0) return; + try + { + await FetchOfflineTagsAsync(); + } + catch (Exception e) + { + E(e); + } + + try + { + await FetchOnlineTagsAsync(); + } + catch (Exception e) + { + E(e); + } + } + + #endregion + + public async Task UserStatistics() => await Client.GetUserStatistics(); + + public async Task Read(string id, Uri url, CancellationTokenSource cancellationSource) + { + if (!Lru.IsOpen) + { + var old = await Cache.GetObject>>(LruKey, null); + Lru.Init(LruCapacity, old); + } + + var cacheContent = Lru.Get(id); + if (cacheContent?.Length > 0) return HtmlToMarkdown(cacheContent); + if (_reader == null) + { + var options = HttpOptions.CreateDefault(); + options.RequestTimeout = 60; + options.UseMobileUserAgent = true; + _reader = new Reader(options); + } + var readContent = await _reader.Read(url, + new ReadOptions { PrettyPrint = true, PreferHTMLEncoding = true, HasHeaderTags = false, UseDeepLinks = true }, + cancellationSource.Token); //Fix Medium Images - content = content.Replace(".medium.com/freeze/max/60/", ".medium.com/freeze/max/360/"); - if (!(r?.Content?.Length > 0)) return content; - await BlobCache.LocalMachine.InsertObject(url?.AbsoluteUri, content); - await BlobCache.LocalMachine.InsertObject("plain_" + url?.AbsoluteUri, r?.PlainContent); - return content; + var content = readContent?.Content.Replace(".medium.com/freeze/max/60/", ".medium.com/freeze/max/360/"); + if (readContent?.Content?.Length < 1) return content; + Lru.Put(id, content); + await Lru.SaveAllToCache(LruKey); + return HtmlToMarkdown(content); + } + + public async Task<(string, string)> AddFromShare(Uri url) + { + const string success = "Successfully Saved to Pocket"; + const string failed = "FAILED (Be Sure You Are Logged In)"; + if (Client != null) + { + await Client.Add(url); + return (success, url.AbsoluteUri); + } + + try + { + _pocketHandler.LoadCacheClient(); + await _pocketHandler.Client.Add(url); + return (success, url.AbsoluteUri); + } + catch (Exception e) + { + return (failed, e.Message); + } } - internal async Task FetchTagsAsync() + public async Task> GetListAsync( + State state, bool? favorite, string tag, string search, int count, int offset) { try { - if (Tags?.Count > 0) return; - var offlineTags = await BlobCache.LocalMachine.GetObject>("tags"); - if (offlineTags != null) - foreach (var t in offlineTags) - Tags?.Add(t); - var tags = (await Client.GetTags()).ToList().Select(o => o.Name); - var enumerable = tags as string[] ?? tags.ToArray(); - if (enumerable.Length < 1) return; - Tags.Clear(); - foreach (var t in enumerable) Tags?.Add(t); - await BlobCache.LocalMachine.InsertObject("tags", Tags); - } - catch { } - finally + if (!Utils.HasInternet) + throw new Exception(); + var pocketItems = await Client.Get( + state: state, favorite: favorite, + tag: tag, contentType: null, + sort: Sort.newest, search: search, + domain: null, since: null, + count: count, offset: offset); + + if (state == State.unread && tag == null && search == null && offset == 0) + await PutItemsInCache(pocketItems); + return pocketItems; + } + catch { - OnPropertyChanged(nameof(Tags)); + return state == State.unread && tag == null && search == null && offset == 0 + ? await GetItemsCache() + : null; } } - internal async Task Delete(PocketItem pocketItem) + public async Task DeleteArticle(PocketItem pocketItem) { try { await Client.Delete(pocketItem); - await BlobCache.LocalMachine.Invalidate(pocketItem.Uri.AbsoluteUri); - await BlobCache.LocalMachine.Invalidate("plain_" + pocketItem.Uri.AbsoluteUri); } catch (Exception e) { - Logger.Logger.E(e); + E(e); } } - public async Task TextProviderForAudioPlayer() - => await BlobCache.LocalMachine.GetObject("plain_" + CurrentPocketItem?.Uri?.AbsoluteUri).Catch(Observable.Return("")); + public async Task ArchiveArticle(PocketItem pocketItem) + { + try + { + await Client.Archive(pocketItem); + } + catch (Exception e) + { + E(e); + } + } + + public string TextProviderForAudioPlayer() => HtmlToRaw(Lru.Get(CurrentPocketItem?.ID)); + + private static string HtmlToRaw(string html) => HtmlUtilities.ConvertToPlainText(html); + + private static string HtmlToMarkdown(string html) => BFound.HtmlToMarkdown.MarkDownDocument.FromHtml(html); } } \ No newline at end of file diff --git a/Source/PocketX/Handlers/SettingsHandler.cs b/Source/PocketX/Handlers/SettingsHandler.cs index d7a2496..9e10bda 100644 --- a/Source/PocketX/Handlers/SettingsHandler.cs +++ b/Source/PocketX/Handlers/SettingsHandler.cs @@ -11,12 +11,12 @@ public static void Load() { try { - var temp = new LocalObjectStorageHelper().Read(Handlers.Keys.Settings); + var temp = new LocalObjectStorageHelper().Read(Keys.Settings); if (temp != null) Settings = temp; } catch { } } - public static void Save() => new LocalObjectStorageHelper().Save(Handlers.Keys.Settings, Settings); + public static void Save() => new LocalObjectStorageHelper().Save(Keys.Settings, Settings); public static void Clear() { diff --git a/Source/PocketX/Handlers/Utils.cs b/Source/PocketX/Handlers/Utils.cs index 2f7f55c..fdb463f 100644 --- a/Source/PocketX/Handlers/Utils.cs +++ b/Source/PocketX/Handlers/Utils.cs @@ -3,18 +3,17 @@ using System.Threading.Tasks; using Windows.Data.Xml.Dom; using Windows.Networking.BackgroundTransfer; -using Windows.Networking.Connectivity; using Windows.Storage; using Windows.Storage.Pickers; using Windows.UI.Notifications; namespace PocketX.Handlers { - class Utils + internal class Utils { internal static int UnixTimeStamp() => (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; - - internal static bool CheckConnection => NetworkInformation.GetInternetConnectionProfile() != null; + + internal static bool HasInternet => Microsoft.Toolkit.Uwp.Connectivity.NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable; internal static async Task DownloadFile(string url, string name, StorageFolder folder) { @@ -79,4 +78,5 @@ internal static async Task TextFromAssets(string path) return FileIO.ReadTextAsync(sFile).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); } } + } diff --git a/Source/PocketX/Package.StoreAssociation.xml b/Source/PocketX/Package.StoreAssociation.xml index c7d098b..e31eff7 100644 --- a/Source/PocketX/Package.StoreAssociation.xml +++ b/Source/PocketX/Package.StoreAssociation.xml @@ -366,27 +366,62 @@ - 3783mindprojects.BitconLottery 3783mindprojects.BlackReader 3783mindprojects.ColorLibrary 3783mindprojects.githublibrary 3783mindprojects.GithubX 3783mindprojects.InfixPrefixPostfix - 3783mindprojects.InstagramPro 3783mindprojects.iTehran 3783mindprojects.KharazmiUniversityBlogs - 3783mindprojects.Lyricer 3783mindprojects.Schema 3783mindprojects.SegaStore 3783MINDPROJECTS.SUBTITLER 3783mindprojects.TelegramStore - 3783mindprojects.TodoX - 3783mindprojects.wakaX - 3783mindprojects.Windos + 3783mindprojects.TwitterX + 3783mindprojects.38069ACD6F7CF 3783mindprojects.YDM + 3783mindprojects.YoutubeX 3783mindprojects.52602C4675F0A 3783mindprojects.53420548E01F3 3783mindprojects.6550439DC5047 - + + + + 0.0.0.0 + X86 + 1.4.6.0 + + + 0.0.0.0 + X64 + 1.4.6.0 + + + 0.0.0.0 + Arm + 1.4.6.0 + + + 0.0.0.0 + Neutral + 1.4.6.0 + + + 0.0.0.0 + Neutral + 1.4.6.0 + + + 0.0.0.0 + Neutral + 1.4.6.0 + + + 0.0.0.0 + Neutral + 1.4.6.0 + + + \ No newline at end of file diff --git a/Source/PocketX/Package.appxmanifest b/Source/PocketX/Package.appxmanifest index fac667d..a7a86d1 100644 --- a/Source/PocketX/Package.appxmanifest +++ b/Source/PocketX/Package.appxmanifest @@ -2,9 +2,10 @@ - + xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" + xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" + IgnorableNamespaces="uap mp uap5 uap3"> + PocketX @@ -38,7 +39,7 @@ - WebLink + Uri False True Always - x86|x64|arm - 88B0404ADE7DA5D3EEE3654671D060758C49C839 + arm + 8DE482F59938C692B4568B9CBFC7EAC5EC95C4A7 1 OnApplicationRun @@ -32,10 +32,12 @@ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP ;2008 full - x86 + x64 false prompt true + false + false bin\x86\Release\ @@ -102,14 +104,13 @@ App.xaml - + MarkdownControl.xaml - - TagsListControl.xaml - + + @@ -123,9 +124,6 @@ - - EditArticleTagsDialog.xaml - ImageDialog.xaml @@ -147,10 +145,7 @@ Designer - - PreserveNewest - - + PreserveNewest @@ -161,6 +156,7 @@ + @@ -207,6 +203,9 @@ + + PreserveNewest + @@ -215,17 +214,14 @@ + MSBuild:Compile Designer - - Designer - MSBuild:Compile - - + Designer MSBuild:Compile @@ -239,10 +235,6 @@ MSBuild:Compile PreserveNewest - - Designer - MSBuild:Compile - Designer MSBuild:Compile @@ -265,14 +257,11 @@ - - 6.5.1 - 0.0.5 - 1.11.4 + 1.11.7 6.2.8 @@ -296,10 +285,12 @@ 1.0.3 + - - - + + {00a36bbd-1b69-4449-8ec5-82d2db0110ad} + CacheManager + {743a9933-e226-479e-be20-190b02caeaaa} Logger diff --git a/Source/PocketX/Themes/DefaultTheme.xaml b/Source/PocketX/Themes/DefaultTheme.xaml index 76bc242..c03a1ca 100644 --- a/Source/PocketX/Themes/DefaultTheme.xaml +++ b/Source/PocketX/Themes/DefaultTheme.xaml @@ -1,9 +1,35 @@  - + - + #CC222222 #FF222222 @@ -16,13 +42,43 @@ - + #CCFFFFFF #FFFFFFFF #FFE6E6E6 #FFCCCCCC - + #FFFFFFFF diff --git a/Source/PocketX/ViewModels/MainContentViewModel.cs b/Source/PocketX/ViewModels/MainContentViewModel.cs index 7fc75ca..f8530e4 100644 --- a/Source/PocketX/ViewModels/MainContentViewModel.cs +++ b/Source/PocketX/ViewModels/MainContentViewModel.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; @@ -11,7 +13,6 @@ using PocketSharp.Models; using PocketX.Handlers; using PocketX.Models; -using PocketX.Views; using PocketX.Views.Dialog; namespace PocketX.ViewModels @@ -75,30 +76,30 @@ public async Task SearchCommand(string q) internal ICommand AddArticle => _addArticle ?? (_addArticle = new SimpleCommand(async param => { - if (Microsoft.Toolkit.Uwp.Connectivity.NetworkHelper.Instance.ConnectionInformation - .IsInternetAvailable) + if (!Utils.HasInternet) { - var dialog = new AddDialog(Settings.AppTheme); - await dialog?.ShowAsync(); - if (dialog.PocketItem == null) return; - ArticlesList.Insert(0, dialog.PocketItem); - await PocketHandler.SetItemCache(0, dialog.PocketItem); + await UiUtils.ShowDialogAsync("You need to connect to the internet first"); + return; } - else await UiUtils.ShowDialogAsync("You need to connect to the internet first"); + var dialog = new AddDialog(); + await dialog?.ShowAsync(); + if (dialog.PocketItem == null) return; + ArticlesList.Insert(0, dialog.PocketItem); + await PocketHandler.PutItemInCache(0, dialog.PocketItem); })); - internal async void PinBtnClicked() => await new UiUtils().PinAppWindow(520, 400); + //internal async void PinBtnClicked() => await new UiUtils().PinAppWindow(520, 400); internal void ShareArticle(DataTransferManager sender, DataRequestedEventArgs args) { var request = args.Request; request.Data.SetText(PocketHandler?.CurrentPocketItem?.Uri?.ToString() ?? ""); request.Data.Properties.Title = "Shared by PocketX"; } - public async Task ToggleArchiveArticleAsync(PocketItem pocketItem, bool IsArchive) + public async Task ToggleArchiveArticleAsync(PocketItem pocketItem, bool isArchive) { if (pocketItem == null) return; try { - if (IsArchive) // Want to add + if (isArchive) // Want to add { await PocketHandler.Client.Unarchive(pocketItem); NotificationHandler.InAppNotification("Added", 2000); @@ -107,7 +108,7 @@ public async Task ToggleArchiveArticleAsync(PocketItem pocketItem, bool IsArchiv } else // Want to Archive { - await PocketHandler.Client.Archive(pocketItem); + await PocketHandler.ArchiveArticle(pocketItem); if (ArchivesList.Count > 0 && ArchivesList[0] != pocketItem) ArchivesList.Insert(0, pocketItem); ArticlesList.Remove(pocketItem); NotificationHandler.InAppNotification("Archived", 2000); @@ -118,7 +119,7 @@ public async Task ToggleArchiveArticleAsync(PocketItem pocketItem, bool IsArchiv public async Task DeleteArticleAsync(PocketItem pocketItem) { if (pocketItem == null) return; - await PocketHandler.Delete(pocketItem); + await PocketHandler.DeleteArticle(pocketItem); CurrentList()?.Remove(pocketItem); NotificationHandler.InAppNotification("Deleted", 2000); } diff --git a/Source/PocketX/Views/Controls/MarkdownControl.xaml b/Source/PocketX/Views/Controls/MarkdownControl.xaml new file mode 100644 index 0000000..2667e6b --- /dev/null +++ b/Source/PocketX/Views/Controls/MarkdownControl.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/PocketX/Controls/MarkdownControl.xaml.cs b/Source/PocketX/Views/Controls/MarkdownControl.xaml.cs similarity index 78% rename from Source/PocketX/Controls/MarkdownControl.xaml.cs rename to Source/PocketX/Views/Controls/MarkdownControl.xaml.cs index 044c630..b17e55d 100644 --- a/Source/PocketX/Controls/MarkdownControl.xaml.cs +++ b/Source/PocketX/Views/Controls/MarkdownControl.xaml.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; @@ -8,40 +9,14 @@ using PocketSharp.Models; using PocketX.Handlers; using PocketX.Models; -using PocketX.Views; +using PocketX.Views.Dialog; -namespace PocketX.Controls +namespace PocketX.Views.Controls { public sealed partial class MarkdownControl : UserControl, INotifyPropertyChanged { - public MarkdownControl() - { - InitializeComponent(); - new MarkdownHandler(MarkdownCtrl); - AudioHandler = new AudioHandler(Media, PocketHandler.GetInstance().TextProviderForAudioPlayer) - { - MediaStartAction = () => { MarkdownAppBar.MaxWidth = 48; }, - MediaEndAction = () => { MarkdownAppBar.MaxWidth = 500; } - }; - MarkdownCtrl.Loaded += async (s, e) => - { - if (string.IsNullOrEmpty(MarkdownText)) - MarkdownText = await Utils.TextFromAssets(@"Assets\Icons\Home.md"); - }; - } - - #region PropertyChanged - public event PropertyChangedEventHandler PropertyChanged; - private void OnPropertyChanged(string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - #endregion - #region Parameters - private Settings Settings => SettingsHandler.Settings; - private AudioHandler AudioHandler { get; } - private ICommand _textToSpeech; - private string _markdownText; - private bool IsArchive { get; set; } - private bool IsInTextView { get; set; } = true; + public string MarkdownText { get => _markdownText; @@ -51,17 +26,19 @@ public string MarkdownText OnPropertyChanged(nameof(MarkdownText)); } } + public PocketItem Article { get => (GetValue(ArticleProperty) is PocketItem i) ? i : null; set { - if (value == null) return; + if (value == null || value == Article) return; SetValue(ArticleProperty, value); IsArchive = value?.IsArchive ?? false; IsInTextView = false; // AppBar_Click action based on IsInTextView AppBar_Click("view", null); Bindings.Update(); + WebView.NavigateToString(""); } } @@ -74,9 +51,43 @@ public PocketItem Article public Func ToggleArchiveArticleAsync { get; set; } public Func DeleteArticleAsync { get; set; } public Func ToggleFavoriteArticleAsync { get; set; } + private CancellationTokenSource _cancellationSource; + private Settings Settings => SettingsHandler.Settings; + private AudioHandler AudioHandler { get; } + private ICommand _textToSpeech; + private string _markdownText; + private bool IsArchive { get; set; } + private bool IsInTextView { get; set; } = true; + + #endregion + + public MarkdownControl() + { + InitializeComponent(); + new MarkdownHandler(MarkdownCtrl); + AudioHandler = new AudioHandler(Media, PocketHandler.GetInstance().TextProviderForAudioPlayer) + { + MediaStartAction = () => { MarkdownAppBar.MaxWidth = 48; }, + MediaEndAction = () => { MarkdownAppBar.MaxWidth = 500; } + }; + MarkdownCtrl.Loaded += async (s, e) => + { + if (string.IsNullOrEmpty(MarkdownText)) + MarkdownText = await Utils.TextFromAssets(@"Assets\Markdown\Home.md"); + }; + } + + #region PropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged(string propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + #endregion #region SplitView + public SplitView SplitView { get => (SplitView)GetValue(SplitViewProperty); @@ -90,15 +101,29 @@ public SplitView SplitView , new PropertyMetadata(0)); private void ToggleSplitView() => SplitView.IsPaneOpen = !SplitView.IsPaneOpen; + #endregion - internal ICommand TextToSpeech => _textToSpeech ?? (_textToSpeech = new SimpleCommand(async param => await AudioHandler.Toggle())); - private void Reload_ArticleView(object sender, RoutedEventArgs e) => OpenInArticleView(true); + internal ICommand TextToSpeech => _textToSpeech ?? + (_textToSpeech = + new SimpleCommand(async param => await AudioHandler.Toggle())); + + private async void Reload_ArticleView(object sender, RoutedEventArgs e) => await OpenInArticleView(); + private async void AppBar_Click(object sender, RoutedEventArgs e) { var tag = sender is Control c ? c?.Tag?.ToString()?.ToLower() : sender is string s ? s : ""; switch (tag) { + case "tag": + if (Utils.HasInternet) + { + await new AddDialog { PrimaryBtnText = "Save" }.ShowAsync(); + OnPropertyChanged(nameof(Article)); + } + else await UiUtils.ShowDialogAsync("You need to connect to the internet first"); + + break; case "favorite": await ToggleFavoriteArticleAsync(Article); Article.IsFavorite = !Article.IsFavorite; @@ -116,10 +141,13 @@ private async void AppBar_Click(object sender, RoutedEventArgs e) Utils.CopyToClipboard(Article?.Uri?.AbsoluteUri); NotificationHandler.InAppNotification("Copied", 2000); break; + case "error": + IsInTextView = true; + AppBar_Click("view", null); + break; case "view": if (IsInTextView) { - FindName(nameof(WebView)); WebView.Visibility = Visibility.Visible; MarkdownGrid.Visibility = Visibility.Collapsed; if (ErrorView != null) ErrorView.Visibility = Visibility.Collapsed; @@ -130,7 +158,7 @@ private async void AppBar_Click(object sender, RoutedEventArgs e) MarkdownGrid.Visibility = Visibility.Visible; if (ErrorView != null) ErrorView.Visibility = Visibility.Collapsed; if (WebView != null) WebView.Visibility = Visibility.Collapsed; - OpenInArticleView(true); + await OpenInArticleView(); } IsInTextView = !IsInTextView; @@ -144,26 +172,29 @@ private async void AppBar_Click(object sender, RoutedEventArgs e) break; } } - public async void OpenInArticleView(bool force = false) + + public async Task OpenInArticleView() { if (Article == null) return; try { + if (_cancellationSource != null && _cancellationSource.Token != CancellationToken.None) + _cancellationSource.Cancel(); MarkdownLoading.IsLoading = true; MarkdownAppBar.Visibility = Visibility.Collapsed; MarkdownText = ""; - MarkdownGrid.Visibility = Visibility.Visible; if (ErrorView != null) ErrorView.Visibility = Visibility.Collapsed; if (WebView != null) WebView.Visibility = Visibility.Collapsed; - - var content = await PocketHandler.GetInstance().Read(Article?.Uri, force); + _cancellationSource = new CancellationTokenSource(); + var content = await PocketHandler.GetInstance().Read(Article?.ID, Article?.Uri, _cancellationSource); MarkdownCtrl.UriPrefix = Article?.Uri?.AbsoluteUri; MarkdownText = content; MarkdownAppBar.Visibility = Visibility.Visible; } - catch + catch (Exception e) { + if (e is OperationCanceledException || e is TaskCanceledException) return; MarkdownGrid.Visibility = Visibility.Collapsed; FindName(nameof(ErrorView)); ErrorView.Visibility = Visibility.Visible; @@ -172,7 +203,8 @@ public async void OpenInArticleView(bool force = false) finally { MarkdownLoading.IsLoading = false; + _cancellationSource = null; } } } -} +} \ No newline at end of file diff --git a/Source/PocketX/Views/Dialog/AddDialog.xaml b/Source/PocketX/Views/Dialog/AddDialog.xaml index 931af93..2663b2c 100644 --- a/Source/PocketX/Views/Dialog/AddDialog.xaml +++ b/Source/PocketX/Views/Dialog/AddDialog.xaml @@ -2,51 +2,58 @@ x:Class="PocketX.Views.Dialog.AddDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:PocketX.Views" + xmlns:chipsControl="using:UWPChipsX" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:local="using:PocketX.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:chipsControl="using:UWPChipsX" - CornerRadius="8" - mc:Ignorable="d" + Loaded="ContentDialog_Loaded" + PrimaryButtonClick="ContentDialog_PrimaryButtonClick" + PrimaryButtonText="{x:Bind PrimaryBtnText}" RequestedTheme="Default" - Loaded="ContentDialog_Loaded" - Title="Add Link"> - + SecondaryButtonText="Cancel" + mc:Ignorable="d"> + + + + - - - + - + - + -