1
0
Fork 0

Add project files.

master
Csharpest118 2 years ago
parent 6d654985bb
commit 568d452c2e

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>PhilExampleCrawler.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>PhilExampleCrawler.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
</Project>

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Interfaces
{
public interface ICategory
{
int ID { get; }
string Name { get; }
}
}

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Interfaces
{
public interface IDeepCloneable<T>
{
T Clone();
}
}

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.Interfaces;
namespace PhilExampleCrawler.Common.Models
{
public class Category : ICategory
{
public string Name { get; }
public int ID { get; }
public List<SubCategory> SubCategories { get; } = new List<SubCategory>();
public Category(string name, int id)
{
Name = name;
ID = id;
}
}
public class SubCategory : ICategory
{
public string Name { get; }
public int ID { get; }
public SubCategory(string name, int id)
{
Name = name;
ID = id;
}
}
}

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Models
{
public struct CrawlSearchParams
{
public string KeyWords { get; set; }
public string LocationStr { get; set; }
public int CategoryID { get; set; }
public int Radius { get; set; }
public CrawlSearchParams(string keywords, string locationStr, int categoryID = -1, int radius = 0)
{
KeyWords = keywords ?? "";
LocationStr = locationStr ?? "";
CategoryID = categoryID;
Radius = radius;
}
}
}

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.Interfaces;
namespace PhilExampleCrawler.Common.Models
{
/// <summary>
/// TODO: turn into record (+ rename to CrawlRequest?)
/// </summary>
public class CrawlSession : IDeepCloneable<CrawlSession>
{
public int ID { get; set; }
public CrawlSearchParams SearchParams { get; set; }
public int MinPrice { get; set; }
public int MaxPrice { get; set; }
public bool IsPrivate { get; set; }
public bool IsCommercial { get; set; }
public CrawlSession(int id, CrawlSearchParams searchParams,
int minPrice = -1, int maxPrice = -1,
bool isPrivate = false,
bool isCommercial = false)
{
ID = id;
SearchParams = searchParams;
MinPrice = minPrice;
MaxPrice = maxPrice;
IsPrivate = isPrivate;
IsCommercial = isCommercial;
}
public CrawlSession Clone()
{
return new CrawlSession(ID, SearchParams, MinPrice, MaxPrice, IsPrivate, IsCommercial);
}
}
}

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Models
{
public class Insertion
{
public string Href { get; }
public int CrawlSessionID { get; }
public string Name { get; }
public int? PostCode { get; }
public string LocationStr { get; }
public decimal Price { get; }
public bool Is_VB { get; }
public DateTime? Date { get; }
public bool IsTopAd { get; }
public bool IsHighlight { get; }
public bool IsRequest { get; }
public Insertion(string href, int crawlSessionID, string name,
int? postCode, string locationStr,
decimal price, bool is_vb, DateTime? date,
bool isTopAd, bool isHighlight, bool isRequest)
{
Href = href;
CrawlSessionID = crawlSessionID;
Name = name;
PostCode = postCode;
LocationStr = locationStr;
Price = price;
Is_VB = is_vb;
Date = date;
IsTopAd = isTopAd;
IsHighlight = isHighlight;
IsRequest = isRequest;
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Models
{
public class User
{
public int ID { get; }
public string AuthCode { get; }
public string Phone { get; }
public bool Optin_Telegram { get; }
public bool? HasTelegram { get; set; }
public DateTime CreateDate { get; }
public List<CrawlSession> CrawlSessions { get; set; } = new List<CrawlSession>();
public User(int id, string authCode, string phone, bool optin_telegram, DateTime createDate)
{
ID = id;
AuthCode = authCode;
Phone = phone;
Optin_Telegram = optin_telegram;
CreateDate = createDate;
}
}
}

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.Models
{
public class UserSession
{
public int ID { get; set; }
public User User { get; set; }
public List<CrawlSession> RegisteredCrawlSessions { get; }
public DateTime ValidUntil { get; set; }
public UserSession(int id, User user)
{
ID = id;
User = user;
RegisteredCrawlSessions = new List<CrawlSession>();
}
public UserSession(int id, User user, List<CrawlSession> registeredCrawlSessions)
{
ID = id;
User = user;
RegisteredCrawlSessions = registeredCrawlSessions;
}
public UserSession(int id, User user, List<CrawlSession> registeredCrawlSessions, DateTime validUntil)
{
ID = id;
User = user;
RegisteredCrawlSessions = registeredCrawlSessions;
ValidUntil = validUntil;
}
}
}

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PhilExampleCrawler.Common.Models
{
public static class Validator
{
private const string ALLOWED = "0123456789";
/// <summary> Removes any character that is not contained in "0123456789" </summary>
public static string PhoneNumber_Normalize(this string phone)
{
if (string.IsNullOrEmpty(phone))
return "";
string p = "";
for (int i = phone.Length - 1; i >= 0; i--)
if (ALLOWED.Any(c => c == phone[i]))
p = phone[i] + p;
return p;
//=> phone.Trim().TrimStart('+').TrimStart('0')
}
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Common.TCP.Enums
{
public enum LoadType : int
{
//User - GETs
RegisterUserSessionLoad = 101,
RegisterUserSessionOKLoad = 102,
//Insertions
NewInsertionLoad = 201
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.TCP.Enums;
namespace PhilExampleCrawler.Common.TCP.Interface
{
public interface ISerializableLoad
{
LoadType Type { get; }
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.TCP.Enums;
using PhilExampleCrawler.Common.TCP.Interface;
namespace PhilExampleCrawler.Common.TCP.Packets
{
//TODO: struct or class? LoadData can be anything so i think class is better
public class BasePacket
{
public ISerializableLoad LoadData { get; set; }
public BasePacket() { }
public BasePacket(ISerializableLoad loadData)
{
LoadData = loadData;
}
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.TCP.Enums;
using PhilExampleCrawler.Common.TCP.Interface;
namespace PhilExampleCrawler.Common.TCP.Packets
{
public struct RegisterUserSessionLoad : ISerializableLoad
{
public LoadType Type => LoadType.RegisterUserSessionLoad;
public string AuthCode { get; set; }
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.TCP.Enums;
using PhilExampleCrawler.Common.TCP.Interface;
namespace PhilExampleCrawler.Common.TCP.Packets
{
public struct NewInsertionLoad : ISerializableLoad
{
public LoadType Type => LoadType.NewInsertionLoad;
public string Href { get; set; }
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.TCP.Enums;
using PhilExampleCrawler.Common.TCP.Interface;
namespace PhilExampleCrawler.Common.TCP.Packets
{
public struct RegisterUserSessionOKLoad : ISerializableLoad
{
public LoadType Type => LoadType.RegisterUserSessionOKLoad;
public string ForAuthCode { get; set; }
public DateTime ValidUntil { get; set; }
}
}

@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using PhilExampleCrawler.Common.Models;
using PhilExampleCrawler.Common.TCP.Enums;
using PhilExampleCrawler.Common.TCP.Interface;
using PhilExampleCrawler.Common.TCP.Packets;
namespace PhilExampleCrawler.Common.TCP.Utils
{
public static class PacketSerializer
{
public static byte[] Serialize(this BasePacket packet)
{
using (MemoryStream m = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(m))
{
writer.Write((int)packet.LoadData.Type);
switch (packet.LoadData.Type)
{
//User - GETs
case LoadType.RegisterUserSessionLoad:
WriteRegisterUserLoad(writer, (RegisterUserSessionLoad)packet.LoadData);
break;
case LoadType.RegisterUserSessionOKLoad:
WriteRegisterUserOKLoad(writer, (RegisterUserSessionOKLoad)packet.LoadData);
break;
//Insertion
case LoadType.NewInsertionLoad:
WriteNewInsertionLoad(writer, (NewInsertionLoad)packet.LoadData);
break;
default:
throw new FormatException("PacketSerializer/Serialize Exception: packet LoadType not found.");
}
}
return m.ToArray();
}
}
public static BasePacket Deserialize(this byte[] packet)
{
using (MemoryStream m = new MemoryStream(packet))
{
using (BinaryReader reader = new BinaryReader(m))
{
var bp = new BasePacket();
LoadType loadType = (LoadType)reader.ReadInt32();
switch (loadType)
{
//User
case LoadType.RegisterUserSessionLoad:
bp.LoadData = ReadRegisterUserLoad(reader);
break;
case LoadType.RegisterUserSessionOKLoad:
bp.LoadData = ReadRegisterUserOKLoad(reader);
break;
//Insertion
case LoadType.NewInsertionLoad:
bp.LoadData = ReadNewInsertionLoad(reader);
break;
default:
throw new FormatException("packet LoadType not found.");
}
return bp;
}
}
throw new InvalidOperationException("Deserialize could not properly create a stream.");
}
#region Write
private static void WriteRegisterUserLoad(BinaryWriter writer, RegisterUserSessionLoad loadData)
{
writer.Write(loadData.AuthCode);
}
private static void WriteRegisterUserOKLoad(BinaryWriter writer, RegisterUserSessionOKLoad loadData)
{
writer.Write(loadData.ForAuthCode);
writer.Write(ToDTString(loadData.ValidUntil));
}
private static void WriteNewInsertionLoad(BinaryWriter writer, NewInsertionLoad loadData)
{
writer.Write(loadData.Href);
}
/*
private static void WriteGetUserLoad(BinaryWriter writer, GetUserLoad loadData)
{
writer.Write(loadData.AuthCode);
}
private static void WriteReturnUserLoad(BinaryWriter writer, ReturnUserLoad loadData)
{
if (loadData.User != null)
{
writer.Write(loadData.User.ID);
writer.Write(loadData.User.AuthCode);
writer.Write(ToDTString(loadData.User.CreateDate));
}
}
private static void WriteRegisterCrawlSessionLoad(BinaryWriter writer, RegisterCrawlSessionLoad loadData)
{
writer.Write(loadData.UserID);
writer.Write(loadData.CrawlSearchParams.KeyWords);
writer.Write(loadData.CrawlSearchParams.LocationStr);
writer.Write(loadData.CrawlSearchParams.CategoryID);
writer.Write(loadData.CrawlSearchParams.Radius);
}
private static void WriteGetCrawlSessionsLoad(BinaryWriter writer, GetCrawlSessionsLoad loadData)
{
writer.Write(loadData.UserID);
}
*/
#endregion
#region Read
private static RegisterUserSessionLoad ReadRegisterUserLoad(BinaryReader reader)
{
return new RegisterUserSessionLoad()
{
AuthCode = reader.ReadString()
};
}
private static RegisterUserSessionOKLoad ReadRegisterUserOKLoad(BinaryReader reader)
{
return new RegisterUserSessionOKLoad()
{
ForAuthCode = reader.ReadString(),
ValidUntil = ToDT(reader.ReadString())
};
}
private static NewInsertionLoad ReadNewInsertionLoad(BinaryReader reader)
{
return new NewInsertionLoad()
{
Href = reader.ReadString()
};
}
/*
private static GetUserLoad ReadGetUserLoad(BinaryReader reader)
{
return new GetUserLoad()
{
AuthCode = reader.ReadString()
};
}
private static ReturnUserLoad ReadReturnUserLoad(BinaryReader reader)
{
return new ReturnUserLoad()
{
User = new User(reader.ReadInt32(),
reader.ReadString(),
ToDT(reader.ReadString()))
};
}
private static RegisterCrawlSessionLoad ReadRegisterCrawlSessionLoad(BinaryReader reader)
{
return new RegisterCrawlSessionLoad()
{
UserID = reader.ReadInt32(),
CrawlSearchParams = new CrawlSearchParams(keywords: reader.ReadString(),
locationStr: reader.ReadString(),
categoryID: reader.ReadInt32(),
radius: reader.ReadInt32())
};
}
private static GetCrawlSessionsLoad ReadGetCrawlSessionsLoad(BinaryReader reader)
{
return new GetCrawlSessionsLoad()
{
UserID = reader.ReadInt32()
};
}
*/
#endregion
private static string ToDTString(DateTime dt) => dt.ToString("ddMMyyyyHHmmss");
private static DateTime ToDT(string dt) => new DateTime(year: int.Parse(dt.Substring(4, 4)),
month: int.Parse(dt.Substring(2, 2)),
day: int.Parse(dt.Substring(0, 2)),
hour: int.Parse(dt.Substring(8, 2)),
minute: int.Parse(dt.Substring(10, 2)),
second: int.Parse(dt.Substring(12, 2)));
}
}

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
namespace PhilExampleCrawler.Core.Abstractions.Interfaces
{
public interface ICrawlingService_HAP
{
event EventHandler<Insertion> OnNewInsertionFound;
void StartCrawling(CrawlSearchParams crawlSearchParams, int interval_s);
void StopCrawling();
/// <summary>
/// Could often throw a WebException.Timeout, as the request timeout is only 2 sec long (15.09.2022)
/// </summary>
void Crawl(CrawlSearchParams searchParams, int timeout_ms);
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Core.Abstractions.Interfaces;
using PhilExampleCrawler.Core.Abstractions.Services;
namespace PhilExampleCrawler.Core
{
public static class Crawler
{
public static ICrawlingService_HAP CreateInstance()
{
return new CrawlingService_HAP();
}
}
}

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
using PhilExampleCrawler.Core.Abstractions.Interfaces;
namespace PhilExampleCrawler.Core.Abstractions.Services
{
internal class CrawlingService_HAP : ICrawlingService_HAP
{
public event EventHandler<Insertion> OnNewInsertionFound;
private readonly BaseCrawler_HAP _crawler = new();
public CrawlingService_HAP()
{
_crawler.OnNewInsertionFound += Crawler_OnNewInsertionFound;
}
private void Crawler_OnNewInsertionFound(object sender, Insertion e)
{
OnNewInsertionFound?.Invoke(sender, e);
}
public void StartCrawling(CrawlSearchParams crawlSearchParams, int interval_s)
{
_crawler.StartCrawling(crawlSearchParams, interval_s);
}
public void StopCrawling()
{
_crawler.StopCrawling();
}
public void Crawl(CrawlSearchParams searchParams, int timeout_ms)
{
_crawler.Crawl(searchParams, timeout_ms);
}
}
}

@ -0,0 +1,247 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using PhilExampleCrawler.Common.Models;
namespace PhilExampleCrawler.Core
{
public class BaseCrawler_Best
{
#region HTML Tags & Selectors
const string SELECTOR_INSERTION = "#srchrslt-adtable > .ad-listitem > article"; //reflects found insertions on the search result page
internal const string SELECTOR_TOPAD = ".aditem-main .aditem-main--top .aditem-main--top--right i.icon-feature-topad";
internal const string SELECTOR_HIGHLIGHT = ".aditem-main .aditem-main--top .aditem-main--top--right i.icon-feature-highlight";
internal const string SELECTOR_BOTTOM_TAG = ".aditem-main .aditem-main--bottom p .simpletag.tag-small";
internal const string SELECTOR_NAME = ".aditem-main .aditem-main--middle .text-module-begin";
internal const string SELECTOR_DATE = ".aditem-main .aditem-main--top .aditem-main--top--right";
internal const string SELECTOR_LOCATION = ".aditem-main .aditem-main--top .aditem-main--top--left";
internal const string SELECTOR_PRICE = ".aditem-main .aditem-main--middle .aditem-main--middle--price-shipping .aditem-main--middle--price-shipping--price";
internal const string ATTR_DATA_HREF = "data-href";
internal const string BOTTOM_TAG_GESUCH = "GESUCH";
#endregion
private readonly CrawlSession _sess;
private readonly HttpClient _httpClient;
private readonly List<string> _cachedHrefs = new();
private bool _firstCrawl = true;
private string _searchUrl;
public BaseCrawler_Best(CrawlSession crawlSession, HttpClient client)
{
_sess = crawlSession;
_searchUrl = AsUrl(crawlSession);
_httpClient = client;
}
public async Task<List<Insertion>> CrawlAsync()
{
try
{
var result = await _httpClient.GetAsync(_searchUrl);
var htmlString = await result.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(htmlString))
{
if (result.RequestMessage.RequestUri.AbsoluteUri != _searchUrl)
_searchUrl = result.RequestMessage.RequestUri.AbsoluteUri;
return null; //TODO: LOG ERROR
}
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlString);
var insNodes = GetInsertionNodes(htmlDoc);
return CompareToFoundNodes(insNodes);
}
catch (Exception ex)
{
Console.WriteLine("TODO: ERROR! " + ex);
return null;
}
}
/// <summary> Returns newly found insertions after the first call to this method </summary>
private List<Insertion> CompareToFoundNodes(IList<HtmlNode> insertionNodes)
{
List<(string href, HtmlNode srcNode)> insHrefNodes = GetInsertionHrefs(insertionNodes);
List<Insertion> insertions = new();
if (_firstCrawl)
{
_cachedHrefs.AddRange(insHrefNodes.Select(x => x.href));
_firstCrawl = false;
}
else
{
foreach ((string href, HtmlNode srcNode) in insHrefNodes)
{
if (!_cachedHrefs.Any(x => x == href))
{
_cachedHrefs.Add(href);
var i = GetInsertion(srcNode, _sess.ID);
if (Validate(i))
insertions.Add(i);
}
}
}
return insertions;
}
private bool Validate(Insertion i)
{
if (i.IsRequest || i.IsTopAd || i.IsHighlight)
return false;
if (i.Price < _sess.MinPrice)
return false;
if (i.Price > _sess.MaxPrice)
return false;
return true;
}
#region Node Methods
private static IList<HtmlNode> GetInsertionNodes(HtmlDocument doc)
{
IList<HtmlNode> insNodes = doc.QuerySelectorAll(SELECTOR_INSERTION);
return insNodes.Where(n => n != null
&& n.HasAttributes
&& n.Attributes.Any(a => a.Name == ATTR_DATA_HREF)).ToList();
}
private static List<(string href, HtmlNode srcNode)> GetInsertionHrefs(IList<HtmlNode> insertionNodes)
=> insertionNodes.Select(x => (x.Attributes["data-href"].Value, x)).ToList();
private static Insertion GetInsertion(HtmlNode insertionNode, int sessionID)
{
var (price, is_vb) = GetPriceVB(insertionNode, SELECTOR_PRICE);
var (pC, loc) = GetPostCodeLocation(insertionNode);
//TEST GetInsertionValue(SELECTOR_HIGHLIGHT) != null
var i = new Insertion(href: insertionNode.Attributes["data-href"].Value,
crawlSessionID: sessionID,
name: GetInnerText(insertionNode, SELECTOR_NAME),
postCode: pC,
locationStr: loc,
price: price,
is_vb: is_vb,
date: GetDate(insertionNode, SELECTOR_DATE),
isTopAd: insertionNode.QuerySelector(SELECTOR_TOPAD) != null,
isHighlight: insertionNode.QuerySelector(SELECTOR_HIGHLIGHT) != null,
isRequest: GetInnerText(insertionNode, SELECTOR_BOTTOM_TAG).ToUpperInvariant() == BOTTOM_TAG_GESUCH);
return i;
}
private static (decimal price, bool is_vb) GetPriceVB(HtmlNode insertionNode, string selector)
{
string priceVB = GetInnerText(insertionNode, selector);
bool is_vb = priceVB.Contains("VB");
string priceStr = ReduceToNumeric(priceVB);
if (string.IsNullOrEmpty(priceStr))
return (0, is_vb);
else if (decimal.TryParse(priceStr, NumberStyles.Number, new CultureInfo("de-DE"), out decimal d))
return (d, is_vb);
else
return (0, is_vb);
}
private static (int postCode, string loc) GetPostCodeLocation(HtmlNode insertionNode)
{
string pcLoc = GetInnerText(insertionNode, SELECTOR_LOCATION);
//replaces multiple subsequents whitespaces with a single whitespace
if (!string.IsNullOrEmpty(pcLoc))
{
pcLoc = System.Text.RegularExpressions.Regex.Replace(pcLoc, @"\s+", " ");
if (pcLoc.Length >= 5 && int.TryParse(pcLoc.Substring(0, 5), out int plz))
{
return (plz, pcLoc.Substring(5).Trim());
}
}
return (-1, null);
}
private static string GetInnerText(HtmlNode insertionNode, string selector)
=> (insertionNode.QuerySelector(selector)?.InnerText ?? "")
.Replace("\n", "").Trim();
private static DateTime? GetDate(HtmlNode insertionNode, string selector)
{
/*
Known formats:
- Heute, 09:02
- Gestern, 21:21
- 26.10.2022
*/
string dateText = GetInnerText(insertionNode, selector);
if (!string.IsNullOrEmpty(dateText))
{
int sepaIndex = dateText.IndexOf(", ");
string start = sepaIndex != -1 ? dateText.Substring(0, sepaIndex) : null;
string end = sepaIndex != -1 && sepaIndex < dateText.Length + 1 ? dateText.Substring(sepaIndex + 2) : dateText;
if (start == "Heute" && TimeSpan.TryParseExact(end, "hh\\:mm", CultureInfo.InvariantCulture, out TimeSpan time))
return DateTime.Today.AddMinutes(time.TotalMinutes);
else if (start == "Gestern" && TimeSpan.TryParseExact(end, "hh\\:mm", CultureInfo.InvariantCulture, out time))
return DateTime.Today.AddDays(-1).AddMinutes(time.TotalMinutes);
else if (DateTime.TryParseExact(end, "dd.MM.yyyy", null, DateTimeStyles.None, out DateTime date))
return date;
}
return null;
}
const string ALLOWED_NUMCHARS = "0123456789,.";
private static string ReduceToNumeric(string s)
{
if (string.IsNullOrEmpty(s))
return "";
string n = "";
for (int i = s.Length - 1; i >= 0; i--)
if (ALLOWED_NUMCHARS.Any(c => c == s[i]))
n = s[i] + n;
return n;
}
//private static List<Insertion> GetInsertions(IList<HtmlNode> insertionNodes)
// => insertionNodes.Select(x => GetInsertion(x)).ToList();
#endregion
private static string AsUrl(CrawlSession cs)
=> string.Format(Config.EXAMPLE_SEARCH_URL, HttpUtility.UrlEncode(cs.SearchParams.KeyWords),
HttpUtility.UrlEncode(cs.SearchParams.LocationStr),
cs.SearchParams.CategoryID,
cs.SearchParams.Radius,
cs.MinPrice <= 0 ? "" : cs.MinPrice,
cs.MaxPrice <= 0 ? "" : cs.MaxPrice,
GetPosterType(cs));
private static string GetPosterType(CrawlSession cs)
{
if (cs.IsPrivate && cs.IsCommercial)
return string.Empty;
else if (cs.IsPrivate)
return "PRIVATE";
else if (cs.IsCommercial)
return "COMMERCIAL";
return string.Empty;
}
}
}

@ -0,0 +1,157 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using PhilExampleCrawler.Common.Models;
namespace PhilExampleCrawler.Core
{
internal class BaseCrawler_HAP
{
#region HTML Tags & Selectors
const string SELECTOR_INSERTION = "#srchrslt-adtable > .ad-listitem > article"; //reflects found insertions on the search result page
internal const string SELECTOR_TOPAD = ".aditem-main .aditem-main--top .aditem-main--top--right i.icon-feature-topad";
internal const string SELECTOR_HIGHLIGHT = ".aditem-main .aditem-main--top .aditem-main--top--right i.icon-feature-highlight";
internal const string SELECTOR_BOTTOM_TAG = ".aditem-main .aditem-main--bottom p .simpletag.tag-small";
internal const string SELECTOR_LOCATION = ".aditem-main .aditem-main--top .aditem-main--top--left";
internal const string ATTR_DATA_HREF = "data-href";
internal const string BOTTOM_TAG_GESUCH = "GESUCH";
#endregion
readonly HtmlWeb _web = new();
List<string> _cachedHrefs = new();
bool _firstCrawl = true;
bool _crawling = false;
internal event EventHandler<Insertion> OnNewInsertionFound;
//TODO: crawl categories
//TODO: crawl locations
internal void StartCrawling(CrawlSearchParams searchParams, int intervalSec = 10)
{
}
internal void Crawl(CrawlSearchParams searchParams, int timeout_ms)
{
var searchUrl = AsUrl(searchParams);
var searchResultDoc = _web.Load(searchUrl, timeout_ms);
if (searchResultDoc == null)
return;
var insNodes = GetInsertionNodes(searchResultDoc);
CompareToFoundNodes(insNodes);
}
//TODO: Implement CancellationToken
internal void StopCrawling()
{
_crawling = false;
_firstCrawl = true;
_cachedHrefs = new List<string>();
}
private void CompareToFoundNodes(IList<HtmlNode> insertionNodes)
{
List<(string href, HtmlNode srcNode)> insHrefNodes = GetInsertionHrefs(insertionNodes);
if (_firstCrawl)
{
_cachedHrefs.AddRange(insHrefNodes.Select(x => x.href));
_firstCrawl = false;
}
else
{
foreach ((string href, HtmlNode srcNode) in insHrefNodes)
{
if (!_cachedHrefs.Any(x => x == href))
{
_cachedHrefs.Add(href);
OnNewInsertionFound?.Invoke(this, GetInsertion(srcNode));
}
}
}
}
#region Node Methods
private static IList<HtmlNode> GetInsertionNodes(HtmlDocument doc)
{
IList<HtmlNode> insNodes = doc.QuerySelectorAll(SELECTOR_INSERTION);
return insNodes.Where(n => n != null
&& n.HasAttributes
&& n.Attributes.Any(a => a.Name == ATTR_DATA_HREF)).ToList();
}
private static List<(string href, HtmlNode srcNode)> GetInsertionHrefs(IList<HtmlNode> insertionNodes)
=> insertionNodes.Select(x => (x.Attributes["data-href"].Value, x)).ToList();
private static Insertion GetInsertion(HtmlNode insertionNode)
{
//var i = new Insertion
//{
// Href = insertionNode.Attributes["data-href"].Value,
// IsTopAd = insertionNode.QuerySelector(SELECTOR_TOPAD) != null,
// IsHighlight = insertionNode.QuerySelector(SELECTOR_HIGHLIGHT) != null,
//};
//var plzLoc = GetInsertionLocation(insertionNode);
//Console.WriteLine("Plz: " + plzLoc.plz + " --- " + "Loc: " + plzLoc.loc);
//var reqNode = insertionNode.QuerySelector(SELECTOR_BOTTOM_TAG);
//if (reqNode != null)
// i.IsRequest = reqNode.InnerHtml?.ToUpperInvariant() == BOTTOM_TAG_GESUCH;
//return i;
return null;
}
private static (int plz, string loc) GetInsertionLocation(HtmlNode insertionNode)
{
var locNode = insertionNode.QuerySelector(SELECTOR_LOCATION);
string inner = locNode.InnerHtml;
if (locNode != null && !string.IsNullOrEmpty(inner))
{
if (inner.IndexOf(">") > -1)
inner = inner.Substring(inner.LastIndexOf(">") + 1).Trim();
if(inner.Length >= 5 && int.TryParse(inner.Substring(0, 5), out int plz))
{
return (plz, inner.Substring(5).Trim());
}
}
return (-1, null);
}
const string nums = "0123456789";
private static List<Insertion> GetInsertions(IList<HtmlNode> insertionNodes)
=> insertionNodes.Select(x => GetInsertion(x)).ToList();
#endregion
private string AsUrl(CrawlSearchParams searchParams)
{
return string.Format(Config.EXAMPLE_SEARCH_URL, HttpUtility.UrlEncode(searchParams.KeyWords),
HttpUtility.UrlEncode(searchParams.LocationStr),
searchParams.CategoryID,
searchParams.Radius);
}
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExampleCrawler.Core
{
internal static class Config
{
internal const string EXAMPLE_SEARCH_URL = "https://page-to-be-crawled.example/foo-bar?" +
"keywords={0}" +
"&locationStr={1}" +
"&categoryId={2}" +
"&radius={3}" +
"&maxPrice={4}" +
"&minPrice={5}" +
"&posterType={6}";
internal static readonly int[] SearchRadii = new[] { 0, 5, 10, 20, 30, 50, 100, 150, 200 };
}
}

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AssemblyName>PhilExampleCrawler.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>PhilExampleCrawler.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<PropertyGroup>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="HAPCSS_Fork">
<HintPath>..\..\HtmlAgilityPackCssSelector\HAPCSS_Fork\bin\Release\netstandard2.0\HAPCSS_Fork.dll</HintPath>
</Reference>
<Reference Include="HAP_Fork">
<HintPath>..\..\HtmlAgilityPack\HAP_Fork\bin\Release\netstandard2.0\HAP_Fork.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

@ -0,0 +1,89 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.Models;
namespace PhilExampleCrawler.Core
{
public static class ExamplePageValueCrawler
{
const string CATEGORIES_URL = "https://page-to-be-crawled.example/categories.html";
const string SELECTOR_CATS = "body > li";
const string SELECTOR_SUBCATS = "ul > li > a";
const string ATTR_DATA_HREF = "data-val";
public static async Task<IEnumerable<Category>> CrawlExamplePageCategories(HttpClient client)
{
try
{
var result = await client.GetAsync(CATEGORIES_URL);
var htmlString = await result.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(htmlString))
return null; //TODO: LOG ERROR
if (!htmlString.StartsWith("<body>"))
htmlString = "<body>" + htmlString + "</body>";
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlString);
IEnumerable<HtmlNode> catNodes = ReadCategoryNodes(htmlDoc);
Dictionary<HtmlNode, Category> cats = ReadCategoryValues(catNodes);
ReadAndAddSubcategories(cats);
return cats.Select(x => x.Value);
}
catch (Exception ex)
{
//TODO: LOG ERROR
}
return null;
}
private static IEnumerable<HtmlNode> ReadCategoryNodes(HtmlDocument htmlDoc)
{
IList<HtmlNode> categories = htmlDoc.QuerySelectorAll(SELECTOR_CATS);
return categories.Where(n => n != null);
}
private static Dictionary<HtmlNode, Category> ReadCategoryValues(IEnumerable<HtmlNode> catNodes)
{
Dictionary<HtmlNode, Category> cats = new();
foreach(HtmlNode catNode in catNodes)
{
HtmlNode catATag = catNode.QuerySelector("a");
if (catATag != null &&
catATag.HasAttributes &&
catATag.Attributes.Any(a => a.Name == ATTR_DATA_HREF) &&
int.TryParse(catATag.Attributes["data-val"].Value, out int catID))
cats.Add(catNode, new Category(catATag.InnerText, catID));
}
return cats;
}
private static void ReadAndAddSubcategories(Dictionary<HtmlNode, Category> cats)
{
foreach(var cat in cats)
{
var subCatATags = cat.Key.QuerySelectorAll(SELECTOR_SUBCATS);
foreach (var aTag in subCatATags)
if (TryGetSubcatID(aTag, out int subCatID))
cat.Value.SubCategories.Add(new SubCategory(aTag.InnerText, subCatID));
}
}
private static bool TryGetSubcatID(HtmlNode aTag, out int subCatID)
{
subCatID = -1;
return aTag != null &&
aTag.HasAttributes &&
aTag.Attributes.Any(a => a.Name == ATTR_DATA_HREF) &&
int.TryParse(aTag.Attributes["data-val"].Value, out subCatID);
}
}
}

@ -0,0 +1,360 @@
using Npgsql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.DataBase.Models;
using System.Data;
namespace PhilExampleCrawler.DataBase
{
public class CrawlSessionAccess
{
private readonly string _dbconn;
public CrawlSessionAccess(DBSettings dbSettings)
{
_dbconn = dbSettings.ToString();
}
public async Task<DB_CrawlSession?> AddCrawlSessionAsync(DB_CrawlSession newCS)
{
DB_CrawlSession? cs = null;
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_InsertCrawlSession(newCS);
if (await cmd.ExecuteScalarAsync() is int csID)
cs = new DB_CrawlSession(id: csID,
user_id: newCS.UserID,
keywords: newCS.Keywords,
location_text: newCS.LocationText,
category_id: newCS.CategoryID,
radius_km: newCS.RadiusKM,
minPrice: newCS.MinPrice,
maxPrice: newCS.MaxPrice,
isPrivate: newCS.IsPrivate,
isCommerial: newCS.IsCommercial);
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return cs;
}
public async Task<bool> UpdateCrawlSessionAsync(DB_CrawlSession cs)
{
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_UpdateCrawlSession(cs);
if (await cmd.ExecuteNonQueryAsync() > 0)
return true;
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return false;
}
public async Task<List<DB_CrawlSession>> GetCrawlSessionsAsync(int userID)
{
List<DB_CrawlSession> l = new();
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_SeletCrawlSession(userID);
using (var dr = await cmd.ExecuteReaderAsync())
while (await dr.ReadAsync())
l.Add(new DB_CrawlSession(id: dr.GetInt32("id"),
user_id: userID,
keywords: dr.GetString("keywords"),
location_text: dr.GetString("location_text"),
category_id: dr.GetInt32("category_id"),
radius_km: dr.GetInt32("radius_km"),
minPrice: dr.GetInt32("min_price"),
maxPrice: dr.GetInt32("max_price"),
isPrivate: dr.GetBoolean("is_private"),
isCommerial: dr.GetBoolean("is_commercial")));
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return l;
}
public async Task<List<DB_CrawlSession>> GetAllCrawlSessionsAsync()
{
List<DB_CrawlSession> l = new();
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_SeletAllCrawlSessions();
using (var dr = await cmd.ExecuteReaderAsync())
while (await dr.ReadAsync())
l.Add(new DB_CrawlSession(id: dr.GetInt32("id"),
user_id: dr.GetInt32("user_id"),
keywords: dr.GetString("keywords"),
location_text: dr.GetString("location_text"),
category_id: dr.GetInt32("category_id"),
radius_km: dr.GetInt32("radius_km"),
minPrice: dr.GetInt32("min_price"),
maxPrice: dr.GetInt32("max_price"),
isPrivate: dr.GetBoolean("is_private"),
isCommerial: dr.GetBoolean("is_commercial")));
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return l;
}
public async Task<bool> RemoveCrawlSessionAsync(int crawlSessionID)
{
bool deleted = false;
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_DeleteCrawlSession(crawlSessionID);
await cmd.ExecuteNonQueryAsync();
deleted = true;
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return deleted;
}
public async Task<List<DB_Insertion>> AddInsertionsAsync(IEnumerable<DB_Insertion> insertions)
{
List<DB_Insertion> l = new();
if (!insertions.Any())
return l;
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
foreach(var i in insertions)
{
cmd.CommandText = SQL_InsertInsertion(i.Href, i.CrawlSessionID, i.Name, i.PostCode, i.LocationStr, i.Price, i.Is_VB,
i.Date, i.IsTopAd, i.IsHighlight, i.IsRequest);
if (await cmd.ExecuteNonQueryAsync() > 0)
l.Add(new DB_Insertion(i));
}
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return l;
}
public async Task<List<DB_Insertion>> GetInsertions(int userID, int? limitPerCrawlSession = 5)
{
List<DB_Insertion> ins = new();
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_SeletAllCrawlSessionIDs(userID);
//crawl session ids
List<int> csIDs = new();
using (var dr = await cmd.ExecuteReaderAsync())
while (await dr.ReadAsync())
csIDs.Add(dr.GetInt32("id"));
//insertions
foreach (int csID in csIDs)
{
cmd.CommandText = SQL_SelectInsertions(csID, limitPerCrawlSession);
using var dr = await cmd.ExecuteReaderAsync();
while (await dr.ReadAsync())
ins.Add(new DB_Insertion(href: dr.GetString("href"),
crawlSessionID: csID,
name: dr.GetString("name"),
postCode: dr.ReadNullableInt("post_code"),
locationStr: dr.ReadNullableString("location_text"),
price: dr.GetDecimal("price"),
is_vb: dr.GetBoolean("is_vb"),
date: dr.ReadNullableDateTime("date"),
isTopAd: dr.GetBoolean("is_topad"),
isHighlight: dr.GetBoolean("is_highlight"),
isRequest: dr.GetBoolean("is_request")));
}
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return ins;
}
#region SQLQueries
private static string SQL_InsertCrawlSession (DB_CrawlSession cs)
=> $"INSERT INTO " +
"crawl_session" +
"(user_id, keywords, location_text, category_id, radius_km, min_price, max_price, is_private, is_commercial)" +
" VALUES " +
"(" +
$"{cs.UserID}," +
$"'{cs.Keywords}'," +
$"'{cs.LocationText}'," +
$"{cs.CategoryID}," +
$"{cs.RadiusKM}," +
$"{cs.MinPrice}," +
$"{cs.MaxPrice}," +
$"{cs.IsPrivate}," +
$"{cs.IsCommercial}" +
")" +
" RETURNING id;";
private static string SQL_UpdateCrawlSession(DB_CrawlSession cs)
=> "UPDATE " +
"crawl_session" +
" SET " +
$"keywords = '{cs.Keywords}'," +
$"location_text = '{cs.LocationText}'," +
$"category_id = {cs.CategoryID}," +
$"radius_km = {cs.RadiusKM}," +
$"min_price = {cs.MinPrice}," +
$"max_price = {cs.MaxPrice}," +
$"is_private = {cs.IsPrivate}," +
$"is_commercial = {cs.IsCommercial}" +
" WHERE " +
$"id = {cs.ID}" +
$" and user_id = {cs.UserID};";
private static string SQL_SeletCrawlSession(int userID) => $"SELECT " +
"id," +
"keywords," +
"location_text," +
"category_id," +
"radius_km," +
"min_price," +
"max_price," +
"is_private," +
"is_commercial" +
" FROM " +
"crawl_session" +
" WHERE " +
$"user_id = {userID};";
private static string SQL_SeletAllCrawlSessions() => $"SELECT " +
"id," +
"user_id," +
"keywords," +
"location_text," +
"category_id," +
"radius_km," +
"min_price," +
"max_price," +
"is_private," +
"is_commercial" +
" FROM " +
"crawl_session;";
private static string SQL_SeletAllCrawlSessionIDs(int userID)
=> $"SELECT " +
"id" +
" FROM " +
"crawl_session" +
" WHERE " +
$"user_id = {userID};";
private static string SQL_DeleteCrawlSession(int crawlSessionID)
=> $"DELETE FROM " +
"crawl_session" +
" WHERE " +
$"id = {crawlSessionID}";
private static string SQL_InsertInsertion(string href, int crawlSessionID, string name, int? postCode, string? locStr, decimal price, bool is_vb,
DateTime? date, bool isTopAd, bool isHighlight, bool isRequest)
=> $"INSERT INTO " +
"insertion" +
"(href, crawl_session_id, name, post_code, location_text, price, is_vb, date, " +
"is_topad, is_highlight, is_request)" +
" VALUES " +
"(" +
$"'{href}'," +
$"{crawlSessionID}," +
$"'{name}'," +
$"{Extensions.WriteNullableInt(postCode)}," +
$"{Extensions.WriteNullableString(locStr)}," +
$"{price}," +
$"{is_vb}," +
$"{Extensions.WriteNullableDateTime(date)}," +
$"{isTopAd}," +
$"{isHighlight}," +
$"{isRequest}" +
");";
private static string SQL_SelectInsertions(int crawlSessionID, int? limit)
=> $"SELECT " +
"href," +
"name," +
"post_code," +
"location_text," +
"price," +
"is_vb," +
"date," +
"is_topad," +
"is_highlight," +
"is_request" +
" FROM " +
"insertion" +
" WHERE " +
$"crawl_session_id = {crawlSessionID}" +
" and is_topad = false" +
" and is_highlight = false" + //TODO: these 3 booleans are untested
" and is_request = false" +
" ORDER BY id desc " +
$" LIMIT {limit?.ToString() ?? "null"}";
#endregion
}
}

@ -0,0 +1,182 @@
using Npgsql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.DataBase.Models;
using System.Data;
namespace PhilExampleCrawler.DataBase
{
//TODO: Could be made static actually
public class UserAccess
{
private readonly string _dbconn;
public UserAccess(DBSettings dbSettings)
{
_dbconn = dbSettings.ToString();
}
public async Task<DB_User?> AddUserAsync(string authCode, string phone, bool optin_telegram)
{
DB_User? u = null;
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_InsertUser(authCode, phone, optin_telegram);
if (await cmd.ExecuteScalarAsync() is int userID)
u = new DB_User(userID, authCode, phone, optin_telegram, null, DateTime.Now);
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return u;
}
public async Task<DB_User?> GetUserAsync(int userID)
{
DB_User? user = null;
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_SelectUser(userID);
using (var dr = await cmd.ExecuteReaderAsync())
while (await dr.ReadAsync())
user = ReadUser(dr);
if (user != null)
{
cmd.CommandText = SQL_SELECT_CrawlSessions(user.ID);
using var dr = await cmd.ExecuteReaderAsync();
while (await dr.ReadAsync())
user.CrawlSessions.Add(ReadCrawlSession(dr, user.ID));
}
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return user;
}
public async Task<List<DB_User>> GetUsersAsync()
{
List<DB_User> users = new();
try
{
using var conn = new NpgsqlConnection(_dbconn);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = SQL_SELECT_Users();
using (var dr = await cmd.ExecuteReaderAsync())
while (await dr.ReadAsync())
users.Add(ReadUser(dr));
foreach (var user in users) {
cmd.CommandText = SQL_SELECT_CrawlSessions(user.ID);
using var dr = await cmd.ExecuteReaderAsync();
while (await dr.ReadAsync())
user.CrawlSessions.Add(ReadCrawlSession(dr, user.ID));
}
conn.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
//TODO: log
}
return users;
}
private static DB_User ReadUser(NpgsqlDataReader dr)
=> new(id: dr.GetInt32("id"),
auth_code: dr.GetString("auth_code"),
phone: dr.GetString("phone"),
optin_telegram: dr.GetBoolean("optin_telegram"),
has_telegram: dr.ReadNullableBoolean("has_telegram"),
createDate: dr.GetDateTime("create_date"));
private static DB_CrawlSession ReadCrawlSession(NpgsqlDataReader dr, int userID)
=> new(id: dr.GetInt32("id"),
user_id: userID,
keywords: dr.GetString("keywords"),
location_text: dr.GetString("location_text"),
category_id: dr.GetInt32("category_id"),
radius_km: dr.GetInt32("radius_km"),
minPrice: dr.GetInt32("min_price"),
maxPrice: dr.GetInt32("max_price"),
isPrivate: dr.GetBoolean("is_private"),
isCommerial: dr.GetBoolean("is_commercial"));
#region SQLQueries
private static string SQL_InsertUser(string authCode, string phone, bool optin_telegram)
=> $"INSERT INTO " +
"phEx_user(auth_code" +
",phone" +
",optin_telegram)" +
" VALUES " +
"(" +
$"'{authCode}'" +
$",'{phone}'" +
$",{optin_telegram})" +
" RETURNING id;";
private static string SQL_SelectUser(int userID) => $"SELECT " +
"id," +
"auth_code," +
"phone," +
"optin_telegram," +
"has_telegram," +
"create_date" +
" FROM " +
"phEx_user" +
" WHERE " +
$"id = {userID};";
private static string SQL_SELECT_Users() => "SELECT " +
"id," +
"auth_code," +
"phone," +
"optin_telegram," +
"has_telegram," +
"create_date" +
" FROM " +
"phEx_user;";
private static string SQL_SELECT_CrawlSessions(int userID) => "SELECT " +
"id," +
"keywords," +
"location_text," +
"category_id," +
"radius_km," +
"min_price," +
"max_price," +
"is_private," +
"is_commercial" +
" FROM " +
"crawl_session" +
" WHERE " +
$"user_id = {userID};";
#endregion
}
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>PhilExampleCrawler.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>PhilExampleCrawler.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<Platforms>AnyCPU;x64</Platforms>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="7.0.0" />
</ItemGroup>
</Project>

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Text;
using System.Data;
namespace PhilExampleCrawler.DataBase
{
internal static class Extensions
{
internal static string WriteNullableString(object? value)
{
return value == null ? "null"
: "'" + value.ToString() + "'";
}
internal static string WriteNullableInt(int? value)
{
return !value.HasValue ? "null"
: value.Value.ToString();
}
internal static string WriteNullableDateTime(DateTime? value)
{
return !value.HasValue ? "null"
: "'" + value.Value.ToString("dd.MM.yyyy HH:mm:ss") + "'";
}
//TODO: or use a generic version?
//internal static T? ReadNullable<T>(this DbDataReader r, string name) where T : struct
//{
// var t = r.GetValue(name);
// if (t == DBNull.Value) return null;
// else return (T)t;
//}
internal static string? ReadNullableString(this DbDataReader r, string name)
{
var t = r.GetValue(name);
if (t == DBNull.Value) return null;
else return (string)t;
}
internal static bool? ReadNullableBoolean(this DbDataReader r, string name)
{
var t = r.GetValue(name);
if (t == DBNull.Value) return null;
else return (bool)t;
}
internal static int? ReadNullableInt(this DbDataReader r, string name)
{
var t = r.GetValue(name);
if (t == DBNull.Value) return null;
else return (int)t;
}
internal static DateTime? ReadNullableDateTime(this DbDataReader r, string name)
{
var t = r.GetValue(name);
if (t == DBNull.Value) return null;
else return (DateTime)t;
}
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PhilExampleCrawler.DataBase.Models
{
//TODO: move initialization to consuming projects config
public class DBSettings
{
internal string Server { get; } = "12.345.678.90";
internal int Port { get; } = 1339;
internal string User { get; } = "postgres";
internal string Password { get; } = "examplepw";
internal string Database { get; } = "exampledb";
public override string ToString()
{
return $"Server={Server};" +
$"Port={Port};" +
$"User Id={User};" +
$"Password={Password};" +
$"Database={Database};";
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PhilExampleCrawler.DataBase.Models
{
public class DB_CrawlSession
{
public int ID { get; }
public int UserID { get; }
public string Keywords { get; }
public string LocationText { get; }
public int CategoryID { get; }
public int RadiusKM { get; }
public int MinPrice { get; }
public int MaxPrice { get; }
public bool IsPrivate { get; }
public bool IsCommercial { get; }
public DB_CrawlSession(int id, int user_id, string keywords, string location_text, int category_id, int radius_km,
int minPrice, int maxPrice, bool isPrivate, bool isCommerial)
{
ID = id;
UserID = user_id;
Keywords = keywords;
LocationText = location_text;
CategoryID = category_id;
RadiusKM = radius_km;
MinPrice = minPrice;
MaxPrice = maxPrice;
IsPrivate = isPrivate;
IsCommercial = isCommerial;
}
}
}

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PhilExampleCrawler.DataBase.Models
{
public class DB_Insertion
{
public string Href { get; }
public int CrawlSessionID { get; }
public string Name { get; }
public int? PostCode { get; set; }
public string? LocationStr { get; set; }
public decimal Price { get; }
public bool Is_VB { get; }
public DateTime? Date { get; }
public bool IsTopAd { get; }
public bool IsHighlight { get; }
public bool IsRequest { get; }
public DB_Insertion(string href, int crawlSessionID, string name,
int? postCode, string? locationStr,
decimal price, bool is_vb, DateTime? date,
bool isTopAd, bool isHighlight, bool isRequest)
{
Href = href;
CrawlSessionID = crawlSessionID;
Name = name;
PostCode = postCode;
LocationStr = locationStr;
Price = price;
Is_VB = is_vb;
Date = date;
IsTopAd = isTopAd;
IsHighlight = isHighlight;
IsRequest = isRequest;
}
public DB_Insertion(DB_Insertion toCopy)
{
Href = toCopy.Href;
CrawlSessionID = toCopy.CrawlSessionID;
Name = toCopy.Name;
PostCode = toCopy.PostCode;
LocationStr = toCopy.LocationStr;
Price = toCopy.Price;
Is_VB = toCopy.Is_VB;
Date = toCopy.Date;
IsTopAd = toCopy.IsTopAd;
IsHighlight = toCopy.IsHighlight;
IsRequest = toCopy.IsRequest;
}
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PhilExampleCrawler.DataBase.Models
{
public class DB_User
{
public int ID { get; }
public string AuthCode { get; }
public string Phone { get; }
public bool Optin_Telegram { get; }
public bool? HasTelegram { get; }
public DateTime CreateDate { get; }
public List<DB_CrawlSession> CrawlSessions { get; } = new();
internal DB_User(int id, string auth_code, string phone, bool optin_telegram, bool? has_telegram, DateTime createDate)
{
ID = id;
AuthCode = auth_code;
Phone = phone;
Optin_Telegram = optin_telegram;
HasTelegram = has_telegram;
CreateDate = createDate;
}
}
}

@ -0,0 +1,195 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhilExampleMobile.Android", "PhilExampleMobile\PhilExampleMobile.Android\PhilExampleMobile.Android.csproj", "{D2C93228-5283-42C0-B6B1-D59AA029804F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhilExampleMobile.iOS", "PhilExampleMobile\PhilExampleMobile.iOS\PhilExampleMobile.iOS.csproj", "{15E6D01B-99F3-4880-83F0-13710A70E9AB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhilExampleMobile", "PhilExampleMobile\PhilExampleMobile\PhilExampleMobile.csproj", "{E62F97A8-7B35-440C-9E1F-38C073BFE253}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCPAPI", "TCPAPI\TCPAPI.csproj", "{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAPI", "WebAPI\WebAPI.csproj", "{62D0CAAE-8610-4895-A395-78FA292C6E94}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataBase", "DataBase\DataBase.csproj", "{14DB5510-2247-46D0-95A6-1FBC53464D35}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scheduler", "Scheduler\Scheduler.csproj", "{6E581074-3B32-43AB-82D6-AB38DDB38080}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|iPhone = Debug|iPhone
Debug|iPhoneSimulator = Debug|iPhoneSimulator
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|iPhone = Release|iPhone
Release|iPhoneSimulator = Release|iPhoneSimulator
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|iPhone.Build.0 = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|x64.ActiveCfg = Debug|x64
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Debug|x64.Build.0 = Debug|x64
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|Any CPU.Build.0 = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|iPhone.ActiveCfg = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|iPhone.Build.0 = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|x64.ActiveCfg = Release|x64
{17E8A117-CFEF-443A-8F76-C8C3EDC4FD45}.Release|x64.Build.0 = Release|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhone.Build.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhone.Deploy.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|x64.ActiveCfg = Debug|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|x64.Build.0 = Debug|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Debug|x64.Deploy.0 = Debug|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|Any CPU.Build.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|Any CPU.Deploy.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhone.ActiveCfg = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhone.Build.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhone.Deploy.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|x64.ActiveCfg = Release|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|x64.Build.0 = Release|x64
{D2C93228-5283-42C0-B6B1-D59AA029804F}.Release|x64.Deploy.0 = Release|x64
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|iPhone.ActiveCfg = Debug|iPhone
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|iPhone.Build.0 = Debug|iPhone
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|x64.ActiveCfg = Debug|x64
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Debug|x64.Build.0 = Debug|x64
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|Any CPU.Build.0 = Release|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|iPhone.ActiveCfg = Release|iPhone
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|iPhone.Build.0 = Release|iPhone
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|x64.ActiveCfg = Release|x64
{15E6D01B-99F3-4880-83F0-13710A70E9AB}.Release|x64.Build.0 = Release|x64
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|iPhone.Build.0 = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|x64.ActiveCfg = Debug|x64
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Debug|x64.Build.0 = Debug|x64
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|Any CPU.Build.0 = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|iPhone.ActiveCfg = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|iPhone.Build.0 = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|x64.ActiveCfg = Release|x64
{E62F97A8-7B35-440C-9E1F-38C073BFE253}.Release|x64.Build.0 = Release|x64
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|iPhone.Build.0 = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|x64.ActiveCfg = Debug|x64
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Debug|x64.Build.0 = Debug|x64
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|Any CPU.Build.0 = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|iPhone.ActiveCfg = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|iPhone.Build.0 = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|x64.ActiveCfg = Release|x64
{F6431FF7-6B87-4A96-89DE-46F7D4EB8ED1}.Release|x64.Build.0 = Release|x64
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|iPhone.Build.0 = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|x64.ActiveCfg = Debug|x64
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Debug|x64.Build.0 = Debug|x64
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|Any CPU.Build.0 = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|iPhone.ActiveCfg = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|iPhone.Build.0 = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|x64.ActiveCfg = Release|x64
{B1C50124-7E15-4BE8-91C0-F3D21F8FABB7}.Release|x64.Build.0 = Release|x64
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|iPhone.Build.0 = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|x64.ActiveCfg = Debug|x64
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Debug|x64.Build.0 = Debug|x64
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|Any CPU.Build.0 = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|iPhone.ActiveCfg = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|iPhone.Build.0 = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|x64.ActiveCfg = Release|x64
{62D0CAAE-8610-4895-A395-78FA292C6E94}.Release|x64.Build.0 = Release|x64
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|iPhone.Build.0 = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|x64.ActiveCfg = Debug|x64
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Debug|x64.Build.0 = Debug|x64
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|Any CPU.Build.0 = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|iPhone.ActiveCfg = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|iPhone.Build.0 = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|x64.ActiveCfg = Release|x64
{14DB5510-2247-46D0-95A6-1FBC53464D35}.Release|x64.Build.0 = Release|x64
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|iPhone.Build.0 = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|x64.ActiveCfg = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Debug|x64.Build.0 = Debug|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|Any CPU.Build.0 = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|iPhone.ActiveCfg = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|iPhone.Build.0 = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|x64.ActiveCfg = Release|Any CPU
{6E581074-3B32-43AB-82D6-AB38DDB38080}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {19271CA7-5BE1-4A87-9983-236A98794CCE}
EndGlobalSection
EndGlobal

@ -0,0 +1,19 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories) and given a Build Action of "AndroidAsset".
These files will be deployed with your package and will be accessible using Android's
AssetManager, like this:
public class ReadAsset : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
InputStream input = Assets.Open ("my_asset.txt");
}
}
Additionally, some Android functions will automatically load asset files:
Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");

@ -0,0 +1,38 @@
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PhilExample.Utils;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ResolutionGroupName(nameof(EffectConstants.EffectGroup))]
[assembly: ExportEffect(typeof(PhilExample.Droid.Effects.RemoveUnderlineEntryEffect),
nameof(PhilExample.Droid.Effects.RemoveUnderlineEntryEffect))]
namespace PhilExample.Droid.Effects
{
public class RemoveUnderlineEntryEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
Android.Graphics.Color entryLineColor = Android.Graphics.Color.Transparent;
Control.BackgroundTintList = ColorStateList.ValueOf(entryLineColor);
}
catch (Exception ex)
{
Console.WriteLine("Error OnAttached " + nameof(RemoveUnderlineEntryEffect), ex.Message);
}
}
protected override void OnDetached() { }
}
}

@ -0,0 +1,34 @@
using System;
using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
namespace PhilExample.Droid
{
[Activity(Label = "PhilExample", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize, ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
public static Activity _THIS { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
_THIS = this;
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
FFImageLoading.Forms.Platform.CachedImageRenderer.Init(true);
FFImageLoading.Forms.Platform.CachedImageRenderer.InitImageViewHandler();
LoadApplication(new App());
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D2C93228-5283-42C0-B6B1-D59AA029804F}</ProjectGuid>
<ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TemplateGuid>{c9e5eea5-ca05-42a1-839b-61506e0a37df}</TemplateGuid>
<OutputType>Library</OutputType>
<RootNamespace>PhilExampleMobile.Droid</RootNamespace>
<AssemblyName>PhilExampleMobile.Android</AssemblyName>
<Deterministic>True</Deterministic>
<AndroidApplication>True</AndroidApplication>
<AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile>
<AndroidResgenClass>Resource</AndroidResgenClass>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v12.0</TargetFrameworkVersion>
<AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent>
<AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidLinkMode>None</AndroidLinkMode>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Android" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Xml" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp">
<Version>2.88.2</Version>
</PackageReference>
<PackageReference Include="Xamarin.CommunityToolkit">
<Version>2.0.5</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Forms">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Svg">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Svg.Forms">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
</ItemGroup>
<ItemGroup>
<Compile Include="Effects\RemoveUnderlineEntryEffect.cs" />
<Compile Include="MainActivity.cs" />
<Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\ForegroundTCPService.cs" />
<Compile Include="Services\NotificationService.cs" />
<Compile Include="Services\ForegroundCrawlService.cs" />
<Compile Include="Utils\NotificationHelper.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\AboutResources.txt" />
<None Include="Assets\AboutAssets.txt" />
<None Include="Properties\AndroidManifest.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\values\styles.xml" />
<AndroidResource Include="Resources\values\colors.xml" />
<AndroidResource Include="Resources\mipmap-anydpi-v26\icon.xml" />
<AndroidResource Include="Resources\mipmap-anydpi-v26\icon_round.xml" />
<AndroidResource Include="Resources\mipmap-hdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-hdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-mdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-mdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xhdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xxhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xxhdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xxxhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xxxhdpi\launcher_foreground.png" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\drawable\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PhilExampleMobile\PhilExampleMobile.csproj">
<Project>{2B20AE11-F221-4436-A260-E66B581B9D3F}</Project>
<Name>PhilExampleMobile</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.PhilExample" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application android:label="PhilExample"
android:theme="@style/MainTheme"
android:usesCleartextTraffic="true">
</application>
</manifest>

@ -0,0 +1,30 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Android.App;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PhilExample.Android")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("PhilExample.Android")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
// Add some common permissions, these can be removed if not needed
[assembly: UsesPermission(Android.Manifest.Permission.Internet)]
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]

@ -0,0 +1,50 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.xml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable-hdpi/
icon.png
drawable-ldpi/
icon.png
drawable-mdpi/
icon.png
layout/
main.xml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called
"Resource" that contains the tokens for each one of the resources included. For example,
for the above Resources layout, this is what the Resource class would expose:
public class Resource {
public class drawable {
public const int icon = 0x123;
}
public class layout {
public const int main = 0x456;
}
public class strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main
to reference the layout/main.xml file, or Resource.strings.first_string to reference the first
string in the dictionary file values/strings.xml.

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/launcher_background" />
<foreground android:drawable="@mipmap/launcher_foreground" />
</adaptive-icon>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/launcher_background" />
<foreground android:drawable="@mipmap/launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="launcher_background">#F3F3F3</color>
<color name="colorPrimary">#160BE9</color>
<color name="colorPrimaryDark">#160BE9</color>
<color name="colorAccent">#160BE9</color>
</resources>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MainTheme" parent="MainTheme.Base">
<!-- As of Xamarin.Forms 4.6 the theme has moved into the Forms binary -->
<!-- If you want to override anything you can do that here. -->
<!-- Underneath are a couple of entries to get you started. -->
<!-- Set theme colors from https://aka.ms/material-colors -->
<!-- colorPrimary is used for the default action bar background -->
<item name="colorPrimary">#F3F3F3</item>
<!-- colorPrimaryDark is used for the status bar -->
<item name="colorPrimaryDark">#F3F3F3</item>
<!-- colorAccent is used as the default value for colorControlActivated
which is used to tint widgets -->
<item name="colorAccent">#160BE9</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:textAllCaps">false</item>
</style>
</resources>

@ -0,0 +1,118 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PhilExample.Droid.Services;
using PhilExample.Droid.Utils;
using PhilExample.Services;
[assembly: Xamarin.Forms.Dependency(typeof(ForegroundCrawlService))]
namespace PhilExample.Droid.Services
{
[Service]
public class ForegroundCrawlService : Service, IForegroundCrawlService
{
private static IForegroundCrawlDelegate _crawlDelegate;
private static readonly int _notiID = 1;
private static bool _running = false;
const string NOTIFICATION_CHANNEL_ID = "1";
const string NOTIFICATION_CHANNEL_NAME = "PhilExample_noti_1";
const string NOTIFICATION_CHANNEL_DESCR = "PhilExample_foreground";
private static System.Threading.Timer _callClock;
#region IForegroundCrawlService
public void Start(IForegroundCrawlDelegate del)
{
_crawlDelegate = del;
if (_running)
UpdateNotification();
else
{
StartService();
_running = true;
}
}
public void UpdateNotification()
{
NotificationHelper.GetAndroidNotificationManager().Notify(_notiID, CreateNotification());
}
public void Stop()
{
if(_callClock != null)
{
_callClock.Dispose();
_callClock = null;
}
//StopForeground(StopForegroundFlags.Remove);
var fgIntent = new Intent(MainActivity._THIS, typeof(ForegroundCrawlService));
MainActivity._THIS.StopService(fgIntent);
_running = false;
}
#endregion
private void StartService()
{
var intent = new Intent(MainActivity._THIS, typeof(ForegroundCrawlService));
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
MainActivity._THIS.StartForegroundService(intent);
else
MainActivity._THIS.StartService(intent);
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
if (_crawlDelegate == null)
throw new NotImplementedException("Android ForegroundCrawlService.StartCommandResult");
Console.WriteLine("Starting foregorund crawl service with _notiID: " + _notiID);
NotificationHelper.CreateNotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NOTIFICATION_CHANNEL_DESCR);
//_foregroundNotification = CreateNotification();
StartForeground(_notiID, CreateNotification());
var startTimeSpan = TimeSpan.Zero;
var periodTimeSpan = TimeSpan.FromSeconds(1);
_callClock = new System.Threading.Timer((e) =>
{
Console.WriteLine("[" + DateTime.Now.TimeOfDay.ToString().Substring(0, 8) + "] (" + _notiID + ") foreground service timer call started");
_crawlDelegate.Crawl();
Console.WriteLine("[" + DateTime.Now.TimeOfDay.ToString().Substring(0, 8) + "] (" + _notiID + ") foreground service timer call finished");
}, null, startTimeSpan, periodTimeSpan);
return StartCommandResult.Sticky;
}
private Notification CreateNotification()
{
string msgTitle = _crawlDelegate.GetForegroundNotificationTitle;
string msgDescr = _crawlDelegate.GetForegroundNotificationDescription;
Intent notificationIntent = new Intent(MainActivity._THIS, typeof(MainActivity));
var onClickIntent = PendingIntent.GetActivity(MainActivity._THIS, 0, notificationIntent, PendingIntentFlags.UpdateCurrent);
var notification = new Notification.Builder(MainActivity._THIS, NOTIFICATION_CHANNEL_ID)
.SetContentIntent(onClickIntent)
.SetContentTitle(msgTitle)
.SetContentText(msgDescr)
.SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
.SetOngoing(true)
.Build();
return notification;
}
public override IBinder OnBind(Intent intent) => null;
}
}

@ -0,0 +1,126 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExample.Droid.Services;
using PhilExample.Droid.Utils;
using PhilExample.Interfaces;
using PhilExample.Services;
[assembly: Xamarin.Forms.Dependency(typeof(ForegroundTCPService))]
namespace PhilExample.Droid.Services
{
[Service]
public class ForegroundTCPService : Service, IForegroundTCPService
{
private static IForegroundTCPDelegate _tcpDelegate;
private static readonly int _notiID = 1;
private static bool _running = false;
const string NOTIFICATION_CHANNEL_ID = "1";
const string NOTIFICATION_CHANNEL_NAME = "PhilExample_noti_1";
const string NOTIFICATION_CHANNEL_DESCR = "PhilExample_foreground";
public bool IsRunning => _running;
#region IForegroundTCPService
public void Start(IForegroundTCPDelegate del)
{
_tcpDelegate = del;
if (_running)
UpdateNotification();
else
{
StartService();
_running = true;
}
}
public void UpdateNotification()
{
NotificationHelper.GetAndroidNotificationManager().Notify(_notiID, CreateNotification());
}
public void Stop()
{
//StopForeground(StopForegroundFlags.Remove);
var fgIntent = new Intent(MainActivity._THIS, typeof(ForegroundTCPService));
MainActivity._THIS.StopService(fgIntent);
_running = false;
}
#endregion
private void StartService()
{
var intent = new Intent(MainActivity._THIS, typeof(ForegroundTCPService));
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
MainActivity._THIS.StartForegroundService(intent);
else
MainActivity._THIS.StartService(intent);
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
if (_tcpDelegate == null) //TODO
throw new NotImplementedException("Android ForegroundTCPService.StartCommandResult");
Console.WriteLine("Starting foregorund tcp service with _notiID: " + _notiID);
NotificationHelper.CreateNotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NOTIFICATION_CHANNEL_DESCR);
StartForeground(_notiID, CreateNotification());
Task.Run(() =>
{
Console.WriteLine("[" + DateTime.Now.TimeOfDay.ToString().Substring(0, 8) + "] (" + _notiID + ") tcp foreground service started");
_tcpDelegate.ReceiveLoop();
_tcpDelegate.OnForegroundStop();
Console.WriteLine("[" + DateTime.Now.TimeOfDay.ToString().Substring(0, 8) + "] (" + _notiID + ") tcp foreground service finished");
});
return StartCommandResult.Sticky;
}
public override void OnDestroy()
{
_running = false;
Console.WriteLine("tcp foreground service OnDestroy was called.");
base.OnDestroy();
}
public override void OnTaskRemoved(Intent rootIntent)
{
_running = false;
Console.WriteLine("tcp foreground service OnTaskRemoved was called.");
base.OnTaskRemoved(rootIntent);
}
private Notification CreateNotification()
{
string msgTitle = _tcpDelegate.GetForegroundNotificationTitle;
string msgDescr = _tcpDelegate.GetForegroundNotificationDescription;
Intent notificationIntent = new Intent(MainActivity._THIS, typeof(MainActivity));
var onClickIntent = PendingIntent.GetActivity(MainActivity._THIS, 0, notificationIntent, PendingIntentFlags.UpdateCurrent);
var notification = new Notification.Builder(MainActivity._THIS, NOTIFICATION_CHANNEL_ID)
.SetContentIntent(onClickIntent)
.SetContentTitle(msgTitle)
.SetContentText(msgDescr)
.SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
.SetOngoing(true)
.Build();
return notification;
}
public override IBinder OnBind(Intent intent) => null;
}
}

@ -0,0 +1,46 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PhilExample.Droid.Services;
using PhilExample.Droid.Utils;
using PhilExample.Services;
[assembly: Xamarin.Forms.Dependency(typeof(InsertionNotificationService))]
namespace PhilExample.Droid.Services
{
public class InsertionNotificationService : IInsertionNotificationService
{
const string NOTIFICATION_CHANNEL_ID = "1000";
const string NOTIFICATION_CHANNEL_NAME = "PhilExample_noti_2";
const string NOTIFICATION_CHANNEL_DESCR = "PhilExample_insertion_noti";
static int notiID = 2000;
static int NotiID { get => notiID > 2020
? (notiID = 2000) //TODO: Test if this assignment does what its supposed to (limit notifications to 20 at max, update old notis)
: notiID++; }
public void PublishNotification(string title, string descr, int insertionID)
{
NotificationHelper.CreateNotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NOTIFICATION_CHANNEL_DESCR);
var notification = new Notification.Builder(MainActivity._THIS, NOTIFICATION_CHANNEL_ID)
//.SetContentIntent(onClickIntent)
.SetContentTitle(title)
.SetContentText(descr)
.SetSmallIcon(Resource.Drawable.abc_btn_default_mtrl_shape)
.Build();
NotificationHelper.GetAndroidNotificationManager().Notify(NotiID, notification);
}
}
}

@ -0,0 +1,36 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PhilExample.Droid.Utils
{
internal static class NotificationHelper
{
internal static NotificationManager GetAndroidNotificationManager() => (NotificationManager)MainActivity._THIS.GetSystemService(Context.NotificationService);
internal static void CreateNotificationChannel(string id, string name, string descr)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
// Notification channels are new in API 26 (and not a part of the
// support library). There is no need to create a notification
// channel on older versions of Android.
return;
}
var channel = new NotificationChannel(id, name, NotificationImportance.High)
{
Description = descr
};
GetAndroidNotificationManager().CreateNotificationChannel(channel);
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Foundation;
using UIKit;
namespace PhilExample.iOS
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
return base.FinishedLaunching(app, options);
}
}
}

@ -0,0 +1,117 @@
{
"images": [
{
"scale": "2x",
"size": "20x20",
"idiom": "iphone",
"filename": "Icon40.png"
},
{
"scale": "3x",
"size": "20x20",
"idiom": "iphone",
"filename": "Icon60.png"
},
{
"scale": "2x",
"size": "29x29",
"idiom": "iphone",
"filename": "Icon58.png"
},
{
"scale": "3x",
"size": "29x29",
"idiom": "iphone",
"filename": "Icon87.png"
},
{
"scale": "2x",
"size": "40x40",
"idiom": "iphone",
"filename": "Icon80.png"
},
{
"scale": "3x",
"size": "40x40",
"idiom": "iphone",
"filename": "Icon120.png"
},
{
"scale": "2x",
"size": "60x60",
"idiom": "iphone",
"filename": "Icon120.png"
},
{
"scale": "3x",
"size": "60x60",
"idiom": "iphone",
"filename": "Icon180.png"
},
{
"scale": "1x",
"size": "20x20",
"idiom": "ipad",
"filename": "Icon20.png"
},
{
"scale": "2x",
"size": "20x20",
"idiom": "ipad",
"filename": "Icon40.png"
},
{
"scale": "1x",
"size": "29x29",
"idiom": "ipad",
"filename": "Icon29.png"
},
{
"scale": "2x",
"size": "29x29",
"idiom": "ipad",
"filename": "Icon58.png"
},
{
"scale": "1x",
"size": "40x40",
"idiom": "ipad",
"filename": "Icon40.png"
},
{
"scale": "2x",
"size": "40x40",
"idiom": "ipad",
"filename": "Icon80.png"
},
{
"scale": "1x",
"size": "76x76",
"idiom": "ipad",
"filename": "Icon76.png"
},
{
"scale": "2x",
"size": "76x76",
"idiom": "ipad",
"filename": "Icon152.png"
},
{
"scale": "2x",
"size": "83.5x83.5",
"idiom": "ipad",
"filename": "Icon167.png"
},
{
"scale": "1x",
"size": "1024x1024",
"idiom": "ios-marketing",
"filename": "Icon1024.png"
}
],
"properties": {},
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>MinimumOSVersion</key>
<string>8.0</string>
<key>CFBundleDisplayName</key>
<string>PhilExample</string>
<key>CFBundleIdentifier</key>
<string>com.companyname.PhilExample</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>CFBundleName</key>
<string>PhilExample</string>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
</dict>
</plist>

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Foundation;
using UIKit;
namespace PhilExample.iOS
{
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">iPhoneSimulator</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{15E6D01B-99F3-4880-83F0-13710A70E9AB}</ProjectGuid>
<ProjectTypeGuids>{FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TemplateGuid>{6143fdea-f3c2-4a09-aafa-6e230626515e}</TemplateGuid>
<OutputType>Exe</OutputType>
<RootNamespace>PhilExampleMobile.iOS</RootNamespace>
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
<AssemblyName>PhilExampleMobile.iOS</AssemblyName>
<MtouchEnableSGenConc>true</MtouchEnableSGenConc>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<ProvisioningType>automatic</ProvisioningType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\iPhoneSimulator\Debug</OutputPath>
<DefineConstants>DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchArch>x86_64</MtouchArch>
<MtouchLink>None</MtouchLink>
<MtouchDebug>true</MtouchDebug>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhoneSimulator' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\iPhoneSimulator\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchLink>None</MtouchLink>
<MtouchArch>x86_64</MtouchArch>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\iPhone\Debug</OutputPath>
<DefineConstants>DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchArch>ARM64</MtouchArch>
<CodesignKey>iPhone Developer</CodesignKey>
<MtouchDebug>true</MtouchDebug>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<MtouchLink>None</MtouchLink>
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\iPhone\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchArch>ARM64</MtouchArch>
<CodesignKey>iPhone Developer</CodesignKey>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="Main.cs" />
<Compile Include="AppDelegate.cs" />
<None Include="Entitlements.plist" />
<None Include="Info.plist" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon1024.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon180.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon167.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon152.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon120.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon87.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon80.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon76.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon60.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon58.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon40.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon29.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon20.png">
<Visible>false</Visible>
</ImageAsset>
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Xml" />
<Reference Include="System.Core" />
<Reference Include="Xamarin.iOS" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp">
<Version>2.88.2</Version>
</PackageReference>
<PackageReference Include="Xamarin.CommunityToolkit">
<Version>2.0.5</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Forms">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Svg">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.FFImageLoading.Svg.Forms">
<Version>2.4.11.982</Version>
</PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<ItemGroup>
<ProjectReference Include="..\PhilExampleMobile\PhilExampleMobile.csproj">
<Project>{2B20AE11-F221-4436-A260-E66B581B9D3F}</Project>
<Name>PhilExampleMobile</Name>
</ProjectReference>
</ItemGroup>
</Project>

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PhilExample.iOS")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("PhilExample.iOS")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("72bdc44f-c588-44f3-b6df-9aace7daafdd")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6245" systemVersion="13F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="X5k-f2-b5h">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="gAE-YM-kbH">
<objects>
<viewController id="X5k-f2-b5h" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Y8P-hJ-Z43"/>
<viewControllerLayoutGuide type="bottom" id="9ZL-r4-8FZ"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="yd7-JS-zBw">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" misplaced="YES" image="Icon-60.png" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="270" y="270" width="60" height="60"/>
<rect key="contentStretch" x="0.0" y="0.0" width="0.0" height="0.0"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.20392156862745098" green="0.59607843137254901" blue="0.85882352941176465" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="23" firstAttribute="centerY" secondItem="yd7-JS-zBw" secondAttribute="centerY" priority="1" id="39"/>
<constraint firstItem="23" firstAttribute="centerX" secondItem="yd7-JS-zBw" secondAttribute="centerX" priority="1" id="41"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XAI-xm-WK6" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="349" y="339"/>
</scene>
</scenes>
<resources>
<image name="Icon-60.png" width="180" height="180"/>
</resources>
</document>

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PhilExample.App">
<Application.Resources>
<Color x:Key="Color_Background">#F3F3F3</Color>
<Color x:Key="Color_Primary">#160BE9</Color>
<Color x:Key="Color_Secondary">#F8CF7B</Color>
<Color x:Key="Color_Error">#FF0000</Color>
<Color x:Key="Color_TextTitle">#2D2D2D</Color>
<Color x:Key="Color_Text1">#000000</Color>
<Color x:Key="Color_Text2">#2D2D2D</Color>
<Color x:Key="Color_Text3">#787974</Color>
<Color x:Key="Color_Text4">#949393</Color>
<Color x:Key="Color_TextHighlight">#160BE9</Color>
<Color x:Key="Color_TextContrast">#FFFFFF</Color>
<Color x:Key="Color_ControlBackground">#FFFFFF</Color>
<x:String x:Key="Img_XBaddne">resource://PhilExample.Resources.Images.x-baddne.svg</x:String>
<!-- Layout/Views -->
<Style TargetType="{x:Type ContentPage}" ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor" Value="{StaticResource Color_Background}" />
</Style>
<Style TargetType="StackLayout">
<Setter Property="BackgroundColor" Value="{StaticResource Color_Background}" />
</Style>
<Style TargetType="Grid">
<Setter Property="BackgroundColor" Value="{StaticResource Color_Background}" />
</Style>
<Style TargetType="CollectionView">
<Setter Property="BackgroundColor" Value="{StaticResource Color_Background}" />
</Style>
</Application.Resources>
</Application>

@ -0,0 +1,53 @@
using System;
using PhilExample.Interfaces;
using PhilExample.UI.Pages;
using PhilExample.Services;
using PhilExampleCrawler.Common.Models;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
[assembly: ExportFont("Nunito-Bold.ttf", Alias = "NunitoBold")]
[assembly: ExportFont("Nunito-SemiBold.ttf", Alias = "NunitoSemiBold")]
[assembly: ExportFont("Nunito-ExtraBold.ttf", Alias = "NunitoExtraBold")]
[assembly: ExportFont("Nunito-Medium.ttf", Alias = "NunitoMedium")]
namespace PhilExample
{
public partial class App : Application
{
public const string WebAPI_URL = "http://12.345.67.890:62285/";
public const string TCPSERVER_IP = "192.168.000.00";
public const int TCPSERVER_PORT = 1234;
public static UserSession CurrentSession { get; set; }
public App()
{
InitializeComponent();
DependencyService.RegisterSingleton<IPhExStorage>(new PhExStorage());
DependencyService.RegisterSingleton<IRESTAccess>(new RESTAccess());
DependencyService.RegisterSingleton<IUserService>(new UserService());
DependencyService.RegisterSingleton<ICrawlSessionService>(new CrawlSessionService());
DependencyService.RegisterSingleton<IExamplePageService>(new ExamplePageService());
//DependencyService.RegisterSingleton<ICrawlService_SH>(new CrawlService_SH());
//DependencyService.RegisterSingleton<ICrawlService_CH>(new CrawlService_CH());
//DependencyService.RegisterSingleton<ITCPClient>(new TCPClient());
MainPage = new AppShell();
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
}
protected override void OnResume()
{
}
public static T GetResource<T>(string key) => (T)App.Current.Resources[key];
}
}

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:PhilExample.UI.Pages"
Title="PhilExample"
x:Class="PhilExample.AppShell">
<Shell.Resources>
<ResourceDictionary>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Color_Background}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="{StaticResource Color_Text1}" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Color_Background}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="{StaticResource Color_Text1}"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
</ResourceDictionary>
</Shell.Resources>
<ShellContent Route="StartupPage"
ContentTemplate="{DataTemplate pages:StartupPage}" />
<TabBar Route="main">
<ShellContent Title="Dashboard"
Route="DashboardPage"
ContentTemplate="{DataTemplate pages:DashboardPage}"/>
<!-- <ShellContent Title="Profile"
Route="ProfilePage"
ContentTemplate="{DataTemplate pages:ProfilePage}" /> -->
</TabBar>
</Shell>

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PhilExample.UI.Pages;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PhilExample
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class AppShell : Xamarin.Forms.Shell
{
public AppShell()
{
InitializeComponent();
//Popups
Routing.RegisterRoute(nameof(AddCrawlSessionPage), typeof(AddCrawlSessionPage));
Routing.RegisterRoute(nameof(SelectCategoryPage), typeof(SelectCategoryPage));
Routing.RegisterRoute(nameof(SelectRadiusPage), typeof(SelectRadiusPage));
}
}
}

@ -0,0 +1,3 @@
using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExample.Interfaces
{
internal interface ICallbackPageVM<T>
{
event EventHandler<T> OnPopped;
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
namespace PhilExample.Interfaces
{
internal interface ICrawlSessionService
{
Task<CrawlSession> RegisterCrawlSessionAsync(int userID, CrawlSession cs);
Task<bool> UpdateCrawlSessionAsync(int userID, CrawlSession cs);
Task<bool> DeleteAsync(int crawlSessionID);
Task<List<Insertion>> GetLatestInsertionAsync(int userID, int? limit);
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhilExample.Models;
using System.Linq;
using PhilExampleCrawler.Common.Models;
using PhilExampleCrawler.Common.Interfaces;
namespace PhilExample.Interfaces
{
internal interface IExamplePageService
{
Task<IEnumerable<Category>> GetCategoriesAsync();
ICategory GetDefaultCategory();
Task<IEnumerable<Category>> GetRadiiAsync();
ICategory GetDefaultRadius();
Task<List<PickerItem>> GetLocationSuggestionsAsync(string query);
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PhilExample.Interfaces
{
public interface IForegroundTCPDelegate
{
string GetForegroundNotificationTitle { get; }
string GetForegroundNotificationDescription { get; }
void ReceiveLoop();
void OnForegroundStop();
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
namespace PhilExample.Interfaces
{
internal interface IPhExStorage
{
Task<bool> SaveCategoriesAsync(IEnumerable<Category> cats);
Task<IEnumerable<Category>> GetCategoriesAsync();
Task<bool> SaveRadiiAsync(IEnumerable<Category> radii);
Task<IEnumerable<Category>> GetRadiiAsync();
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace PhilExample.Interfaces
{
internal interface IRESTAccess
{
Task<T> GetAsync<T>(string url);
Task<T> PostAsync<T>(string url);
Task<T> PostAsync<T>(string url, object postData);
Task<bool> DeleteAsync(string url);
}
}

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
using PhilExampleCrawler.Common.TCP.Packets;
namespace PhilExample.Interfaces
{
internal interface ITCPClient
{
bool IsConnected { get; }
bool ConnectToServer(string ip, int port);
void Disconnect();
event EventHandler<int> OnUserConnected;
event EventHandler<(int connectionID, BasePacket bp)> OnDataReceived;
event EventHandler<int> OnUserDisconnected;
bool IsUserRegistered { get; }
DateTime? RegisteredUntil { get; }
bool RegisterForUpdates(User user);
void Unregister(User user);
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhilExampleCrawler.Common.Models;
namespace PhilExample.Interfaces
{
internal interface IUserService
{
User CurrentUser { get; }
Task<bool> LoginAsync(int userID);
Task<User> RegisterUserAsync(string authCode, string phoneNumber, bool optin_telegram);
}
}

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using PhilExampleCrawler.Common.Models;
namespace PhilExample.Models
{
public class CrawlSessionResponse
{
public bool Success { get; }
public CrawlSession CrawlSession { get; }
public CrawlSessionResponse(bool success, CrawlSession cs)
{
Success = success;
CrawlSession = cs;
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save