Compare commits

...

6 commits

Author SHA1 Message Date
6fb47905c0 Updated Program.
Removed all code from console testing, and moved into App.xaml.cs for
handling of the Syncing of things in co-op with the UI Thread.
2025-07-31 03:35:26 -05:00
b0ac6c1b7b Updated MainWindow
Updated MainWindow.xaml with UI controls for FreeTubeSyncer application
settings.

Updated MainWindow.xaml.cs code behind file top handle logic of not
closing the app when the window is closed, and handling Saving and
Hiding of the Window when close is requestd.
2025-07-31 03:34:31 -05:00
56432cd33d Updated CSProj
Added CommunityToolkit.Mvvm, and SukiUI as required packages.
2025-07-31 03:32:59 -05:00
7099e31ff5 Updated App
Updated App.xaml to use SukiUI.
Added Event Handlers for System Tray.

Updated App.xaml.cs code behind file to implement Logic that was in
Program.cs, into the core of the UI App.  Implemented Async Tasks, to
allow for UI to remain responsive, while doing long running tasks.
Added Loading, Saving and Updating settings from UI updates.
2025-07-31 03:32:30 -05:00
e6236b8a5a Updated Syncer
Added parameter for Rest Base URL to allow configurability of the Base
URL for REST API.
Updated ISyncer, to add Enable(), Disable(), IsEnabled(),
UpdateBaseUrl(), SetEnabled() and PingApi() to allow for settings.

PingApi() will test to see if the URL provided is reachable.
UpdatebaseUrl() will set the URL for the server to communicate with.
SetEnabled(), Enable() Disable() and IsEnabled() to allow for settings
to toggle on and off syncing of specific parts of FreeTube.
2025-07-31 03:30:31 -05:00
ec55cf2f11 Added AppSettings
Added AppSettings for FreeTubeSyncer.
2025-07-31 03:28:08 -05:00
8 changed files with 349 additions and 119 deletions

View file

@ -1,22 +1,24 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI"
x:Class="FreeTubeSyncer.App"
RequestedThemeVariant="Default">
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<sukiUi:SukiTheme ThemeColor="Blue" />
</Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/freetube.png"
ToolTipText="FreeTube Syncer">
ToolTipText="FreeTube Syncer"
Clicked="TrayIcon_OnClicked">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Show Settings"/>
<NativeMenuItem Header="Show Settings" Click="ShowSettings_OnClick"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="Quit"/>
<NativeMenuItem Header="Quit" Click="Quit_OnClick"/>
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>

View file

@ -1,17 +1,42 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using FreeTubeSyncer.Library;
using FreeTubeSyncer.Models;
using FreeTubeSyncer.Models.DatabaseModels;
using FreeTubeSyncer.REST;
namespace FreeTubeSyncer;
public partial class App : Application
{
private DBSyncWatcher _watcher;
private DBSyncWatcher? _watcher;
private List<ISyncer>? _syncers;
private Syncer<History>? _historySyncer;
private Syncer<Playlist>? _playlistSyncer;
private Syncer<Profile>? _profileSyncer;
private Syncer<SearchHistory>? _searchHistorySyncer;
private Syncer<Setting>? _settingSyncer;
private bool _isRunning = true;
private Task? _watcherTask;
private DateTime _lastClick = DateTime.MinValue;
private AppSettings? _settings;
private string? _oldSettings;
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
private TimeSpan _checkInterval;
public event EventHandler? SettingsChanged;
public static App GetApp() => (App)App.Current!;
public static ClassicDesktopStyleApplicationLifetime GetDesktop() => (ClassicDesktopStyleApplicationLifetime)App.Current!.ApplicationLifetime!;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@ -21,9 +46,8 @@ public partial class App : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
//desktop.MainWindow = new MainWindow();
var path = "";
if (OperatingSystem.IsWindows())
if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FreeTube");
else if (OperatingSystem.IsLinux())
{
@ -37,17 +61,187 @@ public partial class App : Application
Console.WriteLine("Failed to find Path for FreeTube!");
}
}
else if (OperatingSystem.IsMacOS())
path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FreeTube");
LoadSettings();
_checkInterval = TimeSpan.FromSeconds(_settings!.CheckInterval);
_watcher = new DBSyncWatcher(path);
_watcher.WatchFiles.Add("history.db", typeof(History));
_watcher.WatchFiles.Add("playlist.db", typeof(Playlist));
_watcher.WatchFiles.Add("profiles.db", typeof(Profile));
_watcher.WatchFiles.Add("search-history.db", typeof(SearchHistory));
_watcher.WatchFiles.Add("settings.db", typeof(Setting));
_historySyncer = new Syncer<History>(_watcher, Path.Join(path, "history.db"), "history.db", _settings.RestBaseUrl, "/history");
_playlistSyncer = new Syncer<Playlist>(_watcher, Path.Join(path, "playlists.db"), "playlists.db", _settings.RestBaseUrl, "/playlist");
_profileSyncer = new Syncer<Profile>(_watcher, Path.Join(path, "profiles.db"), "profiles.db", _settings.RestBaseUrl, "/profile");
_searchHistorySyncer = new Syncer<SearchHistory>(_watcher, Path.Join(path, "search-history.db"), "search-history.db", _settings.RestBaseUrl, "/searchHistory");
_settingSyncer = new Syncer<Setting>(_watcher, Path.Join(path, "settings.db"), "settings.db", _settings.RestBaseUrl, "/settings");
_syncers =
[
_historySyncer,
_playlistSyncer,
_profileSyncer,
_searchHistorySyncer,
_settingSyncer
];
SettingsChanged += HandleSettingsChanged;
_watcherTask = Task.Run(SyncMonitor);
}
base.OnFrameworkInitializationCompleted();
}
public void StampSettings() => _oldSettings = JsonSerializer.Serialize(_settings);
public void ResetSettings()
{
var @new = JsonSerializer.Deserialize<AppSettings>(_oldSettings!);
_settings!.RestBaseUrl = @new!.RestBaseUrl;
_settings.CheckInterval = @new.CheckInterval;
_settings.SyncHistory = @new.SyncHistory;
_settings.SyncPlaylist = @new.SyncPlaylist;
_settings.SyncProfile = @new.SyncProfile;
_settings.SyncSearchHistory = @new.SyncSearchHistory;
_settings.SyncSettings = @new.SyncSettings;
}
public void SaveSettings()
{
var path = GetSettingsPath();
if (!File.Exists(path))
{
var dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
File.WriteAllText(path, JsonSerializer.Serialize(_settings));
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
public void LoadSettings()
{
var path = GetSettingsPath();
if (!File.Exists(path))
{
_settings = new AppSettings();
return;
}
var data = File.ReadAllText(path);
_settings = JsonSerializer.Deserialize<AppSettings>(data);
}
private string GetSettingsPath() => OperatingSystem.IsLinux()
? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FreeTubeSyncer", "settings.json")
: Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "FreeTubeSyncer", "settings.json");
private async Task SyncMonitor()
{
while (_isRunning)
{
await _semaphoreSlim.WaitAsync();
if (!await _syncers![0].PingApi())
{
_semaphoreSlim.Release();
await Task.Delay(5000);
continue;
}
foreach (var syncer in _syncers!)
{
await syncer.FetchDatabase();
}
_semaphoreSlim.Release();
break;
}
var lastCheck = DateTime.Now;
while (_isRunning)
{
await Task.Delay(100);
var sinceLastCheck = DateTime.Now - lastCheck;
if (sinceLastCheck.TotalSeconds > _checkInterval.TotalSeconds)
{
await _semaphoreSlim.WaitAsync();
var start = DateTime.Now;
// TODO: Replace with Logger
Console.WriteLine("Checking for updates...");
foreach (var syncer in _syncers)
await syncer.FetchDatabase();
lastCheck = DateTime.Now;
var end = DateTime.Now - start;
_semaphoreSlim.Release();
// TODO: Replace with Logger
Console.WriteLine($"Check Completed. Total Time: {end}");
continue;
}
if (!_syncers.Any(x => x.IsDirty())) continue;
var procs = Process.GetProcessesByName("FreeTube");
if (procs.Length > 0) continue;
// TODO: Replace with Logger
Console.WriteLine("FreeTube closed, and we have writes to make...");
await Task.Delay(1500);
await _semaphoreSlim.WaitAsync();
var syncStart = DateTime.Now;
foreach (var syncer in _syncers)
syncer.Sync();
var syncEnd = DateTime.Now - syncStart;
_semaphoreSlim.Release();
// TODO: Replace with Logger
Console.WriteLine($"Sync completed in {syncEnd}.");
}
}
private async void HandleSettingsChanged(object? sender, EventArgs e)
{
await _semaphoreSlim.WaitAsync();
var old = JsonSerializer.Deserialize<AppSettings>(_oldSettings!);
if (_settings!.RestBaseUrl != old!.RestBaseUrl)
{
foreach (var syncer in _syncers!)
syncer.UpdateBaseUrl(_settings.RestBaseUrl);
}
if (old.CheckInterval != _settings.CheckInterval) _checkInterval = TimeSpan.FromSeconds(_settings.CheckInterval);
if (old.SyncHistory != _settings.SyncHistory) _historySyncer!.SetEnabled(_settings.SyncHistory);
if (old.SyncPlaylist != _settings.SyncPlaylist) _playlistSyncer!.SetEnabled(_settings.SyncPlaylist);
if (old.SyncProfile != _settings.SyncProfile) _profileSyncer!.SetEnabled(_settings.SyncProfile);
if (old.SyncSearchHistory != _settings.SyncSearchHistory) _searchHistorySyncer!.SetEnabled(_settings.SyncSearchHistory);
if (old.SyncSettings != _settings.SyncSettings) _settingSyncer!.SetEnabled(_settings.SyncSettings);
_semaphoreSlim.Release();
}
private void ShowSettings_OnClick(object? sender, EventArgs e)
{
if (App.Current!.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
if (desktop.MainWindow is null)
{
desktop.MainWindow = new MainWindow()
{
DataContext = _settings
};
}
StampSettings();
desktop.MainWindow!.Show();
}
private void Quit_OnClick(object? sender, EventArgs e)
{
if (App.Current!.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
_semaphoreSlim.Wait();
_isRunning = false;
_semaphoreSlim.Release();
_watcherTask?.Wait();
desktop.Shutdown();
}
private void TrayIcon_OnClicked(object? sender, EventArgs e)
{
var clicked = DateTime.Now;
if (_lastClick == DateTime.MinValue || (clicked - _lastClick).TotalMilliseconds > TimeSpan.FromMilliseconds(500).TotalMilliseconds)
_lastClick = clicked;
else
{
_lastClick = DateTime.MinValue;
ShowSettings_OnClick(sender, e);
}
}
}

View file

@ -19,7 +19,9 @@
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="SukiUI" Version="6.0.2" />
</ItemGroup>
<ItemGroup>

View file

@ -1,9 +1,51 @@
<Window xmlns="https://github.com/avaloniaui"
<suki:SukiWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:suki="https://github.com/kikipoulet/SukiUI"
xmlns:vm="clr-namespace:FreeTubeSyncer.Models"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="345"
Width="400" Height="345"
x:Class="FreeTubeSyncer.MainWindow"
x:DataType="vm:AppSettings"
BackgroundStyle="GradientDarker"
CanResize="False"
CanMaximize="False"
Title="FreeTubeSyncer">
Welcome to Avalonia!
</Window>
<StackPanel Orientation="Vertical" Margin="10" Spacing="5">
<Label Content="Settings" FontSize="24"/>
<Separator/>
<Grid
ColumnDefinitions="182,*"
RowDefinitions="*,*">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Text="REST API Backend Server: " FontWeight="Bold"/>
<TextBox Grid.Row="0" Grid.Column="1" Watermark="http://localhost:8080" Text="{Binding RestBaseUrl, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Text="Refresh Interval: " FontWeight="Bold"/>
<NumericUpDown Grid.Row="1" Grid.Column="1" Watermark="30" suki:NumericUpDownExtensions.Unit="seconds" FormatString="N0" Value="{Binding CheckInterval, Mode=TwoWay}"/>
</Grid>
<Grid
ColumnDefinitions="170,*"
RowDefinitions="*,*,*">
<ToggleSwitch Grid.Row="0" Grid.Column="0" IsPressed="{Binding SyncHistory, Mode=TwoWay}" OnContent="Sync History" />
<ToggleSwitch Grid.Row="0" Grid.Column="1" IsPressed="{Binding SyncPlaylist, Mode=TwoWay}" OnContent="Sync Playlists"/>
<ToggleSwitch Grid.Row="1" Grid.Column="0" IsPressed="{Binding SyncProfile, Mode=TwoWay}" OnContent="Sync Profiles"/>
<ToggleSwitch Grid.Row="1" Grid.Column="1" IsPressed="{Binding SyncSearchHistory, Mode=TwoWay}" OnContent="Sync Search History"/>
<ToggleSwitch Grid.Row="2" Grid.Column="0" IsPressed="{Binding SyncSettings, Mode=TwoWay}" OnContent="Sync Settings"/>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Button
IsEnabled="{Binding SettingsDirty}"
Classes="Flat Accent"
Click="SaveSettings_OnClick"
Content="Save"/>
<Button
IsEnabled="True"
Classes="Danger"
Click="CloseWindow_OnClick"
Content="Cancel"/>
</StackPanel>
</StackPanel>
</suki:SukiWindow>

View file

@ -1,11 +1,29 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using SukiUI.Controls;
namespace FreeTubeSyncer;
public partial class MainWindow : Window
public partial class MainWindow : SukiWindow
{
public MainWindow()
{
InitializeComponent();
this.Closing += (s, e) =>
{
((SukiWindow)s).Hide();
e.Cancel = true;
};
}
private void SaveSettings_OnClick(object? sender, RoutedEventArgs e)
{
App.GetApp().SaveSettings();
Hide();
}
private void CloseWindow_OnClick(object? sender, RoutedEventArgs e)
{
App.GetApp().ResetSettings();
Hide();
}
}

View file

@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace FreeTubeSyncer.Models;
public partial class AppSettings : ObservableObject
{
[ObservableProperty] [property: JsonInclude] private string _restBaseUrl = "http://127.0.0.1:8080";
[ObservableProperty] [property: JsonInclude] private int _checkInterval = 30;
[ObservableProperty] [property: JsonInclude] private bool _syncHistory = true;
[ObservableProperty] [property: JsonInclude] private bool _syncPlaylist = true;
[ObservableProperty] [property: JsonInclude] private bool _syncProfile = true;
[ObservableProperty] [property: JsonInclude] private bool _syncSearchHistory = true;
[ObservableProperty] [property: JsonInclude] private bool _syncSettings = true;
[ObservableProperty] [property: JsonIgnore] private bool _settingsDirty = false;
public AppSettings()
{
PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(SettingsDirty))
return;
SettingsDirty = true;
};
}
}

View file

@ -18,100 +18,7 @@ class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
GlobalJsonOptions.Options.Converters.Add(new StringToLongJsonConverter(false));
var paths = new string[]
{
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config/FreeTube/"),
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".var/app/io.freetubeapp.FreeTube/config/FreeTube")
};
var path = "";
foreach (var tpath in paths)
{
if (!Directory.Exists(tpath)) continue;
path = tpath;
break;
}
var dbWatcher = new DBSyncWatcher(path);
var historySyncer = new Syncer<History>(dbWatcher, Path.Join(path, "history.db"), "history.db", "/history");
var playlistSyncer = new Syncer<Playlist>(dbWatcher, Path.Join(path, "playlists.db"), "playlists.db", "/playlist");
var profileSyncer = new Syncer<Profile>(dbWatcher, Path.Join(path, "profiles.db"), "profiles.db", "/profile");
var searchHistorySyncer = new Syncer<SearchHistory>(dbWatcher, Path.Join(path, "search-history.db"), "search-history.db", "/searchHistory");
var settingsSyncer = new Syncer<Setting>(dbWatcher, Path.Join(path, "settings.db"), "settings.db", "/settings");
Task.Run(() => CheckCanSync(historySyncer, playlistSyncer, profileSyncer, searchHistorySyncer, settingsSyncer));
Console.WriteLine("Watching databases... Press return to exit...");
Console.ReadLine();
}
private static void CheckCanSync(Syncer<History>? historySyncer = null, Syncer<Playlist>? playlistSyncer = null,
Syncer<Profile>? profileSyncer = null, Syncer<SearchHistory>? searchHistorySyncer = null,
Syncer<Setting>? settingsSyncer = null)
{
var syncers = new List<ISyncer>()
{
historySyncer,
playlistSyncer,
profileSyncer,
searchHistorySyncer,
settingsSyncer
};
var lastTime = DateTime.Now;
var checkInterval = TimeSpan.FromSeconds(30);
while (true)
{
if (syncers.Any(x => x != null && x.IsDirty() ))
{
Thread.Sleep(100);
var lastCheck = DateTime.Now - lastTime;
if (lastCheck > checkInterval)
{
var start = DateTime.Now;
Console.WriteLine("Checking for updates...");
foreach (var syncer in syncers)
syncer.FetchDatabase().Wait();
lastTime = DateTime.Now;
var end = DateTime.Now - start;
Console.WriteLine($"Check completed. Total Time: {end}");
}
var procs = Process.GetProcessesByName("FreeTube");
if (procs.Length > 0) continue;
Console.WriteLine("FreeTube has closed and we have updates, we're going to try and update.");
Thread.Sleep(1500);
if (historySyncer != null && historySyncer.IsDirty())
historySyncer.Sync();
if (playlistSyncer != null && playlistSyncer.IsDirty())
playlistSyncer.Sync();
if (profileSyncer != null && profileSyncer.IsDirty())
profileSyncer.Sync();
if (searchHistorySyncer != null && searchHistorySyncer.IsDirty())
searchHistorySyncer.Sync();
if (settingsSyncer != null && settingsSyncer.IsDirty())
settingsSyncer.Sync();
}
else
{
Thread.Sleep(100);
var lastCheck = DateTime.Now - lastTime;
if (lastCheck < checkInterval) continue;
var start = DateTime.Now;
Console.WriteLine("Checking for updates...");
foreach (var syncer in syncers)
syncer.FetchDatabase().Wait();
lastTime = DateTime.Now;
var end = DateTime.Now - start;
Console.WriteLine($"Check completed. Total Time: {end}");
}
}
}
// public static void Main(string[] args) => BuildAvaloniaApp()
// .StartWithClassicDesktopLifetime(args);
public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -19,6 +20,13 @@ public interface ISyncer
void HandleDatabaseChange(string dbName, string entryObject);
void Sync();
bool IsDirty();
void Enable();
void Disable();
bool IsEnabled();
void UpdateBaseUrl(string baseUrl);
void SetEnabled(bool enabled);
Task<bool> PingApi();
}
public class Syncer<T> : ISyncer where T : class, IDataModel, new()
@ -32,23 +40,50 @@ public class Syncer<T> : ISyncer where T : class, IDataModel, new()
private bool _isDirty = false;
private bool _syncing = false;
private bool _enabled = true;
public Syncer(DBSyncWatcher watcher, string dbPath, string dbName, string restEndpoint)
public Syncer(DBSyncWatcher watcher, string dbPath, string dbName, string restBaseUrl, string restEndpoint)
{
_watcher = watcher;
_watcher.WatchFiles[dbName] = typeof(T);
_watcher.OnDatabaseChange += HandleDatabaseChange;
_client = new RestClient(new RestClientOptions("http://192.168.1.30:5050"));
_client = new RestClient(new RestClientOptions(restBaseUrl));
_dbPath = dbPath;
_dbName = dbName;
_restEndpoint = restEndpoint;
FetchDatabase().Wait();
}
public bool IsDirty() => _isDirty;
public bool IsEnabled() => _enabled;
public void Enable() => _enabled = true;
public void Disable() => _enabled = false;
public void SetEnabled(bool enabled) => _enabled = enabled;
public void UpdateBaseUrl(string baseUrl)
{
_client.Dispose();
_client = new RestClient(new RestClientOptions(baseUrl));
}
public async Task<bool> PingApi()
{
try
{
var res = await _client.ExecuteHeadAsync(new RestRequest("/ping"));
if (res.StatusCode == HttpStatusCode.NotFound)
return true;
}
catch (Exception ex)
{
return false;
}
return false;
}
public async Task ReadDatabase()
{
if (!_enabled) return;
var lines = File.ReadAllLines(_dbPath);
foreach (var entry in lines)
{
@ -76,6 +111,7 @@ public class Syncer<T> : ISyncer where T : class, IDataModel, new()
public async Task FetchDatabase()
{
if (!_enabled) return;
var entries = await _client.GetAsync<List<T>>(_restEndpoint);
if (entries == null) return;
foreach (var entry in entries)
@ -98,6 +134,7 @@ public class Syncer<T> : ISyncer where T : class, IDataModel, new()
public async void HandleDatabaseChange(string dbName, string entryObject)
{
if (!_enabled) return;
if (dbName != _dbName)
return;
@ -133,6 +170,7 @@ public class Syncer<T> : ISyncer where T : class, IDataModel, new()
public void Sync()
{
if (!_enabled) return;
if (!_isDirty)
return;
_syncing = true;