mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-09-30 03:18:09 +02:00
(bugfix): keep index status lines from flashing between batches
This commit is contained in:
parent
92c997513b
commit
7198014a21
@ -6,6 +6,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.4] - 2026-09-20
|
||||||
|
|
||||||
|
Status bar index lines no longer flash between batches.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Status bar **Indexing** / **Indexing meaning** lines stay put between batches instead of collapsing and flashing.
|
||||||
|
|
||||||
## [0.3.3] - 2026-09-20
|
## [0.3.3] - 2026-09-20
|
||||||
|
|
||||||
First sync paints mail as it arrives, meaning search can use a discrete GPU, and trash, retention, and logs follow typical desktop mail apps.
|
First sync paints mail as it arrives, meaning search can use a discrete GPU, and trash, retention, and logs follow typical desktop mail apps.
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
<display_length compare="ge">768</display_length>
|
<display_length compare="ge">768</display_length>
|
||||||
</requires>
|
</requires>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.3.4" date="2026-09-20">
|
||||||
|
<description>
|
||||||
|
<p>Status bar indexing and meaning-index lines stay visible between batches instead of flashing.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.3.3" date="2026-09-20">
|
<release version="0.3.3" date="2026-09-20">
|
||||||
<description>
|
<description>
|
||||||
<p>First sync paints mail as it arrives. Meaning search DirectML prefers a discrete GPU. Trash retention, logs, and folder menus match typical desktop mail apps.</p>
|
<p>First sync paints mail as it arrives. Meaning search DirectML prefers a discrete GPU. Trash retention, logs, and folder menus match typical desktop mail apps.</p>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Version>0.3.3</Version>
|
<Version>0.3.4</Version>
|
||||||
<Product>Postclient</Product>
|
<Product>Postclient</Product>
|
||||||
<AssemblyTitle>Postclient</AssemblyTitle>
|
<AssemblyTitle>Postclient</AssemblyTitle>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public interface ISemanticSearchService : IDisposable {
|
|||||||
|
|
||||||
void NotifySettingsChanged();
|
void NotifySettingsChanged();
|
||||||
|
|
||||||
|
void Wake();
|
||||||
|
|
||||||
float[]? EmbedQuery(string query);
|
float[]? EmbedQuery(string query);
|
||||||
|
|
||||||
ArchiveIndexStats IndexStats();
|
ArchiveIndexStats IndexStats();
|
||||||
|
|||||||
@ -17,6 +17,7 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
|||||||
private bool _gpuFailed;
|
private bool _gpuFailed;
|
||||||
private string _status = "";
|
private string _status = "";
|
||||||
private bool _ready;
|
private bool _ready;
|
||||||
|
private long _statusTick;
|
||||||
|
|
||||||
public SemanticSearchService(ConfigurationFileService files, MailArchiveCatalog archive) {
|
public SemanticSearchService(ConfigurationFileService files, MailArchiveCatalog archive) {
|
||||||
_files = files;
|
_files = files;
|
||||||
@ -51,6 +52,10 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
|||||||
|
|
||||||
public void NotifySettingsChanged() {
|
public void NotifySettingsChanged() {
|
||||||
_step?.Cancel();
|
_step?.Cancel();
|
||||||
|
Wake();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Wake() {
|
||||||
try {
|
try {
|
||||||
_wake.Release();
|
_wake.Release();
|
||||||
}
|
}
|
||||||
@ -189,6 +194,9 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
|||||||
|
|
||||||
var pending = _archive.PendingEmbeddings(EmbeddingModelSpec.Id, 8, ready);
|
var pending = _archive.PendingEmbeddings(EmbeddingModelSpec.Id, 8, ready);
|
||||||
if (pending.Count == 0) {
|
if (pending.Count == 0) {
|
||||||
|
await DelayAsync(TimeSpan.FromMilliseconds(800), token).ConfigureAwait(false);
|
||||||
|
if (_archive.PendingEmbeddings(EmbeddingModelSpec.Id, 1, ready).Count > 0)
|
||||||
|
continue;
|
||||||
SetStatus("", ready: true);
|
SetStatus("", ready: true);
|
||||||
await WaitAsync(token).ConfigureAwait(false);
|
await WaitAsync(token).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
@ -307,11 +315,20 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
|||||||
|
|
||||||
private void SetStatus(string line, bool ready) {
|
private void SetStatus(string line, bool ready) {
|
||||||
var nextReady = ready && _embedder is not null;
|
var nextReady = ready && _embedder is not null;
|
||||||
|
var now = Environment.TickCount64;
|
||||||
lock (_gate) {
|
lock (_gate) {
|
||||||
if (_status == line && _ready == nextReady)
|
if (_status == line && _ready == nextReady)
|
||||||
return;
|
return;
|
||||||
|
var hide = string.IsNullOrWhiteSpace(line);
|
||||||
|
if (!hide && !string.IsNullOrWhiteSpace(_status) && now - _statusTick < 250) {
|
||||||
|
_status = line;
|
||||||
|
_ready = nextReady;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_status = line;
|
_status = line;
|
||||||
_ready = nextReady;
|
_ready = nextReady;
|
||||||
|
_statusTick = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
Changed?.Invoke();
|
Changed?.Invoke();
|
||||||
|
|||||||
@ -39,6 +39,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
private const int IndexCatalogChunk = 2_000;
|
private const int IndexCatalogChunk = 2_000;
|
||||||
private const int IndexUiChunk = 250;
|
private const int IndexUiChunk = 250;
|
||||||
private int _indexBodyTotal;
|
private int _indexBodyTotal;
|
||||||
|
private readonly StatusLineHold _indexHold;
|
||||||
|
private readonly StatusLineHold _semanticHold;
|
||||||
private bool _uiReady;
|
private bool _uiReady;
|
||||||
private MailMessageBody? _reading;
|
private MailMessageBody? _reading;
|
||||||
private MessageRowViewModel? _pendingWindow;
|
private MessageRowViewModel? _pendingWindow;
|
||||||
@ -455,6 +457,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
_worker = worker;
|
_worker = worker;
|
||||||
_semantic = semantic;
|
_semantic = semantic;
|
||||||
_updates = updates;
|
_updates = updates;
|
||||||
|
_indexHold = new StatusLineHold(line => IndexLine = line);
|
||||||
|
_semanticHold = new StatusLineHold(line => SemanticLine = line);
|
||||||
files.Current.EnsureDefaults();
|
files.Current.EnsureDefaults();
|
||||||
PstStoreMigrator.Migrate(files, archive);
|
PstStoreMigrator.Migrate(files, archive);
|
||||||
MailArchiveCatalog.MigrateLegacy(files.Current.Mailboxes);
|
MailArchiveCatalog.MigrateLegacy(files.Current.Mailboxes);
|
||||||
@ -512,6 +516,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
_semantic.Changed -= OnSemanticChanged;
|
_semantic.Changed -= OnSemanticChanged;
|
||||||
_semantic.Faulted -= OnSemanticFaulted;
|
_semantic.Faulted -= OnSemanticFaulted;
|
||||||
_semantic.Dispose();
|
_semantic.Dispose();
|
||||||
|
_indexHold.Dispose();
|
||||||
|
_semanticHold.Dispose();
|
||||||
List<IMailSession> sessions;
|
List<IMailSession> sessions;
|
||||||
lock (_connections) {
|
lock (_connections) {
|
||||||
sessions = [.. _connections.Values];
|
sessions = [.. _connections.Values];
|
||||||
@ -533,8 +539,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
var line = _semantic.StatusLine;
|
var line = _semantic.StatusLine;
|
||||||
if (line.Contains("ready", StringComparison.OrdinalIgnoreCase))
|
if (line.Contains("ready", StringComparison.OrdinalIgnoreCase))
|
||||||
line = "";
|
line = "";
|
||||||
if (SemanticLine != line)
|
_semanticHold.Set(line);
|
||||||
SemanticLine = line;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@ -1862,7 +1867,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
? _archive.RemoveFolder(mailboxId, folder)
|
? _archive.RemoveFolder(mailboxId, folder)
|
||||||
: _archive.RemoveUids(mailboxId, folder, uids));
|
: _archive.RemoveUids(mailboxId, folder, uids));
|
||||||
if (IsInitialSyncComplete(mailboxId))
|
if (IsInitialSyncComplete(mailboxId))
|
||||||
_semantic.NotifySettingsChanged();
|
_semantic.Wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DeleteEmlFiles(IEnumerable<string> paths) {
|
private static void DeleteEmlFiles(IEnumerable<string> paths) {
|
||||||
@ -3172,7 +3177,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ready)
|
if (ready)
|
||||||
_semantic.NotifySettingsChanged();
|
_semantic.Wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMessagesAsync(string folder, CancellationToken token, bool resumeBodies = false) {
|
private async Task LoadMessagesAsync(string folder, CancellationToken token, bool resumeBodies = false) {
|
||||||
@ -3684,7 +3689,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
MailArchiveMap.BodyText(body, UnwrapEnvelope),
|
MailArchiveMap.BodyText(body, UnwrapEnvelope),
|
||||||
MailArchiveMap.AttachmentIndex(body, UnwrapEnvelope));
|
MailArchiveMap.AttachmentIndex(body, UnwrapEnvelope));
|
||||||
if (IsInitialSyncComplete(mailboxId))
|
if (IsInitialSyncComplete(mailboxId))
|
||||||
_semantic.NotifySettingsChanged();
|
_semantic.Wake();
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
}
|
}
|
||||||
@ -3747,7 +3752,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
_backfill?.Cancel();
|
_backfill?.Cancel();
|
||||||
_backfill?.Dispose();
|
_backfill?.Dispose();
|
||||||
_backfill = null;
|
_backfill = null;
|
||||||
IndexLine = "";
|
_indexHold.ClearNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureIndexing(string mailboxId) {
|
private void EnsureIndexing(string mailboxId) {
|
||||||
@ -3777,7 +3782,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
_backfill?.Cancel();
|
_backfill?.Cancel();
|
||||||
_backfill?.Dispose();
|
_backfill?.Dispose();
|
||||||
_backfill = null;
|
_backfill = null;
|
||||||
IndexLine = "";
|
_indexHold.ClearNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartIndexing() {
|
private void StartIndexing() {
|
||||||
@ -3858,7 +3863,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
if (pending.Count == 0) {
|
if (pending.Count == 0) {
|
||||||
if (!cataloged)
|
if (!cataloged)
|
||||||
break;
|
break;
|
||||||
await SetIndexLineAsync("", token).ConfigureAwait(false);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3887,8 +3891,10 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fetched == 0 && !cataloged)
|
if (fetched == 0 && !cataloged) {
|
||||||
break;
|
await Task.Delay(500, token).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) {
|
catch (OperationCanceledException) {
|
||||||
@ -3907,10 +3913,14 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
_indexMailboxes.Remove(id);
|
_indexMailboxes.Remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
IndexLine = "";
|
var again = _indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true });
|
||||||
_indexBodyTotal = 0;
|
if (!again) {
|
||||||
if (_indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true }))
|
_indexHold.Set("");
|
||||||
StartIndexing();
|
_indexBodyTotal = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartIndexing();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3949,7 +3959,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
|||||||
private async Task SetIndexLineAsync(string line, CancellationToken token) =>
|
private async Task SetIndexLineAsync(string line, CancellationToken token) =>
|
||||||
await UiAsync(() => {
|
await UiAsync(() => {
|
||||||
if (!token.IsCancellationRequested)
|
if (!token.IsCancellationRequested)
|
||||||
IndexLine = line;
|
_indexHold.Set(line);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
private MailboxAccount? MailboxById(string mailboxId) =>
|
private MailboxAccount? MailboxById(string mailboxId) =>
|
||||||
|
|||||||
104
src/MaksIT.PostClient.UI/ViewModels/StatusLineHold.cs
Normal file
104
src/MaksIT.PostClient.UI/ViewModels/StatusLineHold.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.PostClient.UI.ViewModels;
|
||||||
|
|
||||||
|
|
||||||
|
internal sealed class StatusLineHold {
|
||||||
|
private readonly Action<string> _apply;
|
||||||
|
private readonly int _minShowMs;
|
||||||
|
private readonly int _hideMs;
|
||||||
|
private string _wanted = "";
|
||||||
|
private string _shown = "";
|
||||||
|
private long _shownAt;
|
||||||
|
private int _gen;
|
||||||
|
private CancellationTokenSource? _delay;
|
||||||
|
|
||||||
|
public StatusLineHold(Action<string> apply, int minShowMs = 250, int hideMs = 800) {
|
||||||
|
_apply = apply;
|
||||||
|
_minShowMs = minShowMs;
|
||||||
|
_hideMs = hideMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string line) {
|
||||||
|
_wanted = line ?? "";
|
||||||
|
CancelDelay();
|
||||||
|
if (!string.IsNullOrWhiteSpace(_wanted)) {
|
||||||
|
_gen++;
|
||||||
|
var now = Environment.TickCount64;
|
||||||
|
if (_shown == _wanted)
|
||||||
|
return;
|
||||||
|
if (_shown.Length > 0 && now - _shownAt < _minShowMs) {
|
||||||
|
Start(FlushLater);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Show(_wanted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gen = ++_gen;
|
||||||
|
Start(token => HideLater(gen, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearNow() {
|
||||||
|
CancelDelay();
|
||||||
|
_gen++;
|
||||||
|
_wanted = "";
|
||||||
|
Show("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
CancelDelay();
|
||||||
|
|
||||||
|
private void Start(Func<CancellationToken, Task> work) {
|
||||||
|
var delay = new CancellationTokenSource();
|
||||||
|
_delay = delay;
|
||||||
|
_ = work(delay.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FlushLater(CancellationToken token) {
|
||||||
|
var wait = _minShowMs - (int)(Environment.TickCount64 - _shownAt);
|
||||||
|
if (wait < 0)
|
||||||
|
wait = 0;
|
||||||
|
try {
|
||||||
|
await Task.Delay(wait, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
if (!string.IsNullOrWhiteSpace(_wanted) && _wanted != _shown)
|
||||||
|
Show(_wanted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HideLater(int gen, CancellationToken token) {
|
||||||
|
try {
|
||||||
|
await Task.Delay(_hideMs, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
if (gen != _gen)
|
||||||
|
return;
|
||||||
|
if (string.IsNullOrWhiteSpace(_wanted))
|
||||||
|
Show("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Show(string line) {
|
||||||
|
_shown = line;
|
||||||
|
_shownAt = Environment.TickCount64;
|
||||||
|
_apply(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelDelay() {
|
||||||
|
_delay?.Cancel();
|
||||||
|
_delay?.Dispose();
|
||||||
|
_delay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user