feat(privacy): reconnect database maintenance and the manual retention run
Two sections that had backends and no buttons. Database: path, size, WAL size, message count, and a clear button. The numbers refresh at most every five seconds and not at all while a long operation owns the store -- MessageCount takes the read lock, and asking for it during a VACUUM means waiting for the whole file to be rewritten, on the draw thread. The old version called ClearMessages straight from the draw thread, VACUUM included; it runs on a worker now. One line beyond the old layout sits above the clear button: how many messages are stored, and that exporting keeps a copy. Whoever is about to throw the history away should be told there is a way not to. The legacy Chat 2 files only get a block when they are actually on disk, and the advanced tools only appear when the section is expanded with Shift held. The message injector is not back: it was deleted with the tab and writing 10,000 fake messages into a user's real database is not something to rebuild on the way past. Retention: an "apply now" button, the running hint, and the last-run line, which v1.11.0 shipped as an English literal while both strings sat translated in all 25 languages. Plus reset-to-spec next to the existing clear-overrides, since the two answer different questions and both were already translated. Retention_Apply_Tooltip stays unused and gets a replacement. It ends with "Save your changes first", and the window it was written for had a Save button. Also here, found while wiring the manual trigger: DbOperationGate.End now takes the operation it releases. It used to reset blindly, on the reasoning that a worker must be able to release from a finally without knowing whether it acquired. That is backwards: a worker whose TryBegin was refused also runs its finally, and a blind reset there hands away the lock of whichever operation actually holds it. Worse than no gate, because the refused worker walks off believing it did nothing while a VACUUM starts under somebody's open reader.
This commit is contained in:
+59
-3
@@ -1010,6 +1010,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (DateTimeOffset.UtcNow - Config.RetentionLastRunAt < TimeSpan.FromHours(24))
|
||||
return;
|
||||
|
||||
StartRetentionSweep(notify: false);
|
||||
}
|
||||
|
||||
// Shared by the daily check above and the manual button in settings.
|
||||
//
|
||||
// notify: the unattended sweep stays quiet, because a notification for
|
||||
// something the user did not ask for at a moment they did not choose is
|
||||
// noise. A run they pressed a button for reports back.
|
||||
//
|
||||
// Returns false when the store is already busy, so the caller can say so
|
||||
// instead of leaving the user waiting for a run that never started.
|
||||
internal bool StartRetentionSweep(bool notify)
|
||||
{
|
||||
if (DbOperations.IsBusy)
|
||||
return false;
|
||||
|
||||
// Snapshot the policy so the user can edit settings while the sweep runs.
|
||||
var policy = new Dictionary<int, int>();
|
||||
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
|
||||
@@ -1023,11 +1039,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
var defaultDays = Config.RetentionDefaultDays;
|
||||
|
||||
_retentionSweepRunning = true;
|
||||
|
||||
// IsBackground = true so a stuck sweep never blocks plugin unload.
|
||||
new Thread(() =>
|
||||
var worker = new Thread(() =>
|
||||
{
|
||||
// Bails when anything else already owns the store, not only another
|
||||
// sweep: a user-triggered export or cleanup counts too.
|
||||
try
|
||||
{
|
||||
if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep))
|
||||
return;
|
||||
|
||||
@@ -1037,6 +1057,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
SaveConfig();
|
||||
|
||||
if (notify)
|
||||
Util.WrapperUtil.AddNotification(
|
||||
string.Format(Resources.HellionStrings.Retention_Success, deleted),
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Success
|
||||
);
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||
@@ -1070,19 +1096,49 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
Log.Information("Retention sweep ran, nothing expired.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DbOperations.End(Util.DbOperation.RetentionSweep);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Retention sweep failed");
|
||||
if (notify)
|
||||
Util.WrapperUtil.AddNotification(
|
||||
Resources.HellionStrings.Retention_Error,
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Error
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DbOperations.End();
|
||||
_retentionSweepRunning = false;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
}.Start();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
worker.Start();
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// The thread never ran, so nothing will clear the flag for us.
|
||||
_retentionSweepRunning = false;
|
||||
Log.Error(e, "Could not start the retention sweep thread");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read by the settings tab every frame so the manual button can say a run is
|
||||
// in progress. The gate itself cannot answer that: it goes busy only once
|
||||
// the worker reaches TryBegin, which is after Start returns.
|
||||
private volatile bool _retentionSweepRunning;
|
||||
|
||||
internal bool RetentionSweepRunning => _retentionSweepRunning;
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
|
||||
@@ -96,6 +96,7 @@ internal class HellionStrings
|
||||
internal static string Retention_Apply_Label => Get(nameof(Retention_Apply_Label));
|
||||
internal static string Retention_Apply_Tooltip => Get(nameof(Retention_Apply_Tooltip));
|
||||
internal static string Retention_Running => Get(nameof(Retention_Running));
|
||||
internal static string Retention_RunNow_Tooltip => Get(nameof(Retention_RunNow_Tooltip));
|
||||
internal static string Retention_LastRun_Never => Get(nameof(Retention_LastRun_Never));
|
||||
internal static string Retention_LastRun_At => Get(nameof(Retention_LastRun_At));
|
||||
internal static string Retention_Success => Get(nameof(Retention_Success));
|
||||
@@ -296,6 +297,7 @@ internal class HellionStrings
|
||||
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
|
||||
internal static string Settings_Database_Stats_Heading => Get(nameof(Settings_Database_Stats_Heading));
|
||||
internal static string Settings_Database_Busy => Get(nameof(Settings_Database_Busy));
|
||||
internal static string Settings_Database_ClearHint => Get(nameof(Settings_Database_ClearHint));
|
||||
internal static string Settings_Database_Op_RetentionSweep => Get(nameof(Settings_Database_Op_RetentionSweep));
|
||||
internal static string Settings_Database_Op_Export => Get(nameof(Settings_Database_Op_Export));
|
||||
internal static string Settings_Database_Op_Cleanup => Get(nameof(Settings_Database_Op_Cleanup));
|
||||
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>No hi ha cap canal seleccionat, així que una neteja esborraria tot l'historial. Tria els canals que vols conservar, o fes servir el botó d'esborrar si de debò ho vols eliminar tot.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Hi ha {0:N0} missatges desats. Si vols conservar-ne una còpia, exporta'ls abans d'esborrar.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Maj: executa la neteja de retenció ara mateix en comptes d'esperar el pas diari. Esborra els missatges més antics que els límits de dalt.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Není vybrán žádný kanál, takže úklid by smazal celou historii. Vyberte kanály, které chcete zachovat, nebo použijte tlačítko pro vymazání, pokud opravdu chcete smazat vše.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Uloženo je {0:N0} zpráv. Pokud si chcete ponechat kopii, před vymazáním je exportujte.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: spustí úklid podle doby uchování hned, místo čekání na denní běh. Smaže zprávy starší než limity výše.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Ingen kanal er valgt, så en oprydning ville slette hele historikken. Vælg de kanaler, du vil beholde, eller brug sletteknappen, hvis du virkelig vil af med alt.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Der er gemt {0:N0} beskeder. Vil du beholde en kopi, så eksportér dem før du sletter.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: kører oprydningen med det samme i stedet for at vente på det daglige gennemløb. Sletter beskeder ældre end grænserne ovenfor.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Es ist kein Kanal ausgewählt, eine Bereinigung würde also den gesamten Verlauf löschen. Wähle die Kanäle aus, die du behalten willst, oder nimm den Löschen-Knopf, wenn wirklich alles weg soll.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Es sind {0:N0} Nachrichten gespeichert. Wenn du eine Kopie behalten willst, exportiere sie vor dem Löschen.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Strg+Umschalt: führt den Aufbewahrungslauf sofort aus, statt auf den täglichen Durchlauf zu warten. Löscht Nachrichten, die älter sind als die Grenzen oben.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Δεν έχει επιλεγεί κανένα κανάλι, οπότε μια εκκαθάριση θα διέγραφε όλο το ιστορικό. Επιλέξτε τα κανάλια που θέλετε να κρατήσετε ή χρησιμοποιήστε το κουμπί διαγραφής αν θέλετε πραγματικά να φύγουν όλα.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Έχουν αποθηκευτεί {0:N0} μηνύματα. Αν θέλετε να κρατήσετε αντίγραφο, εξαγάγετέ τα πριν τη διαγραφή.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: εκτελεί την εκκαθάριση διατήρησης αμέσως, χωρίς να περιμένει το ημερήσιο πέρασμα. Διαγράφει μηνύματα παλαιότερα από τα παραπάνω όρια.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>No hay ningún canal seleccionado, así que una limpieza borraría todo el historial. Elige los canales que quieras conservar, o usa el botón de borrar si de verdad quieres eliminarlo todo.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Hay {0:N0} mensajes guardados. Si quieres conservar una copia, expórtalos antes de borrar.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Mayús: ejecuta la limpieza de retención ahora mismo en lugar de esperar al barrido diario. Borra los mensajes más antiguos que los límites de arriba.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Yhtään kanavaa ei ole valittu, joten siivous poistaisi koko historian. Valitse säilytettävät kanavat tai käytä tyhjennyspainiketta, jos haluat todella poistaa kaiken.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Tallennettuna on {0:N0} viestiä. Jos haluat säilyttää kopion, vie ne ennen tyhjennystä.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Vaihto: suorittaa säilytysajon heti sen sijaan, että odottaisi päivittäistä ajoa. Poistaa yllä olevia rajoja vanhemmat viestit.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Aucun canal n'est sélectionné, un nettoyage supprimerait donc tout l'historique. Choisissez les canaux à conserver, ou utilisez le bouton d'effacement si vous voulez vraiment tout supprimer.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} messages sont enregistrés. Si vous voulez en garder une copie, exportez-les avant d'effacer.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Maj : lance le nettoyage de rétention immédiatement au lieu d'attendre le passage quotidien. Supprime les messages plus anciens que les limites ci-dessus.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Nincs kiválasztva csatorna, így a takarítás a teljes előzményt törölné. Válaszd ki a megtartandó csatornákat, vagy használd a törlés gombot, ha tényleg mindent el akarsz távolítani.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} üzenet van elmentve. Ha meg akarsz tartani egy másolatot, törlés előtt exportáld őket.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: azonnal lefuttatja a megőrzési takarítást, nem várja meg a napi futást. Törli a fenti korlátoknál régebbi üzeneteket.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Nessun canale è selezionato, quindi una pulizia cancellerebbe l'intera cronologia. Scegli i canali da conservare, oppure usa il pulsante di cancellazione se vuoi davvero eliminare tutto.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Ci sono {0:N0} messaggi salvati. Se vuoi conservarne una copia, esportali prima di cancellare.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Maiusc: esegue subito la pulizia di conservazione invece di aspettare il passaggio giornaliero. Cancella i messaggi più vecchi dei limiti sopra.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>チャンネルが一つも選ばれていないため、クリーンアップは履歴をすべて削除します。残したいチャンネルを選ぶか、本当にすべて消したい場合は削除ボタンを使ってください。</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} 件のメッセージが保存されています。控えを残したい場合は、削除する前にエクスポートしてください。</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: 毎日の処理を待たずに保存期間の整理をすぐ実行します。上の期限より古いメッセージを削除します。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>선택된 채널이 없어 정리를 실행하면 기록 전체가 삭제됩니다. 남길 채널을 선택하거나, 정말 모두 지우려면 삭제 버튼을 사용하세요.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0}개의 메시지가 저장되어 있습니다. 사본을 남기려면 삭제하기 전에 내보내세요.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: 매일 실행을 기다리지 않고 보존 기간 정리를 지금 실행합니다. 위 기한보다 오래된 메시지를 삭제합니다.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Ingen kanal er valgt, så en opprydding ville slette hele historikken. Velg kanalene du vil beholde, eller bruk sletteknappen hvis du virkelig vil fjerne alt.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} meldinger er lagret. Vil du beholde en kopi, eksporter dem før du sletter.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: kjører oppryddingen med en gang i stedet for å vente på det daglige gjennomløpet. Sletter meldinger eldre enn grensene over.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Er is geen kanaal geselecteerd, dus een opschoning zou de hele geschiedenis wissen. Kies de kanalen die je wilt bewaren, of gebruik de wisknop als je echt alles kwijt wilt.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Er zijn {0:N0} berichten opgeslagen. Wil je een kopie houden, exporteer ze dan voordat je wist.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: voert de opschoning nu meteen uit in plaats van te wachten op de dagelijkse ronde. Wist berichten ouder dan de limieten hierboven.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Nie wybrano żadnego kanału, więc porządkowanie usunęłoby całą historię. Wybierz kanały, które chcesz zachować, albo użyj przycisku czyszczenia, jeśli naprawdę chcesz usunąć wszystko.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Zapisanych jest {0:N0} wiadomości. Jeśli chcesz zachować kopię, wyeksportuj je przed wyczyszczeniem.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: uruchamia porządkowanie od razu, zamiast czekać na codzienny przebieg. Usuwa wiadomości starsze niż limity powyżej.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Nenhum canal está selecionado, então uma limpeza apagaria todo o histórico. Escolha os canais que quer manter, ou use o botão de apagar se realmente quiser remover tudo.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Há {0:N0} mensagens salvas. Se quiser manter uma cópia, exporte-as antes de apagar.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: executa a limpeza de retenção agora em vez de esperar a varredura diária. Apaga mensagens mais antigas que os limites acima.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Não está selecionado nenhum canal, por isso uma limpeza apagaria todo o histórico. Escolha os canais que quer manter, ou use o botão de apagar se quiser mesmo remover tudo.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Estão guardadas {0:N0} mensagens. Se quiser manter uma cópia, exporte-as antes de apagar.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: executa a limpeza de retenção agora em vez de esperar a varredura diária. Apaga mensagens mais antigas do que os limites acima.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1198,4 +1198,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>No channel is selected, so a cleanup would delete the entire history. Pick the channels you want to keep, or use the clear button if you really want everything gone.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} messages are stored. If you want to keep a copy, export them before clearing.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: runs the retention cleanup right now instead of waiting for the daily sweep. Deletes messages older than the limits above.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Niciun canal nu este selectat, deci o curățare ar șterge tot istoricul. Alege canalele pe care vrei să le păstrezi sau folosește butonul de ștergere dacă chiar vrei să dispară tot.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Sunt salvate {0:N0} mesaje. Dacă vrei să păstrezi o copie, exportă-le înainte de ștergere.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: rulează curățarea acum, în loc să aștepte trecerea zilnică. Șterge mesajele mai vechi decât limitele de mai sus.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Не выбран ни один канал, поэтому очистка удалит всю историю. Выберите каналы, которые хотите сохранить, или воспользуйтесь кнопкой удаления, если действительно хотите стереть всё.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Сохранено {0:N0} сообщений. Если хотите оставить копию, экспортируйте их перед удалением.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: выполняет очистку по сроку хранения сразу, не дожидаясь ежедневного прохода. Удаляет сообщения старше указанных выше пределов.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Ingen kanal är vald, så en rensning skulle radera hela historiken. Välj de kanaler du vill behålla, eller använd raderingsknappen om du verkligen vill ta bort allt.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} meddelanden är sparade. Vill du behålla en kopia, exportera dem innan du raderar.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Skift: kör gallringen direkt i stället för att vänta på den dagliga körningen. Raderar meddelanden äldre än gränserna ovan.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Hiçbir kanal seçili değil, bu yüzden temizlik tüm geçmişi siler. Saklamak istediğiniz kanalları seçin veya gerçekten her şeyin gitmesini istiyorsanız silme düğmesini kullanın.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>{0:N0} ileti saklanıyor. Bir kopya tutmak istiyorsanız, silmeden önce dışa aktarın.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: günlük taramayı beklemeden saklama temizliğini hemen çalıştırır. Yukarıdaki sınırlardan eski iletileri siler.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1177,4 +1177,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Не вибрано жодного каналу, тому очищення видалить усю історію. Виберіть канали, які хочете зберегти, або скористайтеся кнопкою видалення, якщо справді хочете стерти все.</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>Збережено {0:N0} повідомлень. Якщо хочете залишити копію, експортуйте їх перед видаленням.</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift: виконує очищення за строком зберігання одразу, не чекаючи щоденного проходу. Видаляє повідомлення, старші за вказані вище межі.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>未选择任何频道,因此清理会删除全部历史记录。请选择要保留的频道,若确实想全部删除,请使用清空按钮。</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>已保存 {0:N0} 条消息。若想留一份副本,请在清空前先导出。</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift:立即执行保留期清理,无需等待每日运行。删除早于上方期限的消息。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1178,4 +1178,10 @@
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>未選擇任何頻道,因此清理會刪除全部歷史紀錄。請選擇要保留的頻道,若確實想全部刪除,請使用清除按鈕。</value>
|
||||
</data>
|
||||
<data name="Settings_Database_ClearHint" xml:space="preserve">
|
||||
<value>已保存 {0:N0} 則訊息。若想留一份副本,請在清除前先匯出。</value>
|
||||
</data>
|
||||
<data name="Retention_RunNow_Tooltip" xml:space="preserve">
|
||||
<value>Ctrl+Shift:立即執行保留期清理,無需等待每日執行。刪除早於上方期限的訊息。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -39,6 +39,21 @@ internal sealed class DataPrivacyTab
|
||||
private volatile bool _cleanupPreviewRunning;
|
||||
private volatile bool _cleanupRunning;
|
||||
|
||||
// Database metadata, refreshed at most every five seconds. MessageCount and
|
||||
// the file sizes are cheap on their own, but MessageCount takes the read
|
||||
// lock, and a VACUUM holds that for the length of a full file rewrite. Once
|
||||
// per frame would not be a stutter, it would be a still image.
|
||||
private long _dbRefreshedAt;
|
||||
private long _dbSize;
|
||||
private long _dbLogSize;
|
||||
private int _dbMessageCount;
|
||||
private volatile bool _clearRunning;
|
||||
|
||||
// Shift held while expanding the section. Keeps the developer tools out of
|
||||
// the way without a permanent switch, the way upstream did it.
|
||||
private bool _dbSectionWasOpen;
|
||||
private bool _dbShowAdvanced;
|
||||
|
||||
// What the preview was computed against. All three inputs decide the outcome,
|
||||
// so all three decide whether it is still valid.
|
||||
private sealed record CleanupPreview(
|
||||
@@ -102,17 +117,24 @@ internal sealed class DataPrivacyTab
|
||||
);
|
||||
|
||||
// RetentionLastRunAt defaults to MinValue on a fresh install, which
|
||||
// would render as "0001-01-01 00:00" and look like a bug; the "Never"
|
||||
// would render as "0001-01-01 00:00" and look like a bug; the "never"
|
||||
// sentinel handles that. Disabling the sweep does NOT reset the
|
||||
// timestamp — the historical last-run value is kept as informational
|
||||
// carry-over until the next sweep updates it.
|
||||
var lastRun =
|
||||
//
|
||||
// Both strings were already translated when v1.11.0 shipped this row
|
||||
// with an English literal.
|
||||
ImGui.TextDisabled(
|
||||
Plugin.Config.RetentionLastRunAt == DateTimeOffset.MinValue
|
||||
? "Never"
|
||||
: Plugin.Config.RetentionLastRunAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
|
||||
ImGui.TextDisabled($"Last run: {lastRun}");
|
||||
? HellionStrings.Retention_LastRun_Never
|
||||
: string.Format(
|
||||
HellionStrings.Retention_LastRun_At,
|
||||
Plugin.Config.RetentionLastRunAt.ToLocalTime()
|
||||
)
|
||||
);
|
||||
|
||||
DrawRetentionOverrides();
|
||||
DrawManualRetentionRun();
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -161,6 +183,17 @@ internal sealed class DataPrivacyTab
|
||||
DrawExportSection();
|
||||
}
|
||||
|
||||
var dbOpen = _w.Section(
|
||||
ImGui.GetID("privacy.database"u8),
|
||||
HellionStrings.Settings_Section_Database,
|
||||
open: false
|
||||
);
|
||||
if (dbOpen && !_dbSectionWasOpen)
|
||||
_dbShowAdvanced = ImGui.GetIO().KeyShift;
|
||||
_dbSectionWasOpen = dbOpen;
|
||||
if (dbOpen)
|
||||
DrawDatabaseSection();
|
||||
|
||||
if (_w.Section(ImGui.GetID("privacy.telemetry"u8), "Telemetry", open: false))
|
||||
{
|
||||
// Read-only placeholder; no telemetry is wired in v1.7.0. Do not
|
||||
@@ -204,7 +237,8 @@ internal sealed class DataPrivacyTab
|
||||
return;
|
||||
}
|
||||
|
||||
if (overrides.Count > 0 && ImGui.Button(HellionStrings.Retention_Clear_Overrides))
|
||||
var hasOverrides = overrides.Count > 0;
|
||||
if (hasOverrides && ImGui.Button(HellionStrings.Retention_Clear_Overrides))
|
||||
{
|
||||
// Same lock the sweep takes when it snapshots the policy, so a clear
|
||||
// cannot cut its enumeration short.
|
||||
@@ -214,6 +248,358 @@ internal sealed class DataPrivacyTab
|
||||
}
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
// SameLine only when there is a button to sit beside; otherwise it would
|
||||
// attach to the last channel row above.
|
||||
if (hasOverrides)
|
||||
ImGui.SameLine();
|
||||
|
||||
// Clearing drops every override and falls back to the global default;
|
||||
// this puts the per-channel spec values back instead. Two different
|
||||
// answers to "I have made a mess of this", and both were translated
|
||||
// before either had a button.
|
||||
if (ImGui.Button(HellionStrings.Retention_Reset_Button))
|
||||
{
|
||||
lock (_plugin.ConfigMapsLock)
|
||||
{
|
||||
overrides.Clear();
|
||||
foreach (var (type, days) in PrivacyDefaults.DefaultRetentionDays)
|
||||
overrides[type] = days;
|
||||
}
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(HellionStrings.Retention_Reset_Spec);
|
||||
}
|
||||
|
||||
// The daily sweep has run unattended since v1.4.8, but only ever on its own
|
||||
// schedule: change a limit and the effect lands up to 24 hours later, with
|
||||
// nothing on screen to say so. The lock this needed was already there,
|
||||
// waiting for a caller.
|
||||
private void DrawManualRetentionRun()
|
||||
{
|
||||
if (!Plugin.Config.RetentionEnabled)
|
||||
return;
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
var running = _plugin.RetentionSweepRunning;
|
||||
var current = _plugin.DbOperations.Current;
|
||||
|
||||
using (ImRaii.Disabled(running || current != DbOperation.None))
|
||||
{
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
HellionStrings.Retention_Apply_Label,
|
||||
HellionStrings.Retention_RunNow_Tooltip
|
||||
)
|
||||
)
|
||||
{
|
||||
// Refusal is reported here rather than from the worker: the
|
||||
// sweep bails silently by design, and a button that does nothing
|
||||
// without saying why is the thing this cycle exists to remove.
|
||||
if (!_plugin.StartRetentionSweep(notify: true))
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(
|
||||
HellionStrings.Settings_Database_Busy,
|
||||
OperationName(_plugin.DbOperations.Current)
|
||||
),
|
||||
NotificationType.Warning
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (running)
|
||||
ImGuiUtil.HelpText(HellionStrings.Retention_Running);
|
||||
}
|
||||
|
||||
private void DrawDatabaseSection()
|
||||
{
|
||||
RefreshDatabaseMetadata();
|
||||
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(Language.Options_Database_Metadata_Path, MessageManager.DatabasePath())
|
||||
);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
ImGui.SetClipboardText(Path.GetDirectoryName(MessageManager.DatabasePath()));
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Metadata_CopyConfigPathNotification,
|
||||
NotificationType.Info
|
||||
);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
|
||||
ImGuiUtil.Tooltip(Language.Options_Database_Metadata_CopyConfigPath);
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
Language.Options_Database_Metadata_Size,
|
||||
StringUtil.BytesToString(_dbSize)
|
||||
)
|
||||
);
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
Language.Options_Database_Metadata_LogSize,
|
||||
StringUtil.BytesToString(_dbLogSize)
|
||||
)
|
||||
);
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(Language.Options_Database_Metadata_MessageCount, _dbMessageCount)
|
||||
);
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
// Beyond the old layout: whoever is about to throw the history away
|
||||
// should see how much of it there is and be told, in the same breath,
|
||||
// that there is a way to keep a copy.
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(HellionStrings.Settings_Database_ClearHint, _dbMessageCount)
|
||||
);
|
||||
|
||||
var current = _plugin.DbOperations.Current;
|
||||
var busy = _clearRunning || current != DbOperation.None;
|
||||
|
||||
using (ImRaii.Disabled(busy))
|
||||
{
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
Language.Options_ClearDatabase_Button,
|
||||
Language.Options_ClearDatabase_Tooltip
|
||||
)
|
||||
)
|
||||
StartClear();
|
||||
}
|
||||
|
||||
if (!_clearRunning && current != DbOperation.None)
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(HellionStrings.Settings_Database_Busy, OperationName(current))
|
||||
);
|
||||
|
||||
DrawLegacyDatabaseBlock();
|
||||
|
||||
if (_dbShowAdvanced)
|
||||
DrawAdvancedDatabaseBlock(busy);
|
||||
}
|
||||
|
||||
// Suspended while a long-running operation owns the store: MessageCount
|
||||
// takes the read lock, and asking for it during a VACUUM means waiting for
|
||||
// the whole file to be rewritten -- on the draw thread.
|
||||
private void RefreshDatabaseMetadata()
|
||||
{
|
||||
if (_plugin.DbOperations.IsBusy || _clearRunning)
|
||||
return;
|
||||
|
||||
if (_dbRefreshedAt + 5_000 > Environment.TickCount64)
|
||||
return;
|
||||
|
||||
_dbSize = _plugin.MessageManager.Store.DatabaseSize();
|
||||
_dbLogSize = _plugin.MessageManager.Store.DatabaseLogSize();
|
||||
_dbMessageCount = _plugin.MessageManager.Store.MessageCount();
|
||||
_dbRefreshedAt = Environment.TickCount64;
|
||||
}
|
||||
|
||||
// The old version called ClearMessages straight from the draw thread, VACUUM
|
||||
// and all. On a large database that is a frozen game for as long as SQLite
|
||||
// needs to rewrite the file.
|
||||
private void StartClear()
|
||||
{
|
||||
if (_clearRunning)
|
||||
return;
|
||||
|
||||
_clearRunning = true;
|
||||
|
||||
var worker = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_plugin.DbOperations.TryBegin(DbOperation.Clear))
|
||||
{
|
||||
Notify(
|
||||
string.Format(
|
||||
HellionStrings.Settings_Database_Busy,
|
||||
OperationName(_plugin.DbOperations.Current)
|
||||
),
|
||||
NotificationType.Warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Clearing messages from database");
|
||||
_plugin.MessageManager.Store.ClearMessages();
|
||||
|
||||
if (
|
||||
!Plugin
|
||||
.Framework.Run(() => _plugin.MessageManager.ClearAllTabs())
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
_logger.LogWarning("Clear: framework refresh timed out after 5s.");
|
||||
}
|
||||
|
||||
Notify(Language.Options_ClearDatabase_Success, NotificationType.Info);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_plugin.DbOperations.End(DbOperation.Clear);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Clearing the database failed");
|
||||
Notify(Language.Options_ClearDatabase_Success, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Both the counters and any cleanup preview describe a database
|
||||
// that no longer exists.
|
||||
_dbRefreshedAt = 0;
|
||||
_cleanupPreview = null;
|
||||
_clearRunning = false;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "HellionChat Clear",
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
worker.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_clearRunning = false;
|
||||
_logger.LogError(e, "Could not start the clear thread");
|
||||
}
|
||||
}
|
||||
|
||||
// Chat 2 left these behind on migration. Drawn only when they are actually
|
||||
// on disk, so the block does not sit there permanently telling most users
|
||||
// about a file they have never had.
|
||||
private void DrawLegacyDatabaseBlock()
|
||||
{
|
||||
var dir = Plugin.Interface.ConfigDirectory.FullName;
|
||||
var old = new FileInfo(Path.Join(dir, "chat.db"));
|
||||
var migrated = new FileInfo(Path.Join(dir, "chat-litedb.db"));
|
||||
if (!old.Exists && !migrated.Exists)
|
||||
return;
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
|
||||
|
||||
if (
|
||||
!ImGuiUtil.CtrlShiftButton(
|
||||
Language.Options_Database_Old_Delete,
|
||||
Language.Options_Database_Old_Delete_Tooltip
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (old.Exists)
|
||||
old.Delete();
|
||||
if (migrated.Exists)
|
||||
migrated.Delete();
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Success,
|
||||
NotificationType.Success
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unable to delete old database");
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Error,
|
||||
NotificationType.Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Untranslated on purpose: these are developer tools, and the labels name
|
||||
// the methods they call.
|
||||
private void DrawAdvancedDatabaseBlock(bool busy)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
ImGui.TextUnformatted(Language.Options_Database_Advanced);
|
||||
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
|
||||
|
||||
using (ImRaii.Disabled(busy))
|
||||
{
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
"Perform maintenance",
|
||||
"Ctrl+Shift: VACUUM, REINDEX and ANALYZE. Runs in the background."
|
||||
)
|
||||
)
|
||||
StartMaintenance();
|
||||
}
|
||||
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
"Reload messages from database",
|
||||
"Ctrl+Shift: MessageManager.FilterAllTabsAsync()"
|
||||
)
|
||||
)
|
||||
{
|
||||
_plugin.MessageManager.ClearAllTabs();
|
||||
_plugin.MessageManager.FilterAllTabsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartMaintenance()
|
||||
{
|
||||
var worker = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_plugin.DbOperations.TryBegin(DbOperation.Cleanup))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_plugin.MessageManager.Store.PerformMaintenance();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_plugin.DbOperations.End(DbOperation.Cleanup);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Manual maintenance failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbRefreshedAt = 0;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "HellionChat Maintenance",
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
worker.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not start the maintenance thread");
|
||||
}
|
||||
}
|
||||
|
||||
// The privacy filter only decides what gets written from now on. Everything
|
||||
@@ -483,7 +869,7 @@ internal sealed class DataPrivacyTab
|
||||
}
|
||||
finally
|
||||
{
|
||||
_plugin.DbOperations.End();
|
||||
_plugin.DbOperations.End(DbOperation.Cleanup);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -742,7 +1128,7 @@ internal sealed class DataPrivacyTab
|
||||
}
|
||||
finally
|
||||
{
|
||||
_plugin.DbOperations.End();
|
||||
_plugin.DbOperations.End(DbOperation.Export);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -62,13 +62,17 @@ internal sealed class DbOperationGate
|
||||
}
|
||||
}
|
||||
|
||||
// Idempotent, and deliberately not checking which operation ends. A worker
|
||||
// that throws must still be able to release from a finally block without
|
||||
// knowing whether it ever acquired.
|
||||
internal void End()
|
||||
// Releases only what the caller acquired. Idempotent for that caller, and a
|
||||
// no-op for anyone else -- a worker whose TryBegin was refused still runs its
|
||||
// finally, and a blind reset there would hand away the lock of whichever
|
||||
// operation actually holds it. That is worse than no gate: the refused
|
||||
// worker walks away believing it did nothing while a VACUUM starts under
|
||||
// somebody's open reader.
|
||||
internal void End(DbOperation operation)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_current == operation)
|
||||
_current = DbOperation.None;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user