diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index ba894ff..aa4bfaa 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -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(); foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays) @@ -1023,67 +1039,107 @@ 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. - if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep)) - return; - try { - var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays); - Config.RetentionLastRunAt = DateTimeOffset.UtcNow; - SaveConfig(); + if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep)) + return; - if (deleted > 0) + try { - Log.Information($"Retention sweep deleted {deleted} expired messages."); - // Schedule on the next framework tick to avoid the ~194ms - // hitch from blocking with .Wait() while the frame finishes. - // The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is - // now guarded by the shared Plugin.TabsListLock (B3), so this - // tick scheduling is purely hitch-avoidance, not safety. - // Pattern reference: SimpleTweaks - // Tweaks/Chat/CaseInsensitiveCommands.cs:45. - Framework.RunOnTick(() => + var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays); + 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) { - // The retention thread is IsBackground=true so plugin - // unload can fire while a scheduled tick is still - // pending; bail before touching anything torn down. - if (_isDisposing) - return; - try + Log.Information($"Retention sweep deleted {deleted} expired messages."); + // Schedule on the next framework tick to avoid the ~194ms + // hitch from blocking with .Wait() while the frame finishes. + // The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is + // now guarded by the shared Plugin.TabsListLock (B3), so this + // tick scheduling is purely hitch-avoidance, not safety. + // Pattern reference: SimpleTweaks + // Tweaks/Chat/CaseInsensitiveCommands.cs:45. + Framework.RunOnTick(() => { - MessageManager.ClearAllTabs(); - MessageManager.FilterAllTabs(); - } - catch (Exception ex) - { - Log.Error(ex, "Retention sweep clear+refilter failed"); - } - }); + // The retention thread is IsBackground=true so plugin + // unload can fire while a scheduled tick is still + // pending; bail before touching anything torn down. + if (_isDisposing) + return; + try + { + MessageManager.ClearAllTabs(); + MessageManager.FilterAllTabs(); + } + catch (Exception ex) + { + Log.Error(ex, "Retention sweep clear+refilter failed"); + } + }); + } + else + { + Log.Information("Retention sweep ran, nothing expired."); + } } - else + finally { - Log.Information("Retention sweep ran, nothing expired."); + 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() { // v1.9.0 B5: time the whole handler (style + font prologue included). diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 2ca6de7..ca7653a 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -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)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index 2095e6f..70f5adc 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -1178,4 +1178,10 @@ 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. + + Hi ha {0:N0} missatges desats. Si vols conservar-ne una còpia, exporta'ls abans d'esborrar. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index 42a7fdf..58c1546 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -1177,4 +1177,10 @@ 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. + + Uloženo je {0:N0} zpráv. Pokud si chcete ponechat kopii, před vymazáním je exportujte. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index f4bd284..777e968 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -1177,4 +1177,10 @@ 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. + + Der er gemt {0:N0} beskeder. Vil du beholde en kopi, så eksportér dem før du sletter. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 54c4595..ece9b51 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -1172,4 +1172,10 @@ 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. + + Es sind {0:N0} Nachrichten gespeichert. Wenn du eine Kopie behalten willst, exportiere sie vor dem Löschen. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index af91bc3..51a6550 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -1177,4 +1177,10 @@ Δεν έχει επιλεγεί κανένα κανάλι, οπότε μια εκκαθάριση θα διέγραφε όλο το ιστορικό. Επιλέξτε τα κανάλια που θέλετε να κρατήσετε ή χρησιμοποιήστε το κουμπί διαγραφής αν θέλετε πραγματικά να φύγουν όλα. + + Έχουν αποθηκευτεί {0:N0} μηνύματα. Αν θέλετε να κρατήσετε αντίγραφο, εξαγάγετέ τα πριν τη διαγραφή. + + + Ctrl+Shift: εκτελεί την εκκαθάριση διατήρησης αμέσως, χωρίς να περιμένει το ημερήσιο πέρασμα. Διαγράφει μηνύματα παλαιότερα από τα παραπάνω όρια. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index 1ff51f0..89660e9 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -1178,4 +1178,10 @@ 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. + + Hay {0:N0} mensajes guardados. Si quieres conservar una copia, expórtalos antes de borrar. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index b9a4d51..743a5ac 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -1177,4 +1177,10 @@ 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. + + Tallennettuna on {0:N0} viestiä. Jos haluat säilyttää kopion, vie ne ennen tyhjennystä. + + + Ctrl+Vaihto: suorittaa säilytysajon heti sen sijaan, että odottaisi päivittäistä ajoa. Poistaa yllä olevia rajoja vanhemmat viestit. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index 0442588..e95fa64 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -1178,4 +1178,10 @@ 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. + + {0:N0} messages sont enregistrés. Si vous voulez en garder une copie, exportez-les avant d'effacer. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index 083c79b..289949b 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -1177,4 +1177,10 @@ 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. + + {0:N0} üzenet van elmentve. Ha meg akarsz tartani egy másolatot, törlés előtt exportáld őket. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index 89b963a..254ee3f 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -1178,4 +1178,10 @@ 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. + + Ci sono {0:N0} messaggi salvati. Se vuoi conservarne una copia, esportali prima di cancellare. + + + Ctrl+Maiusc: esegue subito la pulizia di conservazione invece di aspettare il passaggio giornaliero. Cancella i messaggi più vecchi dei limiti sopra. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index e1329e1..ed0858a 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -1178,4 +1178,10 @@ チャンネルが一つも選ばれていないため、クリーンアップは履歴をすべて削除します。残したいチャンネルを選ぶか、本当にすべて消したい場合は削除ボタンを使ってください。 + + {0:N0} 件のメッセージが保存されています。控えを残したい場合は、削除する前にエクスポートしてください。 + + + Ctrl+Shift: 毎日の処理を待たずに保存期間の整理をすぐ実行します。上の期限より古いメッセージを削除します。 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index 3344e1f..52c99fe 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -1178,4 +1178,10 @@ 선택된 채널이 없어 정리를 실행하면 기록 전체가 삭제됩니다. 남길 채널을 선택하거나, 정말 모두 지우려면 삭제 버튼을 사용하세요. + + {0:N0}개의 메시지가 저장되어 있습니다. 사본을 남기려면 삭제하기 전에 내보내세요. + + + Ctrl+Shift: 매일 실행을 기다리지 않고 보존 기간 정리를 지금 실행합니다. 위 기한보다 오래된 메시지를 삭제합니다. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index 4d7cb89..27f37ac 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -1177,4 +1177,10 @@ 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. + + {0:N0} meldinger er lagret. Vil du beholde en kopi, eksporter dem før du sletter. + + + Ctrl+Shift: kjører oppryddingen med en gang i stedet for å vente på det daglige gjennomløpet. Sletter meldinger eldre enn grensene over. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index 64c7335..763cb82 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -1178,4 +1178,10 @@ 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. + + Er zijn {0:N0} berichten opgeslagen. Wil je een kopie houden, exporteer ze dan voordat je wist. + + + Ctrl+Shift: voert de opschoning nu meteen uit in plaats van te wachten op de dagelijkse ronde. Wist berichten ouder dan de limieten hierboven. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index ea72b2c..d719b9a 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -1177,4 +1177,10 @@ 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. + + Zapisanych jest {0:N0} wiadomości. Jeśli chcesz zachować kopię, wyeksportuj je przed wyczyszczeniem. + + + Ctrl+Shift: uruchamia porządkowanie od razu, zamiast czekać na codzienny przebieg. Usuwa wiadomości starsze niż limity powyżej. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index dd698da..d2e5b25 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -1178,4 +1178,10 @@ 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. + + Há {0:N0} mensagens salvas. Se quiser manter uma cópia, exporte-as antes de apagar. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 10b880a..34b4aae 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -1177,4 +1177,10 @@ 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. + + Estão guardadas {0:N0} mensagens. Se quiser manter uma cópia, exporte-as antes de apagar. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index aa47fe1..9a9583d 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1198,4 +1198,10 @@ 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. + + {0:N0} messages are stored. If you want to keep a copy, export them before clearing. + + + Ctrl+Shift: runs the retention cleanup right now instead of waiting for the daily sweep. Deletes messages older than the limits above. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index 6fc0885..255a0d8 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -1178,4 +1178,10 @@ 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. + + Sunt salvate {0:N0} mesaje. Dacă vrei să păstrezi o copie, exportă-le înainte de ștergere. + + + Ctrl+Shift: rulează curățarea acum, în loc să aștepte trecerea zilnică. Șterge mesajele mai vechi decât limitele de mai sus. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index 597df69..5fd3563 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -1178,4 +1178,10 @@ Не выбран ни один канал, поэтому очистка удалит всю историю. Выберите каналы, которые хотите сохранить, или воспользуйтесь кнопкой удаления, если действительно хотите стереть всё. + + Сохранено {0:N0} сообщений. Если хотите оставить копию, экспортируйте их перед удалением. + + + Ctrl+Shift: выполняет очистку по сроку хранения сразу, не дожидаясь ежедневного прохода. Удаляет сообщения старше указанных выше пределов. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index ccd76bc..4c3e621 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -1178,4 +1178,10 @@ 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. + + {0:N0} meddelanden är sparade. Vill du behålla en kopia, exportera dem innan du raderar. + + + 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. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index 9ad83af..15de60d 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -1177,4 +1177,10 @@ 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. + + {0:N0} ileti saklanıyor. Bir kopya tutmak istiyorsanız, silmeden önce dışa aktarın. + + + Ctrl+Shift: günlük taramayı beklemeden saklama temizliğini hemen çalıştırır. Yukarıdaki sınırlardan eski iletileri siler. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 2c74872..6c42f66 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -1177,4 +1177,10 @@ Не вибрано жодного каналу, тому очищення видалить усю історію. Виберіть канали, які хочете зберегти, або скористайтеся кнопкою видалення, якщо справді хочете стерти все. + + Збережено {0:N0} повідомлень. Якщо хочете залишити копію, експортуйте їх перед видаленням. + + + Ctrl+Shift: виконує очищення за строком зберігання одразу, не чекаючи щоденного проходу. Видаляє повідомлення, старші за вказані вище межі. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index 9d5a775..8681759 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -1178,4 +1178,10 @@ 未选择任何频道,因此清理会删除全部历史记录。请选择要保留的频道,若确实想全部删除,请使用清空按钮。 + + 已保存 {0:N0} 条消息。若想留一份副本,请在清空前先导出。 + + + Ctrl+Shift:立即执行保留期清理,无需等待每日运行。删除早于上方期限的消息。 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index 64a1eb1..2091c3d 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -1178,4 +1178,10 @@ 未選擇任何頻道,因此清理會刪除全部歷史紀錄。請選擇要保留的頻道,若確實想全部刪除,請使用清除按鈕。 + + 已保存 {0:N0} 則訊息。若想留一份副本,請在清除前先匯出。 + + + Ctrl+Shift:立即執行保留期清理,無需等待每日執行。刪除早於上方期限的訊息。 + \ No newline at end of file diff --git a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs index c1f1988..8b459f1 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs @@ -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) diff --git a/HellionChat/Util/DbOperationGate.cs b/HellionChat/Util/DbOperationGate.cs index 744f354..3f6cab6 100644 --- a/HellionChat/Util/DbOperationGate.cs +++ b/HellionChat/Util/DbOperationGate.cs @@ -62,14 +62,18 @@ 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) { - _current = DbOperation.None; + if (_current == operation) + _current = DbOperation.None; } } }