mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-09-29 19:08: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]
|
||||
|
||||
## [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
|
||||
|
||||
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>
|
||||
</requires>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.3.3</Version>
|
||||
<Version>0.3.4</Version>
|
||||
<Product>Postclient</Product>
|
||||
<AssemblyTitle>Postclient</AssemblyTitle>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
|
||||
@ -19,6 +19,8 @@ public interface ISemanticSearchService : IDisposable {
|
||||
|
||||
void NotifySettingsChanged();
|
||||
|
||||
void Wake();
|
||||
|
||||
float[]? EmbedQuery(string query);
|
||||
|
||||
ArchiveIndexStats IndexStats();
|
||||
|
||||
@ -17,6 +17,7 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
||||
private bool _gpuFailed;
|
||||
private string _status = "";
|
||||
private bool _ready;
|
||||
private long _statusTick;
|
||||
|
||||
public SemanticSearchService(ConfigurationFileService files, MailArchiveCatalog archive) {
|
||||
_files = files;
|
||||
@ -51,6 +52,10 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
||||
|
||||
public void NotifySettingsChanged() {
|
||||
_step?.Cancel();
|
||||
Wake();
|
||||
}
|
||||
|
||||
public void Wake() {
|
||||
try {
|
||||
_wake.Release();
|
||||
}
|
||||
@ -189,6 +194,9 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
||||
|
||||
var pending = _archive.PendingEmbeddings(EmbeddingModelSpec.Id, 8, ready);
|
||||
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);
|
||||
await WaitAsync(token).ConfigureAwait(false);
|
||||
return;
|
||||
@ -307,11 +315,20 @@ public sealed class SemanticSearchService : ISemanticSearchService {
|
||||
|
||||
private void SetStatus(string line, bool ready) {
|
||||
var nextReady = ready && _embedder is not null;
|
||||
var now = Environment.TickCount64;
|
||||
lock (_gate) {
|
||||
if (_status == line && _ready == nextReady)
|
||||
return;
|
||||
var hide = string.IsNullOrWhiteSpace(line);
|
||||
if (!hide && !string.IsNullOrWhiteSpace(_status) && now - _statusTick < 250) {
|
||||
_status = line;
|
||||
_ready = nextReady;
|
||||
return;
|
||||
}
|
||||
|
||||
_status = line;
|
||||
_ready = nextReady;
|
||||
_statusTick = now;
|
||||
}
|
||||
|
||||
Changed?.Invoke();
|
||||
|
||||
@ -39,6 +39,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
private const int IndexCatalogChunk = 2_000;
|
||||
private const int IndexUiChunk = 250;
|
||||
private int _indexBodyTotal;
|
||||
private readonly StatusLineHold _indexHold;
|
||||
private readonly StatusLineHold _semanticHold;
|
||||
private bool _uiReady;
|
||||
private MailMessageBody? _reading;
|
||||
private MessageRowViewModel? _pendingWindow;
|
||||
@ -455,6 +457,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
_worker = worker;
|
||||
_semantic = semantic;
|
||||
_updates = updates;
|
||||
_indexHold = new StatusLineHold(line => IndexLine = line);
|
||||
_semanticHold = new StatusLineHold(line => SemanticLine = line);
|
||||
files.Current.EnsureDefaults();
|
||||
PstStoreMigrator.Migrate(files, archive);
|
||||
MailArchiveCatalog.MigrateLegacy(files.Current.Mailboxes);
|
||||
@ -512,6 +516,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
_semantic.Changed -= OnSemanticChanged;
|
||||
_semantic.Faulted -= OnSemanticFaulted;
|
||||
_semantic.Dispose();
|
||||
_indexHold.Dispose();
|
||||
_semanticHold.Dispose();
|
||||
List<IMailSession> sessions;
|
||||
lock (_connections) {
|
||||
sessions = [.. _connections.Values];
|
||||
@ -533,8 +539,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
var line = _semantic.StatusLine;
|
||||
if (line.Contains("ready", StringComparison.OrdinalIgnoreCase))
|
||||
line = "";
|
||||
if (SemanticLine != line)
|
||||
SemanticLine = line;
|
||||
_semanticHold.Set(line);
|
||||
});
|
||||
|
||||
[RelayCommand]
|
||||
@ -1862,7 +1867,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
? _archive.RemoveFolder(mailboxId, folder)
|
||||
: _archive.RemoveUids(mailboxId, folder, uids));
|
||||
if (IsInitialSyncComplete(mailboxId))
|
||||
_semantic.NotifySettingsChanged();
|
||||
_semantic.Wake();
|
||||
}
|
||||
|
||||
private static void DeleteEmlFiles(IEnumerable<string> paths) {
|
||||
@ -3172,7 +3177,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
}
|
||||
|
||||
if (ready)
|
||||
_semantic.NotifySettingsChanged();
|
||||
_semantic.Wake();
|
||||
}
|
||||
|
||||
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.AttachmentIndex(body, UnwrapEnvelope));
|
||||
if (IsInitialSyncComplete(mailboxId))
|
||||
_semantic.NotifySettingsChanged();
|
||||
_semantic.Wake();
|
||||
}
|
||||
catch {
|
||||
}
|
||||
@ -3747,7 +3752,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
_backfill?.Cancel();
|
||||
_backfill?.Dispose();
|
||||
_backfill = null;
|
||||
IndexLine = "";
|
||||
_indexHold.ClearNow();
|
||||
}
|
||||
|
||||
private void EnsureIndexing(string mailboxId) {
|
||||
@ -3777,7 +3782,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
_backfill?.Cancel();
|
||||
_backfill?.Dispose();
|
||||
_backfill = null;
|
||||
IndexLine = "";
|
||||
_indexHold.ClearNow();
|
||||
}
|
||||
|
||||
private void StartIndexing() {
|
||||
@ -3858,7 +3863,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
if (pending.Count == 0) {
|
||||
if (!cataloged)
|
||||
break;
|
||||
await SetIndexLineAsync("", token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -3887,8 +3891,10 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (fetched == 0 && !cataloged)
|
||||
break;
|
||||
if (fetched == 0 && !cataloged) {
|
||||
await Task.Delay(500, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) {
|
||||
@ -3907,10 +3913,14 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
_indexMailboxes.Remove(id);
|
||||
}
|
||||
|
||||
IndexLine = "";
|
||||
_indexBodyTotal = 0;
|
||||
if (_indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true }))
|
||||
StartIndexing();
|
||||
var again = _indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true });
|
||||
if (!again) {
|
||||
_indexHold.Set("");
|
||||
_indexBodyTotal = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
StartIndexing();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3949,7 +3959,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
|
||||
private async Task SetIndexLineAsync(string line, CancellationToken token) =>
|
||||
await UiAsync(() => {
|
||||
if (!token.IsCancellationRequested)
|
||||
IndexLine = line;
|
||||
_indexHold.Set(line);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
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