diff --git a/.gitignore b/.gitignore index ce89292..038b31c 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,12 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# Classic source remains private. +# Only runtime translations are published. +/FluentCleaner.Classic/* +!/FluentCleaner.Classic/Localization/ +!/FluentCleaner.Classic/Localization/*.json +!/FluentCleaner.Classic/TRANSLATING_CLASSIC.md + +/desktop.ini diff --git a/FluentCleaner.Classic/Localization/bg.json b/FluentCleaner.Classic/Localization/bg.json new file mode 100644 index 0000000..76eb0c3 --- /dev/null +++ b/FluentCleaner.Classic/Localization/bg.json @@ -0,0 +1,205 @@ +{ + "_Meta_LanguageDisplayName": "Bulgarian", + "_Meta_TranslatorName": "Руси Димитров", + "_Meta_TranslatorWebsite": "https://github.com/rddim", + "Options_NavBasic": "Настройки", + "Options_TitleBasic": "Настройки", + "Options_SubtitleBasic": "Основни настройки за управление на работата на FluentCleaner", + "BasicSettings_LblLanguage": "Език:", + "BasicSettings_LnkOpenLocalization": "Отваряне на папката с преводите", + "BasicSettings_OpenLocalizationErrorTitle": "Папката не може да бъде отворена", + "BasicSettings_OpenLocalizationError": "Папката с преводите не можа да бъде отворена: {0}", + "BasicSettings_ChkWinapp2": "Winapp2.ini (вграден)", + "BasicSettings_ChkWinapp3": "Winapp3.ini", + "BasicSettings_ChkCustomDb": "Потребителска база данни", + "BasicSettings_BtnBrowse": "Преглед...", + "BasicSettings_BtnReload": "Презареждане", + "BasicSettings_BtnUpdate": "Обновяване...", + "BasicSettings_NotDownloaded": "Все още не е изтеглено", + "BasicSettings_LastUpdated": "Последно обновяване: {0:g}", + "BasicSettings_ReloadTitle": "Презареждане", + "BasicSettings_ReloadMessage": "Базите данни ще бъдат презаредени при следващото зареждане на списъка с елементи в изгледа \"Почистване\". Рестартирайте приложението, за да приложите промените веднага.", + "BasicSettings_UpdateTitle": "Обновяване", + "BasicSettings_UpdateWinapp2Done": "Winapp2.ini беше обновен.", + "BasicSettings_UpdateWinapp3Done": "Winapp3.ini беше обновен.", + "BasicSettings_UpdateFailed": "Неуспешно обновяване: {0}", + "BasicSettings_BrowseDialogFilter": "База данни Winapp2 (*.ini)|*.ini|Всички файлове (*.*)|*.*", + "BasicSettings_BrowseDialogTitle": "Избор на потребителска база данни за почистване", + "BasicSettings_RestartTitle": "Необходим е рестарт", + "BasicSettings_RestartMessage": "Промяната на езика ще влезе в сила след рестартиране на FluentCleaner. Да се рестартира ли сега?", + "BasicSettings_LangSystem": "Стандартен за системата", + "BasicSettings_LangEnglish": "Английски", + "BasicSettings_LangGerman": "Немски", + "Main_NavCleaner": "Почистване", + "Main_NavCustom": "Персонални", + "Main_NavOptions": "Настройки", + "Main_LnkOnlineHelp": "Онлайн помощ", + "Main_LnkCheckUpdates": "Проверка за обновления...", + "Main_GatheringInfo": "Събиране на системна информация...", + "Main_HardwareSpecPlaceholder": "Хардуерна конфигурация", + "Options_NavExclusions": "Изключения", + "Options_NavPostClean": "Задачи", + "Options_NavHistory": "История", + "Options_NavAi": "ИИ", + "Options_NavScheduler": "Планировчик", + "Options_NavAbout": "Относно", + "Options_TitleExclusions": "Изключения", + "Options_SubtitleExclusions": "Пътищата, изброени тук, никога не се почистват — това има приоритет пред всички правила в базата данни", + "Options_TitlePostClean": "Задачи след почистване", + "Options_SubtitlePostClean": "По една команда на ред — изпълняват се последователно след всяко почистване", + "Options_TitleHistory": "История на почистването", + "Options_SubtitleHistory": "Проследяване на натрупването на ненужни файлове с времето", + "Options_TitleAi": "ИИ обяснения", + "Options_SubtitleAi": "Обяснение на всеки запис с помощта на ИИ — изисква ваш собствен безплатен Groq API ключ (по желание, предварителна версия)", + "Options_TitleScheduler": "Планировчик", + "Options_SubtitleScheduler": "Автоматично изпълнение на почистването по график чрез Windows Task Scheduler", + "Options_TitleAbout": "Относно", + "Options_DonationTitle": "FluentCleaner става проект с отворен код", + "Options_DonationMessage": "Вложих много труд в този проект — ако ви е полезен и искате да покажете благодарността си, едно дарение би означавало много.", + "Options_DonationSupport": "Подкрепа за проекта", + "Options_DonationLater": "Може би по-късно", + "Common_Cancel": "Отказ", + "Common_RunCleanerTitle": "Почистване", + "Common_File": "Файл", + "Common_Registry": "Системен регистър", + "Cleaner_BtnRun": "Почистване", + "Cleaner_BtnAnalyze": "Анализ", + "Cleaner_TabWindows": "Windows", + "Cleaner_TabApplications": "Приложения", + "Cleaner_MiCheckAll": "Маркиране на всички", + "Cleaner_MiUncheckAll": "Размаркиране на всички", + "Cleaner_MiAnalyze": "Анализ", + "Cleaner_MiClean": "Почистване", + "Cleaner_MiExplain": "Обяснение с ИИ", + "Cleaner_MiShowSource": "Показване на източника", + "Cleaner_MiRestoreDefault": "Възстановяване на настройките по подразбиране", + "Cleaner_MiOpenFolder": "Отваряне на папката", + "Cleaner_MiExcludeFile": "Изключване на този файл", + "Cleaner_MiExcludeFolder": "Изключване на тази папка", + "Cleaner_ColType": "Тип", + "Cleaner_ColPath": "Път", + "Cleaner_ColEntry": "Запис", + "Cleaner_ColFiles": "Файлове", + "Cleaner_ColRegistry": "Системен регистър", + "Cleaner_ColSize": "Размер", + "Cleaner_BtnBack": "\u003c Обобщени резултати", + "Cleaner_BtnDelete": "Изтриване", + "Cleaner_Ready": "Готово", + "Cleaner_LoadingDb": "Зареждане на базата данни...", + "Cleaner_AnalysisReady": "Анализът завърши — открити са {0} приложения в {1} записа.", + "Cleaner_LoadFailed": "Неуспешно зареждане на базата данни: {0}", + "Cleaner_ConfirmDelete": "Да бъдат ли изтрити {0} файла и {1} записа в регистъра за \"{2}\" ({3})?", + "Cleaner_NothingToClean": "Няма нищо за почистване", + "Cleaner_NothingSelected": "Няма избрани елементи", + "Cleaner_RunAnalyzeFirst": "Първо стартирайте анализа", + "Cleaner_ConfirmCleanAll": "Това ще изтрие безвъзвратно файловете и записите в регистъра, открити по време на анализа. Желаете ли да продължите?", + "Cleaner_MultipleEntries": "{0} записа", + "Cleaner_Analyzing": "Анализиране на {0}...", + "Cleaner_Cleaning": "Почистване на {0}...", + "Cleaner_AnalysisCancelled": "АНАЛИЗЪТ Е ОТМЕНЕН", + "Cleaner_AnalysisComplete": "АНАЛИЗЪТ Е ЗАВЪРШЕН", + "Cleaner_ToBeRemoved": "{0} за изтриване — {1} файла, {2} записа в системния регистър", + "Cleaner_DetailsHeader": "Подробности за файловете, които ще бъдат изтрити (Забележка: Все още няма изтрити файлове)", + "Cleaner_Cancelled": "Отменено", + "Cleaner_Done": "Готово", + "Cleaner_DoneStatus": "{0} — премахнати {1} елемента, освободени {2}", + "Cleaner_AddedExclusion": "Добавено към глобалните изключения", + "Custom_ColName": "Име", + "Custom_ColStatus": "Състояние", + "Custom_StatusEnabled": "Активирано", + "Custom_StatusDisabled": "Деактивирано", + "Custom_MiEdit": "Редактиране...", + "Custom_MiTestLab": "Тестване в \"Лаборатория за правила\".", + "Custom_MiDelete": "Изтриване", + "Custom_BtnNew": "Нов...", + "Custom_BtnDelete": "Изтриване", + "Custom_Hint": "Маркирайте правило, за да го активирате. То ще се появи веднага в раздел \"Почистване\".", + "Custom_ConfirmDeleteTitle": "Потребителско правило", + "Custom_ConfirmDeleteMessage": "Да се изтрие ли \"{0}\"?", + "About_Version": "Версия {0} (Classic)", + "About_PortableSuffix": " · Преносим режим", + "About_Description": "Самостоятелна WinForms версия на FluentCleaner, вдъхновена от класическия CCleaner. Използва същия механизъм за почистване и базата данни Winapp2.ini като оригиналното приложение, без зависимост от WinUI или Fluent.", + "About_TranslatedBy": "Превод: {0}", + "About_LnkGitHub": "GitHub", + "About_LnkIssues": "Докладване на проблем", + "About_LnkFaq": "Често задавани въпроси", + "Exclusions_BtnRemove": "Премахване", + "Exclusions_BtnAdd": "Добавяне", + "Exclusions_ChkEnabled": "Активиране на глобалните изключения", + "Exclusions_LnkProtectedPaths": "Показване на защитените пътища", + "Exclusions_ProtectedPathsDesc": "Тези пътища винаги се пропускат независимо какво е записано в базата данни. Те съдържат данни на разширения за браузъри, без които някои приложения (мениджъри на пароли, блокиращи реклами и др.) може да не работят правилно.", + "Exclusions_ProtectedPathsTitle": "Защитени пътища", + "PostClean_ChkEnabled": "Изпълнение на команди след почистване", + "History_ChkTrack": "Записване на историята на почистването", + "History_Summary": "{0} почиствания · общо освободени {1}", + "History_ColDate": "Дата", + "History_ColFreed": "Освободено", + "History_ColItems": "Елементи", + "History_BtnClear": "Изчистване", + "Ai_LnkDiscussion": "Имате мнение? Включете се в дискусията →", + "Ai_LnkGetKeyOpenAi": "Получаване на ключ (platform.openai.com/api-keys)", + "Ai_LnkGetKeyAnthropic": "Получаване на ключ (console.anthropic.com/settings/keys)", + "Ai_LnkGetKeyGroq": "Получаване на безплатен ключ (console.groq.com)", + "Ai_BtnTest": "Тест", + "Ai_LblApiKeyOpenAi": "OpenAI API ключ", + "Ai_LblApiKeyAnthropic": "Anthropic API ключ", + "Ai_LblApiKeyGroq": "Groq API ключ", + "Ai_LblProvider": "ИИ услуга", + "Ai_LblDesc": "Щракнете с десния бутон върху запис в раздел \"Почистване\" и изберете \"Обяснение с ИИ\", за да получите разбираемо описание. Изберете ИИ услуга и въведете своя API ключ. Той се изпраща само когато изрично поискате обяснение.", + "Ai_EnterKeyFirst": "Първо въведете API ключ.", + "Ai_Testing": "Тестване...", + "Scheduler_LnkOpen": "Отваряне на Windows Task Scheduler", + "Scheduler_BtnApply": "Прилагане", + "Scheduler_ChkShutdown": "Изключване на компютъра след приключване", + "Scheduler_LblTime": "Час", + "Scheduler_FreqDaily": "Всеки ден", + "Scheduler_FreqWeekly": "Всяка седмица (понеделник)", + "Scheduler_FreqLogon": "При влизане в системата", + "Scheduler_LblFrequency": "Честота", + "Scheduler_ChkEnabled": "Активиране на планирано почистване", + "Scheduler_CheckingStatus": "Проверка на състоянието...", + "Scheduler_LblDesc": "Изпълнява същото почистване като командния параметър /AUTO по зададен график чрез Windows Task Scheduler. Изпълнява се само когато сте влезли в системата – не са необходими администраторски права.", + "Scheduler_StatusActive": "✓ Планираната задача е активна.", + "Scheduler_StatusInactive": "✗ Не е създадена планирана задача.", + "CustomDialog_TitleEdit": "Редактиране на потребителско правило", + "CustomDialog_TitleNew": "Ново потребителско правило", + "CustomDialog_LblName": "Име:", + "CustomDialog_BtnTemplate": "Зареждане на шаблон", + "CustomDialog_BtnSave": "Запазване", + "CustomDialog_BtnCancel": "Отказ", + "CustomDialog_Title": "Потребителско правило", + "CustomDialog_NameRequired": "Първо въведете име на правилото.", + "Explain_BtnClose": "Затваряне", + "Explain_Thinking": "Мисли...", + "Explain_Title": "Обяснение с ИИ", + "RuleLab_Title": "Лаборатория за правила – {0}", + "RuleLab_TitleBase": "Лаборатория за правила", + "RuleLab_BtnDryRun": "Пробен анализ", + "RuleLab_BtnCancel": "Отказ", + "RuleLab_Hint": "Редактирайте правилата и използвайте \"Пробен анализ\", за да видите кои файлове ще бъдат открити.", + "RuleLab_Scanning": "Сканиране...", + "RuleLab_NoEntry": "В правилата не е намерен запис за почистване.", + "RuleLab_MoreFiles": "... и още {0} файла", + "RuleLab_Summary": "{0} файла, {1}", + "RuleLab_Cancelled": "Пробният анализ е отменен.", + "RuleLab_Error": "Грешка: {0}", + "RuleLab_BtnCopy": "Копиране", + "RuleLab_BtnReport": "Докладване", + "RuleLab_BtnClose": "Затваряне", + "CleanGuard_BrowsersTitle": "Браузърите са стартирани", + "CleanGuard_BrowsersMessage": "Следните браузъри все още работят:\n\n{0}\n\nЗатворете ги, за да могат данните им да бъдат почистени напълно. Желаете ли да продължите въпреки това?", + "CleanGuard_WarningsTitle": "Предупреждение", + "CleanGuard_WarningsMessage": "Някои от избраните елементи имат предупреждение:\n\n{0}\n\nЖелаете ли да продължите въпреки това?", + "Scheduler_ErrorExecutablePath": "Не можа да бъде определен пътят до изпълнимия файл на FluentCleaner.", + "Scheduler_ResultCreated": "Планираната задача беше създадена.", + "Scheduler_ResultNothingToRemove": "Няма нищо за премахване.", + "Scheduler_ResultRemoved": "Планираната задача беше премахната.", + "Scheduler_ErrorSchtasks": "Windows Task Scheduler върна грешка.", + "Cleaning_DeletedFile": "Изтрит файл: {0}", + "Cleaning_DeletedRegistry": "Изтрит запис в регистъра: {0}", + "Ai_NoApiKey": "Няма конфигуриран {0} API ключ. Добавяне от Настройки → ИИ.", + "Ai_ApiError": "Грешка от {0} API: {1}", + "Ai_UnknownError": "Неизвестна грешка", + "Ai_NoResponse": "Не беше получен отговор.", + "Ai_NetworkError": "Неуспешна връзка с API на {0}: {1}" +} diff --git a/FluentCleaner.Classic/Localization/de.json b/FluentCleaner.Classic/Localization/de.json new file mode 100644 index 0000000..5f6681a --- /dev/null +++ b/FluentCleaner.Classic/Localization/de.json @@ -0,0 +1,206 @@ +{ + "_Meta_LanguageDisplayName": "Deutsch", + "_Meta_TranslatorName": "", + "_Meta_TranslatorWebsite": "", + "Options_NavBasic": "Einstellungen", + "Options_TitleBasic": "Einstellungen", + "Options_SubtitleBasic": "Grundeinstellungen zur Steuerung der FluentCleaner-Funktionen", + "BasicSettings_LblLanguage": "Sprache:", + "BasicSettings_LnkOpenLocalization": "Lokalisierungsordner öffnen", + "BasicSettings_OpenLocalizationErrorTitle": "Ordner konnte nicht geöffnet werden", + "BasicSettings_OpenLocalizationError": "Der Lokalisierungsordner konnte nicht geöffnet werden: {0}", + "BasicSettings_ChkWinapp2": "Winapp2.ini (integriert)", + "BasicSettings_ChkWinapp3": "Winapp3.ini", + "BasicSettings_ChkCustomDb": "Eigene Datenbank", + "BasicSettings_BtnBrowse": "Wählen...", + "BasicSettings_BtnReload": "Neu laden", + "BasicSettings_BtnUpdate": "Update", + "BasicSettings_NotDownloaded": "Noch nicht heruntergeladen", + "BasicSettings_LastUpdated": "Zuletzt aktualisiert am {0:g}", + "BasicSettings_ReloadTitle": "Neu laden", + "BasicSettings_ReloadMessage": "Die Datenbanken werden geladen, sobald die Cleaner-Ansicht das nächste Mal Einträge lädt. Starte die App neu, um dies sofort anzuwenden.", + "BasicSettings_UpdateTitle": "Aktualisieren", + "BasicSettings_UpdateWinapp2Done": "Winapp2.ini aktualisiert.", + "BasicSettings_UpdateWinapp3Done": "Winapp3.ini aktualisiert.", + "BasicSettings_UpdateFailed": "Aktualisierung fehlgeschlagen: {0}", + "BasicSettings_BrowseDialogFilter": "Winapp2-Datenbank (*.ini)|*.ini|Alle Dateien (*.*)|*.*", + "BasicSettings_BrowseDialogTitle": "Eigene Cleaner-Datenbank auswählen", + "BasicSettings_RestartTitle": "Neustart erforderlich", + "BasicSettings_RestartMessage": "Die Sprachänderung wird erst nach einem Neustart von FluentCleaner wirksam. Jetzt neu starten?", + "BasicSettings_LangSystem": "Systemstandard", + "BasicSettings_LangEnglish": "Englisch", + "BasicSettings_LangGerman": "Deutsch", + "Main_NavCleaner": "Cleaner", + "Main_NavCustom": "Extras", + "Main_NavOptions": "Optionen", + "Main_LnkOnlineHelp": "Online-Hilfe", + "Main_LnkCheckUpdates": "Nach Updates suchen...", + "Main_GatheringInfo": "Systeminformationen werden gesammelt…", + "Main_HardwareSpecPlaceholder": "Hardware-Spezifikation", + "Options_NavExclusions": "Ausnahmen", + "Options_NavPostClean": "Aufgaben", + "Options_NavHistory": "Verlauf", + "Options_NavAi": "KI", + "Options_NavScheduler": "Zeitplan", + "Options_NavAbout": "Über", + "Options_TitleExclusions": "Ausnahmen", + "Options_SubtitleExclusions": "Hier gelistete Pfade werden nie gereinigt — überschreibt alle Datenbank-Regeln", + "Options_TitlePostClean": "Aufgaben nach der Reinigung", + "Options_SubtitlePostClean": "Ein Befehl pro Zeile — läuft nacheinander nach jedem Reinigungsvorgang", + "Options_TitleHistory": "Reinigungsverlauf", + "Options_SubtitleHistory": "Verfolge, wie viel Datenmüll sich über die Zeit ansammelt", + "Options_TitleAi": "KI-Erklärungen", + "Options_SubtitleAi": "Lass dir jeden Eintrag per KI erklären — benötigt deinen eigenen API-Schlüssel eines unterstützten Anbieters (optional, Vorschau)", + "Options_TitleScheduler": "Zeitplan", + "Options_SubtitleScheduler": "Reinigung automatisch nach Zeitplan über die Windows-Aufgabenplanung ausführen", + "Options_TitleAbout": "Über", + "Options_DonationTitle": "FluentCleaner wird Open Source", + "Options_DonationMessage": "Wenn dir die App gefällt und du meine Arbeit unterstützen möchtest, freue ich mich sehr über eine Spende.", + "Options_DonationSupport": "Spenden", + "Options_DonationLater": "Später", + "Common_Cancel": "Abbrechen", + "Common_RunCleanerTitle": "Cleaner starten", + "Common_File": "Datei", + "Common_Registry": "Registrierung", + "Cleaner_BtnRun": "Cleaner starten", + "Cleaner_BtnAnalyze": "Analysieren", + "Cleaner_TabWindows": "Windows", + "Cleaner_TabApplications": "Anwendungen", + "Cleaner_MiCheckAll": "Alle auswählen", + "Cleaner_MiUncheckAll": "Alle abwählen", + "Cleaner_MiAnalyze": "Analysieren", + "Cleaner_MiClean": "Bereinigen", + "Cleaner_MiExplain": "Mit KI erklären", + "Cleaner_MiShowSource": "Quelle anzeigen", + "Cleaner_MiRestoreDefault": "Standardzustand wiederherstellen", + "Cleaner_MiOpenFolder": "Ordner öffnen", + "Cleaner_MiExcludeFile": "Diese Datei ausschließen", + "Cleaner_MiExcludeFolder": "Diesen Ordner ausschließen", + "Cleaner_MiExcludeRegistry": "Diesen Registrierungsschlüssel ausschließen", + "Cleaner_ColType": "Typ", + "Cleaner_ColPath": "Pfad", + "Cleaner_ColEntry": "Eintrag", + "Cleaner_ColFiles": "Dateien", + "Cleaner_ColRegistry": "Registrierung", + "Cleaner_ColSize": "Größe", + "Cleaner_BtnBack": "\u003c Zur Übersicht", + "Cleaner_BtnDelete": "Löschen", + "Cleaner_Ready": "Bereit", + "Cleaner_LoadingDb": "Cleaner-Datenbank wird geladen…", + "Cleaner_AnalysisReady": "Analyse bereit — {0} Anwendungen in {1} Einträgen gefunden.", + "Cleaner_LoadFailed": "Datenbank konnte nicht geladen werden: {0}", + "Cleaner_ConfirmDelete": "{0} Dateien und {1} Registrierungseinträge für \"{2}\" löschen ({3})?", + "Cleaner_NothingToClean": "Nichts zu reinigen", + "Cleaner_NothingSelected": "Nichts ausgewählt", + "Cleaner_RunAnalyzeFirst": "Zuerst analysieren", + "Cleaner_ConfirmCleanAll": "Dies löscht dauerhaft die bei der Analyse gefundenen Dateien und Registrierungseinträge. Fortfahren?", + "Cleaner_MultipleEntries": "{0} Einträge", + "Cleaner_Analyzing": "Analysiere {0}…", + "Cleaner_Cleaning": "Reinige {0}…", + "Cleaner_AnalysisCancelled": "ANALYSE ABGEBROCHEN", + "Cleaner_AnalysisComplete": "ANALYSE ABGESCHLOSSEN", + "Cleaner_ToBeRemoved": "{0} werden entfernt — {1} Dateien, {2} Registrierungseinträge", + "Cleaner_DetailsHeader": "Details der zu löschenden Dateien (Hinweis: Es wurde noch nichts gelöscht)", + "Cleaner_Cancelled": "Abgebrochen", + "Cleaner_Done": "Fertig", + "Cleaner_DoneStatus": "{0} — {1} Objekte entfernt, {2} freigegeben", + "Cleaner_AddedExclusion": "Zu globalen Ausnahmen hinzugefügt", + "Custom_ColName": "Name", + "Custom_ColStatus": "Status", + "Custom_StatusEnabled": "Aktiviert", + "Custom_StatusDisabled": "Deaktiviert", + "Custom_MiEdit": "Bearbeiten…", + "Custom_MiTestLab": "Im Regel-Labor testen", + "Custom_MiDelete": "Löschen", + "Custom_BtnNew": "Neu…", + "Custom_BtnDelete": "Löschen", + "Custom_Hint": "Aktiviere einen Cleaner, um ihn sofort auf der Cleaner-Seite anzuzeigen.", + "Custom_ConfirmDeleteTitle": "Benutzerdefinierter Cleaner", + "Custom_ConfirmDeleteMessage": "„{0}\" löschen?", + "About_Version": "Version {0} (Classic)", + "About_PortableSuffix": " · Portabler Modus", + "About_Description": "Ein eigenständiger WinForms-Klon von FluentCleaner im klassischen CCleaner-Stil - gleiche Reinigungs-Engine und Winapp2.ini-Datenbank wie die Original-App, ohne WinUI/Fluent-Abhängigkeit.", + "About_TranslatedBy": "Übersetzt von {0}", + "About_LnkGitHub": "GitHub", + "About_LnkIssues": "Problem melden", + "About_LnkFaq": "FAQ", + "Exclusions_BtnRemove": "Entfernen", + "Exclusions_BtnAdd": "Hinzufügen", + "Exclusions_ChkEnabled": "Globale Ausnahmen aktivieren", + "Exclusions_LnkProtectedPaths": "Geschützte Pfade anzeigen", + "Exclusions_ProtectedPathsDesc": "Diese werden immer übersprungen, unabhängig davon, was die Datenbank sagt. Sie enthalten Browser-Erweiterungsdaten, die manche Apps (Passwort-Manager, Werbeblocker) ohne diese nicht mehr funktionieren lassen.", + "Exclusions_ProtectedPathsTitle": "Geschützte Pfade", + "PostClean_ChkEnabled": "Befehle nach der Reinigung ausführen", + "History_ChkTrack": "Reinigungsverlauf erfassen", + "History_Summary": "{0} Durchläufe · {1} insgesamt freigegeben", + "History_ColDate": "Datum", + "History_ColFreed": "Freigegeben", + "History_ColItems": "Objekte", + "History_BtnClear": "Löschen", + "Ai_LnkDiscussion": "Meinung dazu? An der Diskussion teilnehmen →", + "Ai_LnkGetKeyOpenAi": "Schlüssel holen (platform.openai.com/api-keys)", + "Ai_LnkGetKeyAnthropic": "Schlüssel holen (console.anthropic.com/settings/keys)", + "Ai_LnkGetKeyGroq": "Kostenlosen Schlüssel holen (console.groq.com)", + "Ai_BtnTest": "Testen", + "Ai_LblApiKeyOpenAi": "OpenAI-API-Schlüssel", + "Ai_LblApiKeyAnthropic": "Anthropic-API-Schlüssel", + "Ai_LblApiKeyGroq": "Groq-API-Schlüssel", + "Ai_LblProvider": "KI-Anbieter", + "Ai_LblDesc": "Klicke mit der rechten Maustaste auf einen Eintrag in der Cleaner-Ansicht und wähle „Mit KI erklären\", um eine verständliche Zusammenfassung zu erhalten. Wähle unten einen Anbieter und gib deinen Schlüssel ein - er wird nur bei Bedarf gesendet.", + "Ai_EnterKeyFirst": "Bitte zuerst einen API-Schlüssel eingeben.", + "Ai_Testing": "Teste…", + "Scheduler_LnkOpen": "Windows-Aufgabenplanung öffnen", + "Scheduler_BtnApply": "Übernehmen", + "Scheduler_ChkShutdown": "PC danach herunterfahren", + "Scheduler_LblTime": "Uhrzeit", + "Scheduler_FreqDaily": "Täglich", + "Scheduler_FreqWeekly": "Wöchentlich (Montag)", + "Scheduler_FreqLogon": "Bei Anmeldung", + "Scheduler_LblFrequency": "Häufigkeit", + "Scheduler_ChkEnabled": "Geplante Reinigung aktivieren", + "Scheduler_CheckingStatus": "Status wird geprüft…", + "Scheduler_LblDesc": "Führt dieselbe Reinigung wie der Kommandozeilenschalter /AUTO aus, geplant über die Windows-Aufgabenplanung. Läuft nur, während du angemeldet bist - keine Administratorrechte nötig.", + "Scheduler_StatusActive": "✓ Geplante Aufgabe ist aktiv.", + "Scheduler_StatusInactive": "✗ Nicht geplant.", + "CustomDialog_TitleEdit": "Benutzerdefinierten Cleaner bearbeiten", + "CustomDialog_TitleNew": "Neuer benutzerdefinierter Cleaner", + "CustomDialog_LblName": "Name:", + "CustomDialog_BtnTemplate": "Vorlage laden", + "CustomDialog_BtnSave": "Speichern", + "CustomDialog_BtnCancel": "Abbrechen", + "CustomDialog_Title": "Benutzerdefinierter Cleaner", + "CustomDialog_NameRequired": "Bitte zuerst einen Namen für den Cleaner vergeben.", + "Explain_BtnClose": "Schließen", + "Explain_Thinking": "Denke nach…", + "Explain_Title": "Mit KI erklären", + "RuleLab_Title": "Regel-Labor - {0}", + "RuleLab_TitleBase": "Regel-Labor", + "RuleLab_BtnDryRun": "Testlauf", + "RuleLab_BtnCancel": "Abbrechen", + "RuleLab_Hint": "Regeln bearbeiten und dann per Testlauf eine Vorschau der Treffer anzeigen.", + "RuleLab_Scanning": "Scanne…", + "RuleLab_NoEntry": "Kein Cleaner-Eintrag in den Regeln gefunden.", + "RuleLab_MoreFiles": "... und {0} weitere Dateien", + "RuleLab_Summary": "{0} Dateien, {1}", + "RuleLab_Cancelled": "Testlauf abgebrochen.", + "RuleLab_Error": "Fehler: {0}", + "RuleLab_BtnCopy": "Kopieren", + "RuleLab_BtnReport": "Melden", + "RuleLab_BtnClose": "Schließen", + "CleanGuard_BrowsersTitle": "Browser werden noch ausgeführt", + "CleanGuard_BrowsersMessage": "Diese Browser werden noch ausgeführt:\n\n{0}\n\nSchließe sie zuerst, damit ihre Daten vollständig bereinigt werden können. Trotzdem fortfahren?", + "CleanGuard_WarningsTitle": "Warnung", + "CleanGuard_WarningsMessage": "Einige der ausgewählten Einträge enthalten eine Warnung:\n\n{0}\n\nTrotzdem fortfahren?", + "Scheduler_ErrorExecutablePath": "Der Pfad zur FluentCleaner-Programmdatei konnte nicht ermittelt werden.", + "Scheduler_ResultCreated": "Die geplante Aufgabe wurde erstellt.", + "Scheduler_ResultNothingToRemove": "Es ist keine geplante Aufgabe zum Entfernen vorhanden.", + "Scheduler_ResultRemoved": "Die geplante Aufgabe wurde entfernt.", + "Scheduler_ErrorSchtasks": "Die Windows-Aufgabenplanung ist fehlgeschlagen.", + "Cleaning_DeletedFile": "Gelöscht: {0}", + "Cleaning_DeletedRegistry": "Registrierungseintrag gelöscht: {0}", + "Ai_NoApiKey": "Kein {0}-API-Schlüssel konfiguriert. Füge unter Einstellungen → KI einen hinzu.", + "Ai_ApiError": "{0}-API-Fehler: {1}", + "Ai_UnknownError": "Unbekannter Fehler", + "Ai_NoResponse": "Keine Antwort erhalten.", + "Ai_NetworkError": "Die {0}-API konnte nicht erreicht werden: {1}" +} diff --git a/FluentCleaner.Classic/Localization/en.json b/FluentCleaner.Classic/Localization/en.json new file mode 100644 index 0000000..60f3eb3 --- /dev/null +++ b/FluentCleaner.Classic/Localization/en.json @@ -0,0 +1,206 @@ +{ + "_Meta_LanguageDisplayName": "English", + "_Meta_TranslatorName": "", + "_Meta_TranslatorWebsite": "", + "Options_NavBasic": "Settings", + "Options_TitleBasic": "Settings", + "Options_SubtitleBasic": "Basic settings to control how FluentCleaner functions", + "BasicSettings_LblLanguage": "Language:", + "BasicSettings_LnkOpenLocalization": "Open localization folder", + "BasicSettings_OpenLocalizationErrorTitle": "Could not open folder", + "BasicSettings_OpenLocalizationError": "The localization folder could not be opened: {0}", + "BasicSettings_ChkWinapp2": "Winapp2.ini (built-in)", + "BasicSettings_ChkWinapp3": "Winapp3.ini", + "BasicSettings_ChkCustomDb": "Custom database", + "BasicSettings_BtnBrowse": "Browse…", + "BasicSettings_BtnReload": "Reload", + "BasicSettings_BtnUpdate": "Update…", + "BasicSettings_NotDownloaded": "Not downloaded yet", + "BasicSettings_LastUpdated": "Last updated {0:g}", + "BasicSettings_ReloadTitle": "Reload", + "BasicSettings_ReloadMessage": "Databases will reload the next time the Cleaner view loads entries. Restart the app to apply immediately.", + "BasicSettings_UpdateTitle": "Update", + "BasicSettings_UpdateWinapp2Done": "Winapp2.ini updated.", + "BasicSettings_UpdateWinapp3Done": "Winapp3.ini updated.", + "BasicSettings_UpdateFailed": "Update failed: {0}", + "BasicSettings_BrowseDialogFilter": "Winapp2 database (*.ini)|*.ini|All files (*.*)|*.*", + "BasicSettings_BrowseDialogTitle": "Select custom cleaner database", + "BasicSettings_RestartTitle": "Restart required", + "BasicSettings_RestartMessage": "The language change will take effect after restarting FluentCleaner. Restart now?", + "BasicSettings_LangSystem": "System default", + "BasicSettings_LangEnglish": "English", + "BasicSettings_LangGerman": "German", + "Main_NavCleaner": "Cleaner", + "Main_NavCustom": "Custom", + "Main_NavOptions": "Options", + "Main_LnkOnlineHelp": "Online Help", + "Main_LnkCheckUpdates": "Check for updates...", + "Main_GatheringInfo": "Gathering system information…", + "Main_HardwareSpecPlaceholder": "Hardware spec", + "Options_NavExclusions": "Exclusions", + "Options_NavPostClean": "Tasks", + "Options_NavHistory": "History", + "Options_NavAi": "AI", + "Options_NavScheduler": "Scheduler", + "Options_NavAbout": "About", + "Options_TitleExclusions": "Exclusions", + "Options_SubtitleExclusions": "Paths listed here are never cleaned — overrides all database rules", + "Options_TitlePostClean": "Post-Clean Tasks", + "Options_SubtitlePostClean": "One command per line — runs sequentially after every clean pass", + "Options_TitleHistory": "Clean History", + "Options_SubtitleHistory": "Track how much junk accumulates over time", + "Options_TitleAi": "AI Explanations", + "Options_SubtitleAi": "Explain any entry with AI — requires your own API key from a supported provider (opt-in, preview)", + "Options_TitleScheduler": "Scheduler", + "Options_SubtitleScheduler": "Run cleaning automatically on a schedule via Windows Task Scheduler", + "Options_TitleAbout": "About", + "Options_DonationTitle": "FluentCleaner is going open source", + "Options_DonationMessage": "Put a lot of work into this — if it helps you and you feel like saying thanks, a donation would really mean a lot.", + "Options_DonationSupport": "Support the project", + "Options_DonationLater": "Maybe later", + "Common_Cancel": "Cancel", + "Common_RunCleanerTitle": "Run Cleaner", + "Common_File": "File", + "Common_Registry": "Registry", + "Cleaner_BtnRun": "Run Cleaner", + "Cleaner_BtnAnalyze": "Analyze", + "Cleaner_TabWindows": "Windows", + "Cleaner_TabApplications": "Applications", + "Cleaner_MiCheckAll": "Check all", + "Cleaner_MiUncheckAll": "Uncheck all", + "Cleaner_MiAnalyze": "Analyze", + "Cleaner_MiClean": "Clean", + "Cleaner_MiExplain": "Explain with AI", + "Cleaner_MiShowSource": "Show source", + "Cleaner_MiRestoreDefault": "Restore default state", + "Cleaner_MiOpenFolder": "Open folder", + "Cleaner_MiExcludeFile": "Exclude this file", + "Cleaner_MiExcludeFolder": "Exclude this folder", + "Cleaner_MiExcludeRegistry": "Exclude this registry key", + "Cleaner_ColType": "Type", + "Cleaner_ColPath": "Path", + "Cleaner_ColEntry": "Entry", + "Cleaner_ColFiles": "Files", + "Cleaner_ColRegistry": "Registry", + "Cleaner_ColSize": "Size", + "Cleaner_BtnBack": "\u003c View summary results", + "Cleaner_BtnDelete": "Delete", + "Cleaner_Ready": "Ready", + "Cleaner_LoadingDb": "Loading cleaner database…", + "Cleaner_AnalysisReady": "Analysis ready — {0} apps found in {1} entries.", + "Cleaner_LoadFailed": "Failed to load database: {0}", + "Cleaner_ConfirmDelete": "Delete {0} files and {1} registry items for \"{2}\" ({3})?", + "Cleaner_NothingToClean": "Nothing to clean", + "Cleaner_NothingSelected": "Nothing selected", + "Cleaner_RunAnalyzeFirst": "Run Analyze first", + "Cleaner_ConfirmCleanAll": "This will permanently delete the files and registry entries found during analysis. Continue?", + "Cleaner_MultipleEntries": "{0} entries", + "Cleaner_Analyzing": "Analyzing {0}…", + "Cleaner_Cleaning": "Cleaning {0}…", + "Cleaner_AnalysisCancelled": "ANALYSIS CANCELLED", + "Cleaner_AnalysisComplete": "ANALYSIS COMPLETE", + "Cleaner_ToBeRemoved": "{0} to be removed — {1} files, {2} registry items", + "Cleaner_DetailsHeader": "Details of files to be deleted (Note: No files have been deleted yet)", + "Cleaner_Cancelled": "Cancelled", + "Cleaner_Done": "Done", + "Cleaner_DoneStatus": "{0} — removed {1} items, freed {2}", + "Cleaner_AddedExclusion": "Added to global exclusions", + "Custom_ColName": "Name", + "Custom_ColStatus": "Status", + "Custom_StatusEnabled": "Enabled", + "Custom_StatusDisabled": "Disabled", + "Custom_MiEdit": "Edit…", + "Custom_MiTestLab": "Test in Rule Lab", + "Custom_MiDelete": "Delete", + "Custom_BtnNew": "New…", + "Custom_BtnDelete": "Delete", + "Custom_Hint": "Check a cleaner to enable it; it\u0027ll show up on the Cleaner page right away.", + "Custom_ConfirmDeleteTitle": "Custom Cleaner", + "Custom_ConfirmDeleteMessage": "Delete \"{0}\"?", + "About_Version": "Version {0} (Classic)", + "About_PortableSuffix": " · Portable mode", + "About_Description": "A standalone WinForms clone of FluentCleaner, styled after classic CCleaner - same cleaning engine and Winapp2.ini database as the original app, no WinUI/Fluent dependency.", + "About_TranslatedBy": "Translated by {0}", + "About_LnkGitHub": "GitHub", + "About_LnkIssues": "Report an issue", + "About_LnkFaq": "FAQ", + "Exclusions_BtnRemove": "Remove", + "Exclusions_BtnAdd": "Add", + "Exclusions_ChkEnabled": "Enable global exclusions", + "Exclusions_LnkProtectedPaths": "View protected paths", + "Exclusions_ProtectedPathsDesc": "These are always skipped, no matter what the database says. They hold browser extension data that some apps (password managers, ad blockers) break without.", + "Exclusions_ProtectedPathsTitle": "Protected Paths", + "PostClean_ChkEnabled": "Run post-clean commands", + "History_ChkTrack": "Track clean history", + "History_Summary": "{0} runs · {1} total freed", + "History_ColDate": "Date", + "History_ColFreed": "Freed", + "History_ColItems": "Items", + "History_BtnClear": "Clear", + "Ai_LnkDiscussion": "Thoughts? Join the discussion →", + "Ai_LnkGetKeyOpenAi": "Get a key (platform.openai.com/api-keys)", + "Ai_LnkGetKeyAnthropic": "Get a key (console.anthropic.com/settings/keys)", + "Ai_LnkGetKeyGroq": "Get a free key (console.groq.com)", + "Ai_BtnTest": "Test", + "Ai_LblApiKeyOpenAi": "OpenAI API key", + "Ai_LblApiKeyAnthropic": "Anthropic API key", + "Ai_LblApiKeyGroq": "Groq API key", + "Ai_LblProvider": "AI provider", + "Ai_LblDesc": "Right-click any entry in the Cleaner view and choose \"Explain with AI\" to get a plain-language summary. Choose a provider below and add your key - sent only when you ask.", + "Ai_EnterKeyFirst": "Enter an API key first.", + "Ai_Testing": "Testing…", + "Scheduler_LnkOpen": "Open Windows Task Scheduler", + "Scheduler_BtnApply": "Apply", + "Scheduler_ChkShutdown": "Shut down the PC afterwards", + "Scheduler_LblTime": "Time", + "Scheduler_FreqDaily": "Daily", + "Scheduler_FreqWeekly": "Weekly (Monday)", + "Scheduler_FreqLogon": "At logon", + "Scheduler_LblFrequency": "Frequency", + "Scheduler_ChkEnabled": "Enable scheduled cleaning", + "Scheduler_CheckingStatus": "Checking status…", + "Scheduler_LblDesc": "Runs the same clean as the /AUTO command-line switch, on a schedule via Windows Task Scheduler. Runs only while you are logged on - no admin rights needed.", + "Scheduler_StatusActive": "✓ Scheduled task is active.", + "Scheduler_StatusInactive": "✗ Not scheduled.", + "CustomDialog_TitleEdit": "Edit Custom Cleaner", + "CustomDialog_TitleNew": "New Custom Cleaner", + "CustomDialog_LblName": "Name:", + "CustomDialog_BtnTemplate": "Load Template", + "CustomDialog_BtnSave": "Save", + "CustomDialog_BtnCancel": "Cancel", + "CustomDialog_Title": "Custom Cleaner", + "CustomDialog_NameRequired": "Give the cleaner a name first.", + "Explain_BtnClose": "Close", + "Explain_Thinking": "Thinking…", + "Explain_Title": "Explain with AI", + "RuleLab_Title": "Rule Lab - {0}", + "RuleLab_TitleBase": "Rule Lab", + "RuleLab_BtnDryRun": "Dry Run", + "RuleLab_BtnCancel": "Cancel", + "RuleLab_Hint": "Edit the rules, then Dry Run to preview what they match.", + "RuleLab_Scanning": "Scanning…", + "RuleLab_NoEntry": "No cleaner entry found in the rules.", + "RuleLab_MoreFiles": "... and {0} more files", + "RuleLab_Summary": "{0} files, {1}", + "RuleLab_Cancelled": "Dry run cancelled.", + "RuleLab_Error": "Error: {0}", + "RuleLab_BtnCopy": "Copy", + "RuleLab_BtnReport": "Report", + "RuleLab_BtnClose": "Close", + "CleanGuard_BrowsersTitle": "Browsers are running", + "CleanGuard_BrowsersMessage": "These browsers are still running:\n\n{0}\n\nClose them first so their data can be fully cleaned. Continue anyway?", + "CleanGuard_WarningsTitle": "Warning", + "CleanGuard_WarningsMessage": "Some of the selected items carry a warning:\n\n{0}\n\nContinue anyway?", + "Scheduler_ErrorExecutablePath": "Could not resolve the FluentCleaner executable path.", + "Scheduler_ResultCreated": "Scheduled task created.", + "Scheduler_ResultNothingToRemove": "Nothing to remove.", + "Scheduler_ResultRemoved": "Scheduled task removed.", + "Scheduler_ErrorSchtasks": "Windows Task Scheduler failed.", + "Cleaning_DeletedFile": "Deleted: {0}", + "Cleaning_DeletedRegistry": "Deleted registry item: {0}", + "Ai_NoApiKey": "No {0} API key configured. Add one under Settings → AI.", + "Ai_ApiError": "{0} API error: {1}", + "Ai_UnknownError": "Unknown error", + "Ai_NoResponse": "No response received.", + "Ai_NetworkError": "Could not reach the {0} API: {1}" +} diff --git a/FluentCleaner.Classic/Localization/gr.json b/FluentCleaner.Classic/Localization/gr.json new file mode 100644 index 0000000..b2366ac --- /dev/null +++ b/FluentCleaner.Classic/Localization/gr.json @@ -0,0 +1,205 @@ +{ + "_Meta_LanguageDisplayName": "Greek", + "_Meta_TranslatorName": "", + "_Meta_TranslatorWebsite": "", + "Options_NavBasic": "Ρυθμίσεις", + "Options_TitleBasic": "Ρυθμίσεις", + "Options_SubtitleBasic": "Βασικές ρυθμίσεις για τον έλεγχο του τρόπου λειτουργίας του FluentCleaner", + "BasicSettings_LblLanguage": "Γλώσσα:", + "BasicSettings_LnkOpenLocalization": "Άνοιγμα Τοπικού Φακέλου", + "BasicSettings_OpenLocalizationErrorTitle": "Δεν ήταν δυνατό το άνοιγμα του φακέλου", + "BasicSettings_OpenLocalizationError": "Δεν ήταν δυνατό το άνοιγμα του φακέλου τοπικής προσαρμογής: {0}", + "BasicSettings_ChkWinapp2": "Winapp2.ini (built-in)", + "BasicSettings_ChkWinapp3": "Winapp3.ini", + "BasicSettings_ChkCustomDb": "Προσαρμοσμένη βάση δεδομένων", + "BasicSettings_BtnBrowse": "Διαδρομή…", + "BasicSettings_BtnReload": "Επαναφόρτιση", + "BasicSettings_BtnUpdate": "Αναβάθμιση…", + "BasicSettings_NotDownloaded": "Δεν έχει ληφθεί ακόμα", + "BasicSettings_LastUpdated": "Τελευταία ενημέρωση {0:g}", + "BasicSettings_ReloadTitle": "Επαναφόρτιση", + "BasicSettings_ReloadMessage": "Οι βάσεις δεδομένων θα επαναφορτωθούν την επόμενη φορά που η προβολή Cleaner θα φορτώσει καταχωρήσεις. Επανεκκινήστε την εφαρμογή για άμεση εφαρμογή..", + "BasicSettings_UpdateTitle": "Αναβάθμιση", + "BasicSettings_UpdateWinapp2Done": "Winapp2.ini updated.", + "BasicSettings_UpdateWinapp3Done": "Winapp3.ini updated.", + "BasicSettings_UpdateFailed": "Η ενημέρωση απέτυχε: {0}", + "BasicSettings_BrowseDialogFilter": "Winapp2 database (*.ini)|*.ini|All files (*.*)|*.*", + "BasicSettings_BrowseDialogTitle": "Επιλέξτε προσαρμοσμένη βάση δεδομένων καθαρισμού", + "BasicSettings_RestartTitle": "Απαιτείται επανεκκίνηση", + "BasicSettings_RestartMessage": "Η αλλαγή γλώσσας θα τεθεί σε ισχύ μετά την επανεκκίνηση του FluentCleaner. Επανεκκινήστε τώρα.?", + "BasicSettings_LangSystem": "Προεπιλογή συστήματος", + "BasicSettings_LangEnglish": "Αγγλικά", + "BasicSettings_LangGerman": "Γερμανικά", + "Main_NavCleaner": "Καθαρισμός", + "Main_NavCustom": "Προσαρμογή", + "Main_NavOptions": "Επιλογές", + "Main_LnkOnlineHelp": "Ηλεκτρονική βοήθεια", + "Main_LnkCheckUpdates": "Έλεγχος για αναβαθμίσεις...", + "Main_GatheringInfo": "Συλλογή πληροφοριών συστήματος…", + "Main_HardwareSpecPlaceholder": "Προδιαγραφές υλικού", + "Options_NavExclusions": "Εξαιρέσεις", + "Options_NavPostClean": "Εργασίες", + "Options_NavHistory": "Ιστορικό", + "Options_NavAi": "AI", + "Options_NavScheduler": "Χρονοπρογραμματιστής", + "Options_NavAbout": "Σχετικά", + "Options_TitleExclusions": "Εξαιρέσεις", + "Options_SubtitleExclusions": "Οι διαδρομές που αναφέρονται εδώ δεν καθαρίζονται ποτέ — παρακάμπτει όλους τους κανόνες της βάσης δεδομένων", + "Options_TitlePostClean": "Εργασίες μετά τον καθαρισμό", + "Options_SubtitlePostClean": "Μία εντολή ανά γραμμή — εκτελείται διαδοχικά μετά από κάθε καθαρό πέρασμα", + "Options_TitleHistory": "Καθαρισμός Ιστορικού", + "Options_SubtitleHistory": "Παρακολουθήστε πόσα άχρηστα αντικείμενα συσσωρεύονται με την πάροδο του χρόνου", + "Options_TitleAi": "Εξηγήσεις Τεχνητής Νοημοσύνης", + "Options_SubtitleAi": "Εξηγήστε οποιαδήποτε καταχώρηση με AI — απαιτεί το δικό σας δωρεάν κλειδί Groq API (εγγραφή, προεπισκόπηση)", + "Options_TitleScheduler": "Χρονοπρογραμματιστής", + "Options_SubtitleScheduler": "Εκτελέστε αυτόματα τον καθαρισμό βάσει προγράμματος μέσω του Χρονοδιαγράμματος εργασιών των Windows", + "Options_TitleAbout": "Σχετικά", + "Options_DonationTitle": "Το FluentCleaner γίνεται ανοιχτού κώδικα", + "Options_DonationMessage": "Δούλεψε πολύ σε αυτό — αν σε βοηθάει και έχεις όρεξη να πεις ευχαριστώ, μια δωρεά θα σήμαινε πραγματικά πολλά.", + "Options_DonationSupport": "Υποστηρίξτε το έργο", + "Options_DonationLater": "Ίσως αργότερα", + "Common_Cancel": "Ακύρωση", + "Common_RunCleanerTitle": "Εκτέλεση Καθαρισμού", + "Common_File": "Αρχείο", + "Common_Registry": "Καταχώρηση", + "Cleaner_BtnRun": "Εκτέλεση Καθαρισμού", + "Cleaner_BtnAnalyze": "Ανάλυση", + "Cleaner_TabWindows": "Windows", + "Cleaner_TabApplications": "Εφαρμογές", + "Cleaner_MiCheckAll": "Έλεγχος Όλων", + "Cleaner_MiUncheckAll": "Αποεπιλέξτε όλα", + "Cleaner_MiAnalyze": "Ανάλυση", + "Cleaner_MiClean": "Καθαρισμός", + "Cleaner_MiExplain": "Εξηγήστε με Τεχνητή Νοημοσύνη", + "Cleaner_MiShowSource": "Εμφάνιση πηγής", + "Cleaner_MiRestoreDefault": "Επαναφορά προεπιλεγμένης κατάστασης", + "Cleaner_MiOpenFolder": "Άνοιγμα φακέλου", + "Cleaner_MiExcludeFile": "Εξαίρεση αυτού του αρχείου", + "Cleaner_MiExcludeFolder": "Εξαίρεση αυτού του φακέλου", + "Cleaner_ColType": "Τύπος", + "Cleaner_ColPath": "Διαδρομή", + "Cleaner_ColEntry": "Entry", + "Cleaner_ColFiles": "Αρχεία", + "Cleaner_ColRegistry": "Registry", + "Cleaner_ColSize": "Μέγεθος", + "Cleaner_BtnBack": "\u003c Προβολή συνοπτικών αποτελεσμάτων", + "Cleaner_BtnDelete": "Διαγραφή", + "Cleaner_Ready": "Έτοιμο", + "Cleaner_LoadingDb": "Φόρτωση βάσης δεδομένων καθαρισμού…", + "Cleaner_AnalysisReady": "Η ανάλυση είναι έτοιμη — {0} εφαρμογές που βρέθηκαν σε {1} καταχωρήσεις.", + "Cleaner_LoadFailed": "Αποτυχία φόρτωσης βάσης δεδομένων: {0}", + "Cleaner_ConfirmDelete": "Delete {0} files and {1} registry items for \"{2}\" ({3})?", + "Cleaner_NothingToClean": "Δεν υπάρχει τίποτα για καθάρισμα", + "Cleaner_NothingSelected": "Δεν έχει επιλεγεί τίποτα", + "Cleaner_RunAnalyzeFirst": "Εκτελέστε πρώτα την Ανάλυση", + "Cleaner_ConfirmCleanAll": "Αυτό θα διαγράψει οριστικά τα αρχεία και τις καταχωρήσεις μητρώου που βρέθηκαν κατά την ανάλυση. Συνέχεια?", + "Cleaner_MultipleEntries": "{0} καταχωρήσεις", + "Cleaner_Analyzing": "Ανάλυση {0}…", + "Cleaner_Cleaning": "Καθάρισμα {0}…", + "Cleaner_AnalysisCancelled": "Η ΑΝΑΛΥΣΗ ΑΚΥΡΩΘΗΚΕ", + "Cleaner_AnalysisComplete": "ΟΛΟΚΛΗΡΩΜΕΝΗ ΑΝΑΛΥΣΗ", + "Cleaner_ToBeRemoved": "{0} να αφαιρεθεί — {1} αρχεία, {2} rστοιχεία μητρώου", + "Cleaner_DetailsHeader": "Λεπτομέρειες αρχείων προς διαγραφή (Σημείωση: Δεν έχουν διαγραφεί ακόμη αρχεία)", + "Cleaner_Cancelled": "Ακυρώθηκε", + "Cleaner_Done": "Ολοκληρώθηκε", + "Cleaner_DoneStatus": "{0} — αφαιρέθηκε {1} αντικείμενα, απελευθερωμένα {2}", + "Cleaner_AddedExclusion": "Προστέθηκε στις καθολικές εξαιρέσεις", + "Custom_ColName": "Όνομα", + "Custom_ColStatus": "Κατάσταση", + "Custom_StatusEnabled": "Enabled", + "Custom_StatusDisabled": "Disabled", + "Custom_MiEdit": "Επεξεργασία…", + "Custom_MiTestLab": "Δοκιμή στο Εργαστήριο Κανόνων", + "Custom_MiDelete": "Διαγραφή", + "Custom_BtnNew": "New…", + "Custom_BtnDelete": "Διαγραφή", + "Custom_Hint": "Ελέγξτε ένα πρόγραμμα καθαρισμού για να το ενεργοποιήσετε. Θα εμφανιστεί αμέσως στη σελίδα του προγράμματος καθαρισμού.", + "Custom_ConfirmDeleteTitle": "Προσαρμοσμένο καθαριστικό", + "Custom_ConfirmDeleteMessage": "Διαγραφή \"{0}\"?", + "About_Version": "Έκδοση {0} (Classic)", + "About_PortableSuffix": " · Φορητή λειτουργία", + "About_Description": "Ένας αυτόνομος κλώνος WinForms του FluentCleaner, βασισμένος στο κλασικό CCleaner - η ίδια μηχανή καθαρισμού και η ίδια βάση δεδομένων Winapp2.ini με την αρχική εφαρμογή, χωρίς εξάρτηση WinUI/Fluent.", + "About_TranslatedBy": "Μεταφράστηκε από Πανταζίδης Νίκος {0}", + "About_LnkGitHub": "github.com", + "About_LnkIssues": "Αναφορά προβλήματος", + "About_LnkFaq": "FAQ", + "Exclusions_BtnRemove": "Αφαίρεση", + "Exclusions_BtnAdd": "Προσθήκη", + "Exclusions_ChkEnabled": "Ενεργοποίηση καθολικών εξαιρέσεων", + "Exclusions_LnkProtectedPaths": "Προβολή προστατευμένων διαδρομών", + "Exclusions_ProtectedPathsDesc": "Αυτά παραλείπονται πάντα, ανεξάρτητα από το τι αναφέρει η βάση δεδομένων. Διατηρούν δεδομένα επεκτάσεων προγράμματος περιήγησης που ορισμένες εφαρμογές (διαχειριστές κωδικών πρόσβασης, προγράμματα αποκλεισμού διαφημίσεων) δεν μπορούν να χρησιμοποιήσουν.", + "Exclusions_ProtectedPathsTitle": "Προστατευμένες Διαδρομές", + "PostClean_ChkEnabled": "Εκτέλεση εντολών μετά τον καθαρισμό", + "History_ChkTrack": "Παρακολούθηση ιστορικού καθαρισμού", + "History_Summary": "{0}εκτελεσμένα· {1} συνολικά ελευθερώθηκαν", + "History_ColDate": "Ημερομηνία", + "History_ColFreed": "Απελευθερωμένος", + "History_ColItems": "Αντικείμενα", + "History_BtnClear": "Καθαρισμός", + "Ai_LnkDiscussion": "Σκέψεις; Συμμετέχετε στη συζήτηση →", + "Ai_LnkGetKeyOpenAi": "Αποκτήστε ένα κλειδί (platform.openai.com/api-keys)", + "Ai_LnkGetKeyAnthropic": "Λήψη κλειδιού (console.anthropic.com/settings/keys)", + "Ai_LnkGetKeyGroq": "Αποκτήστε ένα δωρεάν κλειδί (console.groq.com)", + "Ai_BtnTest": "Τέστ", + "Ai_LblApiKeyOpenAi": "Κλειδί API OpenAI", + "Ai_LblApiKeyAnthropic": "Κλειδί API Anthropic", + "Ai_LblApiKeyGroq": "Κλειδί API Groq", + "Ai_LblProvider": "Πάροχος Τεχνητής Νοημοσύνης", + "Ai_LblDesc": "Κάντε δεξί κλικ σε οποιαδήποτε καταχώρηση στην προβολή Cleaner και επιλέξτε \"Εξήγηση με Τεχνητή Νοημοσύνη\" για να λάβετε μια σύνοψη σε απλά Αγγλικά. Επιλέξτε έναν πάροχο παρακάτω και προσθέστε το κλειδί σας - αποστέλλεται μόνο όταν το ζητήσετε.", + "Ai_EnterKeyFirst": "Εισαγάγετε πρώτα ένα κλειδί API.", + "Ai_Testing": "Δοκιμές…", + "Scheduler_LnkOpen": "Άνοιγμα Χρονοδιαγράμματος Εργασιών των Windows", + "Scheduler_BtnApply": "Εφαρμογή", + "Scheduler_ChkShutdown": "Κλείστε τον υπολογιστή μετά", + "Scheduler_LblTime": "Ώρα", + "Scheduler_FreqDaily": "Καθημερινά", + "Scheduler_FreqWeekly": "Εβδομαδιαία (Δευτέρα)", + "Scheduler_FreqLogon": "Κατά τη σύνδεση", + "Scheduler_LblFrequency": "Συχνότητα", + "Scheduler_ChkEnabled": "Ενεργοποίηση προγραμματισμένου καθαρισμού", + "Scheduler_CheckingStatus": "Έλεγχος κατάστασης…", + "Scheduler_LblDesc": "Εκτελείται με τον ίδιο καθαρό τρόπο όπως ο διακόπτης γραμμής εντολών /AUTO, βάσει προγράμματος μέσω του Χρονοδιαγράμματος εργασιών των Windows. Εκτελείται μόνο όταν είστε συνδεδεμένοι - δεν απαιτούνται δικαιώματα διαχειριστή.", + "Scheduler_StatusActive": "✓ Η προγραμματισμένη εργασία είναι ενεργή.", + "Scheduler_StatusInactive": "✗ Δεν έχει προγραμματιστεί.", + "CustomDialog_TitleEdit": "Επεξεργασία προσαρμοσμένου καθαριστή", + "CustomDialog_TitleNew": "Νέο προσαρμοσμένος καθαρισμός", + "CustomDialog_LblName": "Όνομα:", + "CustomDialog_BtnTemplate": "Φόρτωση προτύπου", + "CustomDialog_BtnSave": "Αποθήκευση", + "CustomDialog_BtnCancel": "Ακύρωση", + "CustomDialog_Title": "Προσαρμοσμένο καθαρισμός", + "CustomDialog_NameRequired": "Δώστε πρώτα ένα όνομα στον καθαρισμό.", + "Explain_BtnClose": "Κλείσιμο", + "Explain_Thinking": "Σκέψη…", + "Explain_Title": "Εξηγήστε με Τεχνητή Νοημοσύνη", + "RuleLab_Title": "Εργαστήριο Κανόνων - {0}", + "RuleLab_TitleBase": "Εργαστήριο Κανόνων", + "RuleLab_BtnDryRun": "Ξηρή λειτουργία", + "RuleLab_BtnCancel": "Ακύρωση", + "RuleLab_Hint": "Επεξεργαστείτε τους κανόνες και, στη συνέχεια, κάντε Dry Run για να κάνετε προεπισκόπηση με τι ταιριάζουν.", + "RuleLab_Scanning": "Ερευνα…", + "RuleLab_NoEntry": "Δεν βρέθηκε καμία πιο καθαρή καταχώρηση στους κανόνες.", + "RuleLab_MoreFiles": "... και {0} περισσότερα αρχεία", + "RuleLab_Summary": "{0} Αρχεία, {1}", + "RuleLab_Cancelled": "Η ξηρή λειτουργία ακυρώθηκε.", + "RuleLab_Error": "Σφάλμα: {0}", + "RuleLab_BtnCopy": "Αντιγραφή", + "RuleLab_BtnReport": "Αναφορά", + "RuleLab_BtnClose": "Κλείσιμο", + "CleanGuard_BrowsersTitle": "Τα προγράμματα περιήγησης εκτελούνται", + "CleanGuard_BrowsersMessage": "Αυτά τα προγράμματα περιήγησης εξακολουθούν να εκτελούνται:\n\n{0}\n\nΚλείστε τα πρώτα, ώστε τα δεδομένα τους να καθαριστούν πλήρως. Συνέχεια ούτως ή άλλως;", + "CleanGuard_WarningsTitle": "Προσοχή", + "CleanGuard_WarningsMessage": "Ορισμένα από τα επιλεγμένα στοιχεία φέρουν μια προειδοποίηση:\n\n{0}\n\nΝα συνεχιστεί ούτως ή άλλως;", + "Scheduler_ErrorExecutablePath": "Δεν ήταν δυνατή η επίλυση της διαδρομής εκτελέσιμου αρχείου FluentCleaner.", + "Scheduler_ResultCreated": "Η προγραμματισμένη εργασία δημιουργήθηκε.", + "Scheduler_ResultNothingToRemove": "Τίποτα για Διαγραφή.", + "Scheduler_ResultRemoved": "Η προγραμματισμένη εργασία καταργήθηκε.", + "Scheduler_ErrorSchtasks": "Ο Χρονοδιάγραμμα εργασιών των Windows απέτυχε.", + "Cleaning_DeletedFile": "Διεγραμμένο: {0}", + "Cleaning_DeletedRegistry": "Διαγραμμένο στοιχείο μητρώου: {0}", + "Ai_NoApiKey": "Όχι {0} Διαμορφώθηκε κλειδί API. Προσθέστε ένα στις Ρυθμίσεις → Τεχνητή Νοημοσύνη.", + "Ai_ApiError": "{0} Σφάλμα API: {1}", + "Ai_UnknownError": "Άγνωστο σφάλμα", + "Ai_NoResponse": "Δεν ελήφθη απάντηση.", + "Ai_NetworkError": "Δεν ήταν δυνατή η πρόσβαση στο {0} API: {1}" +} diff --git a/FluentCleaner.Classic/Localization/zh-CN.json b/FluentCleaner.Classic/Localization/zh-CN.json new file mode 100644 index 0000000..78a10d9 --- /dev/null +++ b/FluentCleaner.Classic/Localization/zh-CN.json @@ -0,0 +1,205 @@ +{ + "_Meta_LanguageDisplayName": "Chinese (Simplified)", + "_Meta_TranslatorName": "大眼仔~旭", + "_Meta_TranslatorWebsite": "https://www.dayanzai.me/", + "Options_NavBasic": "设置", + "Options_TitleBasic": "设置", + "Options_SubtitleBasic": "基础设置,控制 FluentCleaner 的运行方式", + "BasicSettings_LblLanguage": "语言:", + "BasicSettings_LnkOpenLocalization": "打开语言文件目录", + "BasicSettings_OpenLocalizationErrorTitle": "无法打开文件夹", + "BasicSettings_OpenLocalizationError": "无法打开语言目录:{0}", + "BasicSettings_ChkWinapp2": "Winapp2.ini(内置)", + "BasicSettings_ChkWinapp3": "Winapp3.ini", + "BasicSettings_ChkCustomDb": "自定义数据库", + "BasicSettings_BtnBrowse": "浏览…", + "BasicSettings_BtnReload": "重新加载", + "BasicSettings_BtnUpdate": "更新…", + "BasicSettings_NotDownloaded": "尚未下载", + "BasicSettings_LastUpdated": "上次更新:{0:g}", + "BasicSettings_ReloadTitle": "重新加载", + "BasicSettings_ReloadMessage": "数据库将在清理列表下次加载时生效。重启程序可立即应用。", + "BasicSettings_UpdateTitle": "更新", + "BasicSettings_UpdateWinapp2Done": "Winapp2.ini 更新完成。", + "BasicSettings_UpdateWinapp3Done": "Winapp3.ini 更新完成。", + "BasicSettings_UpdateFailed": "更新失败:{0}", + "BasicSettings_BrowseDialogFilter": "Winapp2 数据库 (*.ini)|*.ini|所有文件 (*.*)|*.*", + "BasicSettings_BrowseDialogTitle": "选择自定义清理数据库", + "BasicSettings_RestartTitle": "需要重启", + "BasicSettings_RestartMessage": "语言更改将在重启 FluentCleaner 后生效。是否立即重启?", + "BasicSettings_LangSystem": "跟随系统", + "BasicSettings_LangEnglish": "English", + "BasicSettings_LangGerman": "Deutsch", + "Main_NavCleaner": "清理器", + "Main_NavCustom": "自定义规则", + "Main_NavOptions": "设置", + "Main_LnkOnlineHelp": "在线帮助", + "Main_LnkCheckUpdates": "检查更新…", + "Main_GatheringInfo": "正在收集系统信息…", + "Main_HardwareSpecPlaceholder": "硬件信息", + "Options_NavExclusions": "排除项", + "Options_NavPostClean": "清理后任务", + "Options_NavHistory": "清理历史", + "Options_NavAi": "AI 功能", + "Options_NavScheduler": "计划任务", + "Options_NavAbout": "关于", + "Options_TitleExclusions": "排除项", + "Options_SubtitleExclusions": "此处填写的路径永远不会被清理,优先级高于所有数据库规则", + "Options_TitlePostClean": "清理后任务", + "Options_SubtitlePostClean": "每行一条命令,每次清理完成后按顺序执行", + "Options_TitleHistory": "清理历史", + "Options_SubtitleHistory": "追踪垃圾文件随时间的累积情况", + "Options_TitleAi": "AI 条目说明", + "Options_SubtitleAi": "使用 AI 解释清理条目,需自备免费 Groq API 密钥(可选,预览功能)", + "Options_TitleScheduler": "计划任务", + "Options_SubtitleScheduler": "通过 Windows 任务计划程序自动定时执行清理", + "Options_TitleAbout": "关于", + "Options_DonationTitle": "FluentCleaner 即将开源", + "Options_DonationMessage": "本项目投入大量开发精力。如果工具对你有用,欢迎酌情赞助支持。", + "Options_DonationSupport": "支持项目", + "Options_DonationLater": "稍后再说", + "Common_Cancel": "取消", + "Common_RunCleanerTitle": "执行清理", + "Common_File": "文件", + "Common_Registry": "注册表", + "Cleaner_BtnRun": "执行清理", + "Cleaner_BtnAnalyze": "分析", + "Cleaner_TabWindows": "Windows", + "Cleaner_TabApplications": "应用程序", + "Cleaner_MiCheckAll": "全选", + "Cleaner_MiUncheckAll": "取消全选", + "Cleaner_MiAnalyze": "分析", + "Cleaner_MiClean": "清理", + "Cleaner_MiExplain": "AI 解读", + "Cleaner_MiShowSource": "查看源规则", + "Cleaner_MiRestoreDefault": "恢复默认状态", + "Cleaner_MiOpenFolder": "打开文件夹", + "Cleaner_MiExcludeFile": "排除此文件", + "Cleaner_MiExcludeFolder": "排除此文件夹", + "Cleaner_ColType": "类型", + "Cleaner_ColPath": "路径", + "Cleaner_ColEntry": "条目", + "Cleaner_ColFiles": "文件数", + "Cleaner_ColRegistry": "注册表项", + "Cleaner_ColSize": "大小", + "Cleaner_BtnBack": "< 返回汇总结果", + "Cleaner_BtnDelete": "删除", + "Cleaner_Ready": "就绪", + "Cleaner_LoadingDb": "正在加载清理数据库…", + "Cleaner_AnalysisReady": "分析完成,共 {1} 条规则匹配到 {0} 个应用。", + "Cleaner_LoadFailed": "数据库加载失败:{0}", + "Cleaner_ConfirmDelete": "确定删除「{2}」({3})对应的 {0} 个文件与 {1} 条注册表项?", + "Cleaner_NothingToClean": "暂无可清理项目", + "Cleaner_NothingSelected": "未选中任何项目", + "Cleaner_RunAnalyzeFirst": "请先执行分析", + "Cleaner_ConfirmCleanAll": "此操作将永久删除分析找到的文件与注册表项,是否继续?", + "Cleaner_MultipleEntries": "{0} 条条目", + "Cleaner_Analyzing": "正在分析 {0}…", + "Cleaner_Cleaning": "正在清理 {0}…", + "Cleaner_AnalysisCancelled": "分析已取消", + "Cleaner_AnalysisComplete": "分析完成", + "Cleaner_ToBeRemoved": "待清理:{1} 个文件、{2} 条注册表项,总计 {0}", + "Cleaner_DetailsHeader": "待删除文件详情(注意:文件尚未删除)", + "Cleaner_Cancelled": "已取消", + "Cleaner_Done": "完成", + "Cleaner_DoneStatus": "{0} — 移除 {1} 项,释放空间 {2}", + "Cleaner_AddedExclusion": "已添加至全局排除列表", + "Custom_ColName": "名称", + "Custom_ColStatus": "状态", + "Custom_StatusEnabled": "启用", + "Custom_StatusDisabled": "禁用", + "Custom_MiEdit": "编辑…", + "Custom_MiTestLab": "在规则实验室测试", + "Custom_MiDelete": "删除", + "Custom_BtnNew": "新建…", + "Custom_BtnDelete": "删除", + "Custom_Hint": "勾选规则即可启用,启用后会立即显示在清理页面。", + "Custom_ConfirmDeleteTitle": "自定义清理规则", + "Custom_ConfirmDeleteMessage": "确定删除「{0}」?", + "About_Version": "版本 {0}(经典版)", + "About_PortableSuffix": " · 便携模式", + "About_Description": "基于 WinForms 重制的 FluentCleaner,界面风格参考经典版 CCleaner;使用与原版一致的清理引擎和 Winapp2.ini 数据库,无需依赖 WinUI/Fluent。", + "About_TranslatedBy": "翻译:{0}", + "About_LnkGitHub": "GitHub", + "About_LnkIssues": "提交问题反馈", + "About_LnkFaq": "常见问题", + "Exclusions_BtnRemove": "移除", + "Exclusions_BtnAdd": "添加", + "Exclusions_ChkEnabled": "启用全局排除项", + "Exclusions_LnkProtectedPaths": "查看受保护路径", + "Exclusions_ProtectedPathsDesc": "无论数据库规则如何,这些路径始终跳过清理。其中存放浏览器扩展数据,清理后可能导致密码管理器、广告拦截插件异常。", + "Exclusions_ProtectedPathsTitle": "受保护路径", + "PostClean_ChkEnabled": "执行清理后命令", + "History_ChkTrack": "记录清理历史", + "History_Summary": "共 {0} 次清理 · 总计释放 {1}", + "History_ColDate": "日期", + "History_ColFreed": "释放空间", + "History_ColItems": "清理项数", + "History_BtnClear": "清空", + "Ai_LnkDiscussion": "有想法?前往讨论区 →", + "Ai_LnkGetKeyOpenAi": "获取密钥(platform.openai.com/api-keys)", + "Ai_LnkGetKeyAnthropic": "获取密钥(console.anthropic.com/settings/keys)", + "Ai_LnkGetKeyGroq": "获取免费密钥(console.groq.com)", + "Ai_BtnTest": "测试", + "Ai_LblApiKeyOpenAi": "OpenAI API 密钥", + "Ai_LblApiKeyAnthropic": "Anthropic API 密钥", + "Ai_LblApiKeyGroq": "Groq API 密钥", + "Ai_LblProvider": "AI 服务商", + "Ai_LblDesc": "在清理列表右键条目,选择「AI 解读」获取通俗说明。选择服务商并填入密钥;仅在你主动调用时发送请求。", + "Ai_EnterKeyFirst": "请先填写 API 密钥。", + "Ai_Testing": "正在测试…", + "Scheduler_LnkOpen": "打开 Windows 任务计划程序", + "Scheduler_BtnApply": "应用", + "Scheduler_ChkShutdown": "清理完成后关闭电脑", + "Scheduler_LblTime": "时间", + "Scheduler_FreqDaily": "每天", + "Scheduler_FreqWeekly": "每周(周一)", + "Scheduler_FreqLogon": "登录时", + "Scheduler_LblFrequency": "执行频率", + "Scheduler_ChkEnabled": "启用定时清理", + "Scheduler_CheckingStatus": "正在检测状态…", + "Scheduler_LblDesc": "功能等同于命令行参数 /AUTO,由 Windows 任务计划程序定时执行。仅在用户登录状态运行,无需管理员权限。", + "Scheduler_StatusActive": "✓ 定时任务已启用", + "Scheduler_StatusInactive": "✗ 未设置定时任务", + "CustomDialog_TitleEdit": "编辑自定义清理规则", + "CustomDialog_TitleNew": "新建自定义清理规则", + "CustomDialog_LblName": "名称:", + "CustomDialog_BtnTemplate": "加载模板", + "CustomDialog_BtnSave": "保存", + "CustomDialog_BtnCancel": "取消", + "CustomDialog_Title": "自定义清理规则", + "CustomDialog_NameRequired": "请先为规则填写名称。", + "Explain_BtnClose": "关闭", + "Explain_Thinking": "思考中…", + "Explain_Title": "AI 解读", + "RuleLab_Title": "规则实验室 - {0}", + "RuleLab_TitleBase": "规则实验室", + "RuleLab_BtnDryRun": "模拟运行", + "RuleLab_BtnCancel": "取消", + "RuleLab_Hint": "修改规则后,点击模拟运行预览匹配结果。", + "RuleLab_Scanning": "正在扫描…", + "RuleLab_NoEntry": "规则内未匹配到清理条目。", + "RuleLab_MoreFiles": "…以及另外 {0} 个文件", + "RuleLab_Summary": "{0} 个文件,{1}", + "RuleLab_Cancelled": "模拟运行已取消。", + "RuleLab_Error": "错误:{0}", + "RuleLab_BtnCopy": "复制", + "RuleLab_BtnReport": "提交反馈", + "RuleLab_BtnClose": "关闭", + "CleanGuard_BrowsersTitle": "浏览器正在运行", + "CleanGuard_BrowsersMessage": "以下浏览器仍在运行:\n\n{0}\n\n请先关闭浏览器以完整清理数据。是否仍要继续?", + "CleanGuard_WarningsTitle": "警告", + "CleanGuard_WarningsMessage": "选中的部分条目存在风险提示:\n\n{0}\n\n是否仍要继续?", + "Scheduler_ErrorExecutablePath": "无法定位 FluentCleaner 程序路径。", + "Scheduler_ResultCreated": "定时任务创建成功。", + "Scheduler_ResultNothingToRemove": "无需删除任务。", + "Scheduler_ResultRemoved": "定时任务已移除。", + "Scheduler_ErrorSchtasks": "Windows 任务计划程序操作失败。", + "Cleaning_DeletedFile": "已删除:{0}", + "Cleaning_DeletedRegistry": "已删除注册表项:{0}", + "Ai_NoApiKey": "未配置 {0} API 密钥,请前往 设置 → AI 功能 添加密钥。", + "Ai_ApiError": "{0} API 请求出错:{1}", + "Ai_UnknownError": "未知错误", + "Ai_NoResponse": "未收到返回结果。", + "Ai_NetworkError": "无法连接 {0} API:{1}" +} \ No newline at end of file diff --git a/TRANSLATING_CLASSIC.md b/FluentCleaner.Classic/TRANSLATING_CLASSIC.md similarity index 100% rename from TRANSLATING_CLASSIC.md rename to FluentCleaner.Classic/TRANSLATING_CLASSIC.md diff --git a/FluentCleaner.Core/FluentCleaner.Core.csproj b/FluentCleaner.Core/FluentCleaner.Core.csproj new file mode 100644 index 0000000..5a1ee06 --- /dev/null +++ b/FluentCleaner.Core/FluentCleaner.Core.csproj @@ -0,0 +1,25 @@ + + + + + netstandard2.0 + latest + enable + enable + FluentCleaner + + + + + + + + + + + + diff --git a/FluentCleaner/Models/CleanerEntry.cs b/FluentCleaner.Core/Models/CleanerEntry.cs similarity index 93% rename from FluentCleaner/Models/CleanerEntry.cs rename to FluentCleaner.Core/Models/CleanerEntry.cs index f938929..0fea61f 100644 --- a/FluentCleaner/Models/CleanerEntry.cs +++ b/FluentCleaner.Core/Models/CleanerEntry.cs @@ -3,6 +3,7 @@ namespace FluentCleaner.Models; /* Represents one application entry parsed from Winapp2.ini. Holds everything needed to detect whether the app is installed and to describe what files and registry keys should be cleaned. */ + public class CleanerEntry { public string Name { get; set; } = ""; // Display name shown in the UI (e.g. "Microsoft Edge"). @@ -19,4 +20,6 @@ public class CleanerEntry // Not from Winapp2.ini; used internally by FluentCleaner: public bool IsCustom { get; set; } // True for entries loaded from the user's Custom/ folder. + + public string RawText { get; set; } = ""; // Verbatim ini block for the "Show source" view. } diff --git a/FluentCleaner/Models/ExcludeKeyEntry.cs b/FluentCleaner.Core/Models/ExcludeKeyEntry.cs similarity index 100% rename from FluentCleaner/Models/ExcludeKeyEntry.cs rename to FluentCleaner.Core/Models/ExcludeKeyEntry.cs diff --git a/FluentCleaner/Models/FileKeyEntry.cs b/FluentCleaner.Core/Models/FileKeyEntry.cs similarity index 100% rename from FluentCleaner/Models/FileKeyEntry.cs rename to FluentCleaner.Core/Models/FileKeyEntry.cs diff --git a/FluentCleaner/Models/RegKeyEntry.cs b/FluentCleaner.Core/Models/RegKeyEntry.cs similarity index 100% rename from FluentCleaner/Models/RegKeyEntry.cs rename to FluentCleaner.Core/Models/RegKeyEntry.cs diff --git a/FluentCleaner/Models/ScanResult.cs b/FluentCleaner.Core/Models/ScanResult.cs similarity index 100% rename from FluentCleaner/Models/ScanResult.cs rename to FluentCleaner.Core/Models/ScanResult.cs diff --git a/FluentCleaner/Services/CategoryResolver.cs b/FluentCleaner.Core/Services/CategoryResolver.cs similarity index 90% rename from FluentCleaner/Services/CategoryResolver.cs rename to FluentCleaner.Core/Services/CategoryResolver.cs index 645fc7a..cbee2f1 100644 --- a/FluentCleaner/Services/CategoryResolver.cs +++ b/FluentCleaner.Core/Services/CategoryResolver.cs @@ -45,5 +45,10 @@ public static class CategoryResolver return new CategoryInfo("Other Applications", 2000); } - public readonly record struct CategoryInfo(string Name, int Order); + public readonly struct CategoryInfo + { + public CategoryInfo(string name, int order) { Name = name; Order = order; } + public string Name { get; } + public int Order { get; } + } } diff --git a/FluentCleaner/Services/CustomEntryService.cs b/FluentCleaner.Core/Services/CustomEntryService.cs similarity index 97% rename from FluentCleaner/Services/CustomEntryService.cs rename to FluentCleaner.Core/Services/CustomEntryService.cs index c61e0c9..b4afcfd 100644 --- a/FluentCleaner/Services/CustomEntryService.cs +++ b/FluentCleaner.Core/Services/CustomEntryService.cs @@ -4,7 +4,7 @@ namespace FluentCleaner.Services; // Owns the Custom/ folder. // CustomPage uses CustomDir to manage files; CleanerPageViewModel calls LoadEnabledEntriesAsync() -// to pull active entries into the cleaner +// to pull active entries into the cleaner public class CustomEntryService { public static readonly string CustomDir = diff --git a/FluentCleaner/Services/DetectionService.cs b/FluentCleaner.Core/Services/DetectionService.cs similarity index 98% rename from FluentCleaner/Services/DetectionService.cs rename to FluentCleaner.Core/Services/DetectionService.cs index 13b6966..644a61c 100644 --- a/FluentCleaner/Services/DetectionService.cs +++ b/FluentCleaner.Core/Services/DetectionService.cs @@ -61,7 +61,7 @@ public class DetectionService } private static RegistryKey? OpenKey(string hive, string subKey) => - CleaningService.OpenHive(hive)?.OpenSubKey(subKey, writable: false); + RegistryHelpers.OpenHive(hive)?.OpenSubKey(subKey, writable: false); private bool CheckFile(string rawPath) { diff --git a/FluentCleaner/Services/PathExpander.cs b/FluentCleaner.Core/Services/PathExpander.cs similarity index 54% rename from FluentCleaner/Services/PathExpander.cs rename to FluentCleaner.Core/Services/PathExpander.cs index 2b4e7e5..8d65cf7 100644 --- a/FluentCleaner/Services/PathExpander.cs +++ b/FluentCleaner.Core/Services/PathExpander.cs @@ -3,6 +3,7 @@ namespace FluentCleaner.Services; /* Handles the two jobs that make FileKey paths tricky: 1. Expand %EnvVar% tokens (winapp2 uses its own subset, not all Windows vars) 2. Walk directory trees where path segments contain * wildcards */ + public class PathExpander { private readonly Dictionary _vars = BuildVarMap(); @@ -17,35 +18,35 @@ public class PathExpander map[$"%{name}%"] = value.TrimEnd('\\', '/'); } - Add("AppData", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); - Add("LocalAppData", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); - Add("LocalLowAppData", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "LocalLow")); - Add("ProgramFiles", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); + Add("AppData", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + Add("LocalAppData", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); + Add("LocalLowAppData", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "LocalLow")); + Add("ProgramFiles", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); Add("ProgramFiles(x86)", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)); - Add("ProgramFilesX86", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)); // Winapp2 alias (no parentheses) - Add("ProgramData", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)); - Add("CommonAppData", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)); - Add("UserProfile", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); - Add("Documents", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); - Add("Desktop", Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); - Add("Music", Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)); - Add("Pictures", Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)); - Add("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos)); - Add("SystemRoot", Environment.GetFolderPath(Environment.SpecialFolder.Windows)); - Add("WinDir", Environment.GetFolderPath(Environment.SpecialFolder.Windows)); - Add("System", Environment.GetFolderPath(Environment.SpecialFolder.System)); - Add("SystemX86", Environment.GetFolderPath(Environment.SpecialFolder.SystemX86)); - Add("Temp", Path.GetTempPath().TrimEnd('\\', '/')); - Add("Tmp", Path.GetTempPath().TrimEnd('\\', '/')); - Add("SystemDrive", Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Windows))?.TrimEnd('\\') ?? "C:"); + Add("ProgramFilesX86", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)); // Winapp2 alias (no parentheses) + Add("ProgramData", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)); + Add("CommonAppData", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)); + Add("UserProfile", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + Add("Documents", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); + Add("Desktop", Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); + Add("Music", Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)); + Add("Pictures", Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)); + Add("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos)); + Add("SystemRoot", Environment.GetFolderPath(Environment.SpecialFolder.Windows)); + Add("WinDir", Environment.GetFolderPath(Environment.SpecialFolder.Windows)); + Add("System", Environment.GetFolderPath(Environment.SpecialFolder.System)); + Add("SystemX86", Environment.GetFolderPath(Environment.SpecialFolder.SystemX86)); + Add("Temp", Path.GetTempPath().TrimEnd('\\', '/')); + Add("Tmp", Path.GetTempPath().TrimEnd('\\', '/')); + Add("SystemDrive", Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Windows))?.TrimEnd('\\') ?? "C:"); return map; } public string ExpandVariables(string path) { - foreach (var (token, value) in _vars) - path = path.Replace(token, value, StringComparison.OrdinalIgnoreCase); + foreach (var pair in _vars) + path = ReplaceIgnoreCase(path, pair.Key, pair.Value); // Let the OS handle any remaining %VAR% tokens we don't know about path = Environment.ExpandEnvironmentVariables(path); @@ -76,15 +77,31 @@ public class PathExpander // If the path references %ProgramFiles%, also try %ProgramFiles(x86)%. // HashSet prevents duplicates when both point to the same dir (32-bit OS). - if (rawPath.Contains("%ProgramFiles%", StringComparison.OrdinalIgnoreCase)) + if (rawPath.IndexOf("%ProgramFiles%", StringComparison.OrdinalIgnoreCase) >= 0) { - var x86Path = rawPath.Replace("%ProgramFiles%", "%ProgramFiles(x86)%", - StringComparison.OrdinalIgnoreCase); + var x86Path = ReplaceIgnoreCase(rawPath, "%ProgramFiles%", "%ProgramFiles(x86)%"); ResolveRecursive(ExpandVariables(x86Path), results); } return results.ToList(); } + // net48/netstandard2.0 have no case-insensitive string.Replace overload; + // this is just the manual IndexOf-and-splice version of that + private static string ReplaceIgnoreCase(string source, string oldValue, string newValue) + { + var sb = new System.Text.StringBuilder(); + int pos = 0; + while (true) + { + int idx = source.IndexOf(oldValue, pos, StringComparison.OrdinalIgnoreCase); + if (idx < 0) { sb.Append(source, pos, source.Length - pos); break; } + sb.Append(source, pos, idx - pos); + sb.Append(newValue); + pos = idx + oldValue.Length; + } + return sb.ToString(); + } + private static void ResolveRecursive(string path, HashSet results) { var parts = path.Split(new[] { '\\', '/' }, StringSplitOptions.None); @@ -101,12 +118,12 @@ public class PathExpander var basePath = wcIdx == 0 ? Path.GetPathRoot(path) ?? "" - : string.Join('\\', parts[..wcIdx]); + : string.Join("\\", parts.Take(wcIdx)); if (!Directory.Exists(basePath)) return; - var wildcard = parts[wcIdx]; - var remaining = parts[(wcIdx + 1)..]; + var wildcard = parts[wcIdx]; + var remaining = parts.Skip(wcIdx + 1).ToArray(); try { @@ -120,7 +137,7 @@ public class PathExpander if (remaining.Length == 0) results.Add(match); else - ResolveRecursive(Path.Combine(match, string.Join('\\', remaining)), results); + ResolveRecursive(Path.Combine(match, string.Join("\\", remaining)), results); } } catch (UnauthorizedAccessException) { } diff --git a/FluentCleaner.Core/Services/RegistryHelpers.cs b/FluentCleaner.Core/Services/RegistryHelpers.cs new file mode 100644 index 0000000..ace22c3 --- /dev/null +++ b/FluentCleaner.Core/Services/RegistryHelpers.cs @@ -0,0 +1,18 @@ +using Microsoft.Win32; + +namespace FluentCleaner.Services; + +// Maps hive abbreviations (and full names) to their registry root key. +// Shared by DetectionService and CleaningService so neither owns the mapping. +public static class RegistryHelpers +{ + public static RegistryKey? OpenHive(string hive) => hive switch + { + "HKCU" or "HKEY_CURRENT_USER" => Registry.CurrentUser, + "HKLM" or "HKEY_LOCAL_MACHINE" => Registry.LocalMachine, + "HKU" or "HKEY_USERS" => Registry.Users, + "HKCC" or "HKEY_CURRENT_CONFIG" => Registry.CurrentConfig, + "HKCR" or "HKEY_CLASSES_ROOT" => Registry.ClassesRoot, + _ => null + }; +} diff --git a/FluentCleaner/Services/Winapp2Parser.cs b/FluentCleaner.Core/Services/Winapp2Parser.cs similarity index 52% rename from FluentCleaner/Services/Winapp2Parser.cs rename to FluentCleaner.Core/Services/Winapp2Parser.cs index b2b7375..65e2224 100644 --- a/FluentCleaner/Services/Winapp2Parser.cs +++ b/FluentCleaner.Core/Services/Winapp2Parser.cs @@ -1,4 +1,5 @@ using FluentCleaner.Models; +using System.Text; using System.Text.RegularExpressions; namespace FluentCleaner.Services; @@ -8,16 +9,17 @@ namespace FluentCleaner.Services; // FileKey1=..., FileKey2=..., Detect, Detect1, Detect2, etc. public class Winapp2Parser { - private static readonly Regex RxFileKey = new(@"^FileKey\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // FileKey1, FileKey2 ...; number required - private static readonly Regex RxRegKey = new(@"^RegKey\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // RegKey1, RegKey2 ... + private static readonly Regex RxFileKey = new(@"^FileKey\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // FileKey1, FileKey2 ...; number required + private static readonly Regex RxRegKey = new(@"^RegKey\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // RegKey1, RegKey2 ... private static readonly Regex RxExcludeKey = new(@"^ExcludeKey\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // ExcludeKey1, ExcludeKey2 ... - private static readonly Regex RxDetect = new(@"^Detect\d*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // Detect or Detect1; number optional + private static readonly Regex RxDetect = new(@"^Detect\d*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // Detect or Detect1; number optional private static readonly Regex RxDetectFile = new(@"^DetectFile\d*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // DetectFile or DetectFile1; number optional public List Parse(string content) { var entries = new List(); CleanerEntry? current = null; + var raw = new StringBuilder(); // collects the verbatim block of the current entry for "Show source" //Split on both \r and \n;WinUI 3 TextBox saves with \r only (not \r\n), //so splitting on just \n would leave the entire file as a single line @@ -26,14 +28,16 @@ public class Winapp2Parser var line = rawLine.Trim(); if (line.Length == 0 || line[0] == ';' || line[0] == '#') continue; - if (line.StartsWith('[') && line.EndsWith(']')) + if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal)) { - if (current is not null && IsValid(current)) entries.Add(current); + if (current is not null && IsValid(current)) { current.RawText = raw.ToString().TrimEnd(); entries.Add(current); } + + raw.Clear().AppendLine(line); // new block starts at the [header] var name = line[1..^1].Trim(); //Skip the files own header block if (name.StartsWith("Winapp2", StringComparison.OrdinalIgnoreCase) || - name.StartsWith("version", StringComparison.OrdinalIgnoreCase)) + name.StartsWith("version", StringComparison.OrdinalIgnoreCase)) { current = null; continue; @@ -46,37 +50,42 @@ public class Winapp2Parser if (current is null) continue; + raw.AppendLine(line); // keep every key line so the source view matches the ini + var eqIdx = line.IndexOf('='); if (eqIdx < 0) continue; - var key = line[..eqIdx].Trim(); + var key = line[..eqIdx].Trim(); var value = line[(eqIdx + 1)..].Trim(); if (value.Length == 0) continue; - if (key.Equals("LangSecRef", StringComparison.OrdinalIgnoreCase)) { if (int.TryParse(value, out var n)) current.LangSecRef = n; } - else if (key.Equals("Section", StringComparison.OrdinalIgnoreCase)) current.Section = value; + if (key.Equals("LangSecRef", StringComparison.OrdinalIgnoreCase)) { if (int.TryParse(value, out var n)) current.LangSecRef = n; } + else if (key.Equals("Section", StringComparison.OrdinalIgnoreCase)) current.Section = value; else if (key.Equals("SpecialDetect", StringComparison.OrdinalIgnoreCase)) current.SpecialDetect = value; - else if (key.Equals("Warning", StringComparison.OrdinalIgnoreCase)) current.Warning = value; - else if (key.Equals("Default", StringComparison.OrdinalIgnoreCase)) current.Default = value.Equals("True", StringComparison.OrdinalIgnoreCase); - else if (RxDetect.IsMatch(key)) current.DetectKeys.Add(value); + else if (key.Equals("Warning", StringComparison.OrdinalIgnoreCase)) current.Warning = value; + else if (key.Equals("Default", StringComparison.OrdinalIgnoreCase)) current.Default = value.Equals("True", StringComparison.OrdinalIgnoreCase); + else if (RxDetect.IsMatch(key)) current.DetectKeys.Add(value); else if (RxDetectFile.IsMatch(key)) current.DetectFiles.Add(value); - else if (RxFileKey.IsMatch(key)) current.FileKeys.Add(FileKeyEntry.Parse(value)); - else if (RxRegKey.IsMatch(key)) current.RegKeys.Add(RegKeyEntry.Parse(value)); + else if (RxFileKey.IsMatch(key)) current.FileKeys.Add(FileKeyEntry.Parse(value)); + else if (RxRegKey.IsMatch(key)) current.RegKeys.Add(RegKeyEntry.Parse(value)); else if (RxExcludeKey.IsMatch(key)) current.ExcludeKeys.Add(ExcludeKeyEntry.Parse(value)); } - if (current is not null && IsValid(current)) entries.Add(current); + if (current is not null && IsValid(current)) { current.RawText = raw.ToString().TrimEnd(); entries.Add(current); } return entries; } // An entry is only useful if it can be detected AND has something to clean private static bool IsValid(CleanerEntry e) => (e.DetectKeys.Count > 0 || e.DetectFiles.Count > 0 || e.SpecialDetect is not null) && - (e.FileKeys.Count > 0 || e.RegKeys.Count > 0); + (e.FileKeys.Count > 0 || e.RegKeys.Count > 0); public async Task> ParseFileAsync(string filePath) { - var content = await File.ReadAllTextAsync(filePath); + // netstandard2.0/net48 don't have File.ReadAllTextAsync;StreamReader does the same job + string content; + using (var reader = new StreamReader(filePath)) + content = await reader.ReadToEndAsync(); return Parse(content); } } diff --git a/FluentCleaner.slnx b/FluentCleaner.slnx index 4f97570..bb65119 100644 --- a/FluentCleaner.slnx +++ b/FluentCleaner.slnx @@ -10,6 +10,8 @@ + + diff --git a/FluentCleaner/App.xaml.cs b/FluentCleaner/App.xaml.cs index 8c5cec1..a28035c 100644 --- a/FluentCleaner/App.xaml.cs +++ b/FluentCleaner/App.xaml.cs @@ -26,11 +26,28 @@ public partial class App : Application UnhandledException += (_, e) => { - e.Handled = true; // prevent silent 0xC000027B process termination - System.Diagnostics.Debug.WriteLine($"[UnhandledException] {e.Exception}"); + e.Handled = true; //stop the app from dying silently(0xC000027B) + LogCrash(e.Exception); }; } + //when the app blows up, dump it to crash.log so users can send it over. + //lives next to auto.log: %AppData%\FluentCleaner\crash.log + private static void LogCrash(Exception ex) + { + try + { + var path = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "FluentCleaner", "crash.log"); + + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.AppendAllText(path, + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}]\n{ex}\n{new string('-', 60)}\n\n"); + } + catch { /* logging must never be the thing that crashes us */ } + } + //Entry point;load settings, build the window, wire everything up. protected override void OnLaunched(LaunchActivatedEventArgs args) { diff --git a/FluentCleaner/Assets/AppIcon.avif b/FluentCleaner/Assets/AppIcon.avif deleted file mode 100644 index 5448815..0000000 Binary files a/FluentCleaner/Assets/AppIcon.avif and /dev/null differ diff --git a/FluentCleaner/Assets/Banner.avif b/FluentCleaner/Assets/Banner.avif deleted file mode 100644 index ad16e32..0000000 Binary files a/FluentCleaner/Assets/Banner.avif and /dev/null differ diff --git a/FluentCleaner/Assets/Banner.png b/FluentCleaner/Assets/Banner.png deleted file mode 100644 index 08fc1bf..0000000 Binary files a/FluentCleaner/Assets/Banner.png and /dev/null differ diff --git a/FluentCleaner/FluentCleaner.csproj b/FluentCleaner/FluentCleaner.csproj index c586e8b..3a0c033 100644 --- a/FluentCleaner/FluentCleaner.csproj +++ b/FluentCleaner/FluentCleaner.csproj @@ -15,8 +15,8 @@ None enable enable - 26.07.01 - 26.07.01 + 26.07.05 + 26.07.05 Assets\AppIcon.ico @@ -30,6 +30,24 @@ + + + + + + + + + + + + + + + diff --git a/FluentCleaner/Services/AiExplainer.cs b/FluentCleaner/Services/AiExplainer.cs index 2bd5c38..40c987b 100644 --- a/FluentCleaner/Services/AiExplainer.cs +++ b/FluentCleaner/Services/AiExplainer.cs @@ -6,70 +6,58 @@ using FluentCleaner.Models; namespace FluentCleaner.Services; -/* Talks to Groq (llama-3.3-70b) to explain a Winapp2 entry in plain English. - The real file paths and registry keys go into the prompt so the answer is actually about - what THIS entry does, not some generic Winapp2 boilerplate. - Groq has been free for me so far; grab a key at console.groq.com. - Cached in-memory so reopening the same entry is instant. */ +// Explains Winapp2 entries and generates Custom Cleaners through the provider +// selected in Settings. Results are cached per provider, language and rule. public static class AiExplainer { private static readonly HttpClient _http = new(); private static readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); + private static (string BaseUrl, string Model) ProviderInfo(string provider) => provider switch + { + "OpenAI" => ("https://api.openai.com/v1/chat/completions", "gpt-4o-mini"), + "Anthropic" => ("https://api.anthropic.com/v1/messages", "claude-haiku-4-5-20251001"), + _ => ("https://api.groq.com/openai/v1/chat/completions", "openai/gpt-oss-120b"), + }; + + private static string? ApiKey(string provider) => provider switch + { + "OpenAI" => AppSettings.Instance.OpenAiApiKey ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY"), + "Anthropic" => AppSettings.Instance.AnthropicApiKey ?? Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"), + _ => AppSettings.Instance.GroqApiKey ?? Environment.GetEnvironmentVariable("GROQ_API_KEY"), + }; + + private static bool IsAnthropic(string provider) => provider == "Anthropic"; + + public static bool HasConfiguredKey => + !string.IsNullOrWhiteSpace(ApiKey(AppSettings.Instance.AiProvider)); + + private static string ProviderMessage(string key, string provider, string detail) + { + var template = ResourceService.Get(key); + return template.Contains("{1}", StringComparison.Ordinal) + ? string.Format(template, provider, detail) + : $"{provider}: {detail}"; + } + public static async Task ExplainAsync(CleanerEntry entry) { - if (_cache.TryGetValue(entry.Name, out var cached)) + var provider = AppSettings.Instance.AiProvider; + var cacheKey = $"{provider}\n{AppSettings.Instance.Language}\n{entry.RawText}"; + if (_cache.TryGetValue(cacheKey, out var cached)) return cached; - var apiKey = AppSettings.Instance.GroqApiKey - ?? Environment.GetEnvironmentVariable("GROQ_API_KEY"); + var apiKey = ApiKey(provider); if (string.IsNullOrWhiteSpace(apiKey)) - return ResourceService.Get("AI_NoKey"); + return $"{provider}: {ResourceService.Get("AI_NoKeyShort")}"; - var prompt = BuildPrompt(entry); + var systemPrompt = "You are a Windows PC expert. Explain Winapp2 cleaner entries concisely and accurately based on the file paths and registry keys provided." + LangInstruction(); + var (text, error) = await SendChatAsync(provider, apiKey!, systemPrompt, BuildPrompt(entry), 300); + if (error != null) + return ProviderMessage("AI_ApiError", provider, error); - try - { - using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.groq.com/openai/v1/chat/completions"); - req.Headers.Add("Authorization", $"Bearer {apiKey}"); - req.Content = new StringContent( - JsonSerializer.Serialize(new - { - model = "llama-3.3-70b-versatile", - max_tokens = 300, - messages = new[] - { - new { role = "system", content = "You are a Windows PC expert. Explain Winapp2 cleaner entries concisely and accurately based on the file paths and registry keys provided." + LangInstruction() }, - new { role = "user", content = prompt } - } - }), - Encoding.UTF8, "application/json"); - - var res = await _http.SendAsync(req); - var json = await res.Content.ReadAsStringAsync(); - - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - - if (root.TryGetProperty("error", out var err)) - { - var msg = err.TryGetProperty("message", out var m) ? m.GetString() : "Unknown error"; - return ResourceService.Fmt("AI_ApiError", msg); - } - - var text = root - .GetProperty("choices")[0] - .GetProperty("message") - .GetProperty("content") - .GetString() ?? ResourceService.Get("AI_NoResponse"); - - _cache[entry.Name] = text; - return text; - } - catch (Exception ex) - { - return ResourceService.Fmt("AI_NetworkError", ex.Message); - } + _cache[cacheKey] = text!; + return text!; } // Generates a Winapp2 INI entry from a plain-English description. @@ -117,93 +105,105 @@ public static class AiExplainer " - Use Write-Host to report progress.\n" + " - NEVER use Invoke-Expression or download and execute remote code."); - // Shared HTTP helper used by GenerateEntryAsync and GenerateScriptAsync. + // Sends generation requests through the same provider selected for explanations. private static async Task GenerateAsync(string userMsg, string systemPrompt, string errorPrefix) { - var apiKey = AppSettings.Instance.GroqApiKey - ?? Environment.GetEnvironmentVariable("GROQ_API_KEY"); + var provider = AppSettings.Instance.AiProvider; + var apiKey = ApiKey(provider); if (string.IsNullOrWhiteSpace(apiKey)) - return $"{errorPrefix}{ResourceService.Get("AI_NoKeyShort")}"; + return $"{errorPrefix}{provider}: {ResourceService.Get("AI_NoKeyShort")}"; + + var (text, error) = await SendChatAsync(provider, apiKey!, systemPrompt, userMsg, 500); + return error == null + ? text! + : $"{errorPrefix}{ProviderMessage("AI_ApiError", provider, error)}"; + } + + // A small real request verifies both the key and the selected provider. + public static async Task TestKeyAsync(string apiKey, string provider) + { + var userMsg = "Describe FluentCleaner by Belim (builtbybel) in 2 short sentences. " + + "Facts: open-source, built solo, written in C# on .NET 10 and WinUI 3, native Windows UI, " + + "no telemetry, uses the Winapp2 database. Do NOT mention AI, machine learning or any " + + "AI-related features. Keep it factual." + LangInstruction(); + + var (text, error) = await SendChatAsync(provider, apiKey, systemPrompt: null, userMsg, maxTokens: 512); + return error != null ? $"✗ {error}" : $"✓ {text}"; + } + + // Groq and OpenAI use chat completions; Anthropic uses its own message shape. + private static async Task<(string? Text, string? Error)> SendChatAsync( + string provider, string apiKey, string? systemPrompt, string userMsg, int maxTokens) + { + var (baseUrl, model) = ProviderInfo(provider); try { - using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.groq.com/openai/v1/chat/completions"); - req.Headers.Add("Authorization", $"Bearer {apiKey}"); - req.Content = new StringContent( - JsonSerializer.Serialize(new + using var req = new HttpRequestMessage(HttpMethod.Post, baseUrl); + + object body; + if (IsAnthropic(provider)) + { + req.Headers.Add("x-api-key", apiKey); + req.Headers.Add("anthropic-version", "2023-06-01"); + var messages = new[] { new { role = "user", content = userMsg } }; + body = systemPrompt is null + ? new { model, max_tokens = maxTokens, messages } + : new { model, max_tokens = maxTokens, system = systemPrompt, messages }; + } + else + { + req.Headers.Add("Authorization", $"Bearer {apiKey}"); + var messages = systemPrompt == null + ? new[] { new { role = "user", content = userMsg } } + : new[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userMsg } }; + + if (provider == "Groq") { - model = "llama-3.3-70b-versatile", - max_tokens = 500, - messages = new[] + body = new { - new { role = "system", content = systemPrompt }, - new { role = "user", content = userMsg } - } - }), - Encoding.UTF8, "application/json"); + model, + max_completion_tokens = maxTokens, + reasoning_effort = "low", + reasoning_format = "hidden", + messages + }; + } + else + { + body = new { model, max_tokens = maxTokens, messages }; + } + } - var res = await _http.SendAsync(req); + req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + + using var res = await _http.SendAsync(req); var json = await res.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (root.TryGetProperty("error", out var err)) { - var msg = err.TryGetProperty("message", out var m) ? m.GetString() : "Unknown error"; - return $"{errorPrefix}Groq error: {msg}"; + var msg = err.ValueKind == JsonValueKind.Object && err.TryGetProperty("message", out var m) + ? m.GetString() : err.GetString(); + return (null, msg ?? "Unknown error"); } - return root - .GetProperty("choices")[0] - .GetProperty("message") - .GetProperty("content") - .GetString() ?? $"{errorPrefix}No response received."; + var text = IsAnthropic(provider) + ? root.GetProperty("content")[0].GetProperty("text").GetString() + : root.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString(); + + return string.IsNullOrWhiteSpace(text) + ? (null, ResourceService.Get("AI_NoResponse")) + : (text, null); } catch (Exception ex) { - return $"{errorPrefix}Could not reach Groq API: {ex.Message}"; + return (null, ProviderMessage("AI_NetworkError", provider, ex.Message)); } } - //just a quick key test;asks Groq one sentence about FluentCleaner; returns "✓ " or "✗" - public static async Task TestKeyAsync(string apiKey) - { - try - { - using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.groq.com/openai/v1/chat/completions"); - req.Headers.Add("Authorization", $"Bearer {apiKey}"); - req.Content = new StringContent( - JsonSerializer.Serialize(new - { - model = "llama-3.3-70b-versatile", - max_tokens = 150, - messages = new[] - { - new { role = "user", content = "Describe FluentCleaner by Belim (builtbybel) in 2 short sentences. " + - "Facts: open-source, built solo and fueled by coffee, written in C# on .NET 10 + WinUI 3, " + - "native Windows 11 UI, no telemetry, uses the Winapp2 database. " + - "Do NOT mention AI, machine learning or any AI-related features. Keep it factual." } - } - }), - Encoding.UTF8, "application/json"); - - var res = await _http.SendAsync(req); - var json = await res.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - - if (root.TryGetProperty("error", out var err)) - return "✗ " + (err.TryGetProperty("message", out var m) ? m.GetString() : "API error"); - - var text = root.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString(); - return "✓ " + text; - } - catch (Exception ex) { return "✗ " + ex.Message; } - } - - // Returns " Please respond in German." etc. so empty only for English. - // Falls back to the Windows UI culture when we pick System default + // Match the answer language to the active Modern UI language. private static string LangInstruction() { var lang = AppSettings.Instance.Language; @@ -221,7 +221,7 @@ public static class AiExplainer catch { return string.Empty; } } - //build a prompt with real paths so the model knows exactly what gets cleaned + // Include the real paths so the explanation is about this rule, not its title alone. private static string BuildPrompt(CleanerEntry entry) { var sb = new StringBuilder(); diff --git a/FluentCleaner/Services/AppSettings.cs b/FluentCleaner/Services/AppSettings.cs index 90b91ab..5aa41bb 100644 --- a/FluentCleaner/Services/AppSettings.cs +++ b/FluentCleaner/Services/AppSettings.cs @@ -5,14 +5,14 @@ namespace FluentCleaner.Services; public record CleanHistoryEntry(DateTime Date, long BytesFreed, int ItemsRemoved); +// App-wide settings, persisted as a single settings.json and exposed through the +// static Instance. The Classic app reads/writes the SAME file; keys it owns +// (WindowX/WindowY, …) land in Extra and get written back untouched, so a save +// here never strips them out of the shared file. public class AppSettings { - // single shared instance, loaded from disk at startup. - // NOTE: initialized in the static constructor (below), NOT with an inline "= Load()". - // Static field initializers run in textual order, and Load() depends on the path fields - // (PortablePath/SettingsFile/JsonOptions) declared further down. An inline initializer here - // would run BEFORE those fields are set, making SettingsFile null and Load() silently return - // empty defaults — which is exactly the bug that made saved settings (language!) never load. + // set in the static ctor (below), not "= Load()" here: the load path is built + // from the fields further down, which aren't set yet at this point. public static AppSettings Instance { get; private set; } = null!; // Portable mode: if settings.json sits next to the exe, use it instead of %AppData%. @@ -33,7 +33,6 @@ public class AppSettings WriteIndented = true }; - // Runs after ALL static field initializers above are set, so Load() sees valid paths. static AppSettings() => Instance = Load(); // --- persisted settings ----------------------------------------------- @@ -67,8 +66,18 @@ public class AppSettings public bool CleanHistoryEnabled { get; set; } = true; public List CleanHistory { get; set; } = []; - // Groq API key for AI entry explanations; null = not configured + // Provider used for explanations and Custom Cleaner generation. + public string AiProvider { get; set; } = "Groq"; + + // Provider keys stay separate so switching providers never overwrites another key. public string? GroqApiKey { get; set; } + public string? OpenAiApiKey { get; set; } + public string? AnthropicApiKey { get; set; } + + // Modern and Classic use separate task names and schedules. + public string ModernSchedulerFrequency { get; set; } = "Daily"; + public string ModernSchedulerTime { get; set; } = "03:00"; + public bool ModernSchedulerShutdownAfter { get; set; } // true once the user dismisses the startup donation tip public bool DonationDismissed { get; set; } = false; @@ -76,6 +85,11 @@ public class AppSettings // UI language override; "" = follow Windows, "en-US" / "de-DE" = forced public string Language { get; set; } = ""; + // Classic writes keys this app doesn't type (WindowX/WindowY). Keep them across + // a save instead of dropping them from the shared file. + [JsonExtensionData] + public Dictionary Extra { get; set; } = []; + // ----------------------------------------------------------------------- diff --git a/FluentCleaner/Services/CleaningService.cs b/FluentCleaner/Services/CleaningService.cs index 67f3ab8..3b7226a 100644 --- a/FluentCleaner/Services/CleaningService.cs +++ b/FluentCleaner/Services/CleaningService.cs @@ -28,7 +28,7 @@ public class CleaningService inflate the reported size for no reason. */ private ScanResult Analyze(CleanerEntry entry, IProgress? progress, CancellationToken token = default) { - var result = new ScanResult { Entry = entry }; + var result = new ScanResult { Entry = entry }; var excluded = BuildExclusions(entry); // Wrap the caller's progress so every path report is prefixed with the entry name. @@ -38,13 +38,17 @@ public class CleaningService IProgress? entryProgress = progress is null ? null : new PrefixedProgress(entry.Name, progress); + // dedup across FileKeys via a set;List.Contains would crawl on big entries + // (Firefox Caches alone is ~8600 files) + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var fileKey in entry.FileKeys) { try { foreach (var file in FindFiles(fileKey, excluded, entryProgress, token)) { - if (result.FilesToDelete.Contains(file)) continue; + if (!seen.Add(file)) continue; // Skip files that are truly inaccessible (hard lock / no permissions). var size = TryGetDeletableSize(file); @@ -82,7 +86,7 @@ public class CleaningService progress?.Report(dir); foreach (var f in EnumerateFilesSafe(dir, patterns, recurse, progress, token)) - if (!IsExcluded(f, excluded)) + if (!IsExcluded(f, excluded) && !IsProtected(f)) yield return f; } } @@ -111,8 +115,10 @@ public class CleaningService //C:\Users\All Users >> C:\ProgramData >>> All Users >>...forever ;) //Real content is always reachable via the canonical path;no need to follow aliases try - { dirs = Directory.EnumerateDirectories(root) - .Where(d => (File.GetAttributes(d) & FileAttributes.ReparsePoint) == 0); } + { + dirs = Directory.EnumerateDirectories(root) + .Where(d => (File.GetAttributes(d) & FileAttributes.ReparsePoint) == 0); + } catch { yield break; } foreach (var sub in dirs) @@ -128,7 +134,7 @@ public class CleaningService private static IEnumerable FindRegistryItems(RegKeyEntry regKey) { var (hive, subKey) = SplitHiveSubKey(regKey.KeyPath); - using var root = OpenHive(hive); + using var root = RegistryHelpers.OpenHive(hive); if (root is null) yield break; using var key = root.OpenSubKey(subKey, writable: false); @@ -154,7 +160,7 @@ public class CleaningService Also returns the count of successfully deleted items and the total bytes freed.*/ private (int count, long bytes) Clean(ScanResult result, IProgress? progress, CancellationToken token = default) { - int count = 0; + int count = 0; long bytes = 0; foreach (var file in result.FilesToDelete) @@ -195,7 +201,7 @@ public class CleaningService private static void DeleteRegistryItem(RegistryItemToDelete item) { var (hive, subKey) = SplitHiveSubKey(item.KeyPath); - using var root = OpenHive(hive); + using var root = RegistryHelpers.OpenHive(hive); if (root is null) return; if (item.ValueName is not null) @@ -206,7 +212,7 @@ public class CleaningService else { var parentSubKey = Path.GetDirectoryName(subKey)?.Replace('/', '\\') ?? ""; - var keyName = Path.GetFileName(subKey); + var keyName = Path.GetFileName(subKey); using var parent = root.OpenSubKey(parentSubKey, writable: true); parent?.DeleteSubKeyTree(keyName, throwOnMissingSubKey: false); // delete the whole key tree; if it's already gone, skip silently } @@ -238,7 +244,7 @@ public class CleaningService /* Turns the entry's ExcludeKey lines into rules we can actually match against during the scan. REG exclusions are skipped here;they don't apply to file paths anyway. - Global exclusions from Settings are layered on top — they override everything. */ + Global exclusions from Settings are layered on top,they override everything. */ private List BuildExclusions(CleanerEntry entry) { var rules = new List(); @@ -291,6 +297,23 @@ public class CleaningService return false; } + /* Built-in safety net;paths we never delete no matter what the database says. + These hold data that breaks apps when wiped, so a broad REMOVESELF rule + shouldn't be able to touch them. Add a line to protect more. */ + private static readonly string[] ProtectedSegments = + { + // browser extension databases (1Password, Bitwarden, uBlock filter lists…); + // normal site storage ("https_...") stays cleanable, only "chrome-extension_" is off-limits + @"\IndexedDB\chrome-extension_", + }; + + // Same list, read-only;lets the Settings page show users exactly what's protected + public static IReadOnlyList ProtectedPaths => ProtectedSegments; + + // True when the path sits under one of the protected segments + private static bool IsProtected(string path) => + ProtectedSegments.Any(s => path.Contains(s, StringComparison.OrdinalIgnoreCase)); + // Splits "HKCU\Software\Foo" into ("HKCU", "Software\Foo"). private static (string hive, string subKey) SplitHiveSubKey(string path) { @@ -298,17 +321,6 @@ public class CleaningService return idx < 0 ? (path.ToUpperInvariant(), "") : (path[..idx].ToUpperInvariant(), path[(idx + 1)..]); } - // Shared with DetectionService; maps hive abbreviations to registry root keys. Yeah, a shared RegistryHelper would be cleaner, but im too lazy here - internal static RegistryKey? OpenHive(string hive) => hive switch - { - "HKCU" or "HKEY_CURRENT_USER" => Registry.CurrentUser, - "HKLM" or "HKEY_LOCAL_MACHINE" => Registry.LocalMachine, - "HKU" or "HKEY_USERS" => Registry.Users, - "HKCC" or "HKEY_CURRENT_CONFIG" => Registry.CurrentConfig, - "HKCR" or "HKEY_CLASSES_ROOT" => Registry.ClassesRoot, - _ => null - }; - // --- Nested Types --------------------------------------------- /* One rule parsed from an ExcludeKeyN= line. diff --git a/FluentCleaner/Services/ResourceService.cs b/FluentCleaner/Services/ResourceService.cs index ca2f591..3ddc5ac 100644 --- a/FluentCleaner/Services/ResourceService.cs +++ b/FluentCleaner/Services/ResourceService.cs @@ -4,14 +4,14 @@ namespace FluentCleaner.Services; //Localized string lookup for code-side strings (status messages, dialogs). //XAML uses x:Uid directly -//New language: drop a Strings\{lang}\Resources.resw folder and rebuild. +//New language: drop a Strings\{lang}\Resources.resw folder and rebuild public static class ResourceService { private static ResourceLoader? _loader; private static ResourceLoader Loader => _loader ??= new ResourceLoader(); - // Scans Strings\{lang}\ folders next to the exe for installed languages. + //Scans Strings\{lang}\ folders next to the exe for installed languages public static IReadOnlyList GetAvailableLanguages() { var result = new List(); @@ -25,7 +25,7 @@ public static class ResourceService return result; } - //null / "" = follow Windows display language. + //null / "" = follow Windows display language public static void SetLanguage(string? lang) { try @@ -37,7 +37,7 @@ public static class ResourceService _loader = null; //force a fresh loader so lookups pick up the new language } - // Returns the key itself as fallback when a string is missing. + // Returns the key itself as fallback when a string is missing public static string Get(string key) { try @@ -49,7 +49,7 @@ public static class ResourceService catch { return key; } } - // Format-string lookup; used for interpolated status messages. + //Format-string lookup; used for interpolated status messages. // e.g. ResourceService.Fmt("St_ScanComplete", files, reg, size) public static string Fmt(string key, params object[] args) { @@ -57,4 +57,4 @@ public static class ResourceService //If Get fell back to the key itself (no braces>> not a format string), skip formatting return template == key && !template.Contains('{') ? key : string.Format(template, args); } -} +} \ No newline at end of file diff --git a/FluentCleaner/Services/SilentRunner.cs b/FluentCleaner/Services/SilentRunner.cs index d9eccdc..942b2f8 100644 --- a/FluentCleaner/Services/SilentRunner.cs +++ b/FluentCleaner/Services/SilentRunner.cs @@ -101,7 +101,11 @@ public static class SilentRunner UseShellExecute = false, CreateNoWindow = true }); - if (p is not null) await p.WaitForExitAsync(); + if (p is not null) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + try { await p.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { } + } } catch { } } diff --git a/FluentCleaner/Services/TaskSchedulerService.cs b/FluentCleaner/Services/TaskSchedulerService.cs new file mode 100644 index 0000000..ccbf806 --- /dev/null +++ b/FluentCleaner/Services/TaskSchedulerService.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; + +namespace FluentCleaner.Services; + +public enum SchedulerFrequency { Daily, Weekly, Logon } + +//Creates the Windows task that starts this executable with /AUTO. Modern uses +//its own task name so installing a schedule never replaces the Classic one +public static class TaskSchedulerService +{ + private const string TaskName = "FluentCleaner Modern AutoClean"; + + public static bool Exists() => Run("/Query", "/TN", TaskName).ExitCode == 0; + + public static (bool Ok, string Message) CreateOrUpdate( + SchedulerFrequency frequency, TimeSpan time, bool shutdownAfter) + { + var exePath = Environment.ProcessPath; + if (string.IsNullOrWhiteSpace(exePath)) + return (false, ResourceService.Get("Scheduler_ErrorExecutablePath")); + + var taskRun = $"\"{exePath}\" /AUTO{(shutdownAfter ? " /SHUTDOWN" : "")}"; + var schedule = frequency switch + { + SchedulerFrequency.Weekly => new[] { "/SC", "WEEKLY", "/D", "MON", "/ST", time.ToString(@"hh\:mm") }, + SchedulerFrequency.Logon => new[] { "/SC", "ONLOGON" }, + _ => new[] { "/SC", "DAILY", "/ST", time.ToString(@"hh\:mm") }, + }; + + var args = new List { "/Create", "/TN", TaskName, "/TR", taskRun }; + args.AddRange(schedule); + args.Add("/F"); + + var result = Run(args.ToArray()); + return result.ExitCode == 0 + ? (true, ResourceService.Get("Scheduler_ResultCreated")) + : (false, string.IsNullOrWhiteSpace(result.Output) + ? ResourceService.Get("Scheduler_ErrorSchtasks") + : result.Output.Trim()); + } + + public static (bool Ok, string Message) Delete() + { + if (!Exists()) + return (true, ResourceService.Get("Scheduler_ResultNothingToRemove")); + + var result = Run("/Delete", "/TN", TaskName, "/F"); + return result.ExitCode == 0 + ? (true, ResourceService.Get("Scheduler_ResultRemoved")) + : (false, string.IsNullOrWhiteSpace(result.Output) + ? ResourceService.Get("Scheduler_ErrorSchtasks") + : result.Output.Trim()); + } + + private static (int ExitCode, string Output) Run(params string[] args) + { + try + { + var startInfo = new ProcessStartInfo("schtasks.exe") + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + foreach (var arg in args) + startInfo.ArgumentList.Add(arg); + + using var process = Process.Start(startInfo); + if (process is null) + return (-1, ResourceService.Get("Scheduler_ErrorSchtasks")); + + var output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd(); + process.WaitForExit(); + return (process.ExitCode, output); + } + catch (Exception ex) + { + return (-1, ex.Message); + } + } +} diff --git a/FluentCleaner/Strings/de-DE/Resources.resw b/FluentCleaner/Strings/de-DE/Resources.resw index 4e13473..7c0d2f8 100644 --- a/FluentCleaner/Strings/de-DE/Resources.resw +++ b/FluentCleaner/Strings/de-DE/Resources.resw @@ -44,7 +44,7 @@ Bereinigen Mit KI erklären Quelle anzeigen - Rechtsklick auf einen Eintrag auf der Cleaner-Seite → Mit KI erklären. Powered by Groq (llama-3.3-70b) — kostenlos, keine Abrechnung. Es wird nichts ohne deine ausdrückliche Aktion gesendet. Erfordert einen eigenen API-Schlüssel. + Rechtsklick auf einen Eintrag auf der Cleaner-Seite → Mit KI erklären. Wähle Groq, OpenAI oder Anthropic. Es wird nichts ohne deine ausdrückliche Aktion gesendet. Erfordert einen eigenen API-Schlüssel. 💡 Sobald dein Schlüssel gespeichert ist, schaltet die Seite „Eigene Reiniger" ebenfalls KI frei — beschreibe was bereinigt werden soll und sie generiert direkt einen fertigen Winapp2-Eintrag oder ein PowerShell-Skript. Ergebnisse Zurück zu den Ergebnissen @@ -178,10 +178,19 @@ Verfolge, wie viel Junk sich im Laufe der Zeit ansammelt. Bereinigungsverlauf Verlauf löschen + Geplante Reinigung + Führt dieselbe Reinigung wie /AUTO über die Windows-Aufgabenplanung aus. Die Aufgabe läuft nur, während du angemeldet bist, und benötigt keine Administratorrechte. + Geplante Reinigung aktivieren + Häufigkeit + Uhrzeit + PC danach herunterfahren + Übernehmen + Windows-Aufgabenplanung öffnen KI-Erklärungen opt-in Vorschau Meinung? Zur Diskussion → + Anbieter Speichern Testen Schlüssel holen @@ -220,6 +229,17 @@ Auf Updates prüfen ⚠ Zuerst einen API-Schlüssel eingeben. Teste… + Täglich + Wöchentlich (Montag) + Bei Anmeldung + ✓ Geplante Aufgabe ist aktiv. + ✗ Nicht geplant. + Wird übernommen… + Der Pfad zur FluentCleaner-Programmdatei konnte nicht ermittelt werden. + Die geplante Aufgabe wurde erstellt. + Es ist keine geplante Aufgabe zum Entfernen vorhanden. + Die geplante Aufgabe wurde entfernt. + Die Windows-Aufgabenplanung ist fehlgeschlagen. Einstellungen exportiert nach {0} Export fehlgeschlagen: {0} Einstellungen importiert aus {0} @@ -280,10 +300,10 @@ - Kein API-Schlüssel konfiguriert. Gehe zu Einstellungen → KI-Erklärungen, um deinen Groq-API-Schlüssel hinzuzufügen (kostenlos auf console.groq.com). - Groq-API-Fehler: {0} + Für den gewählten Anbieter ist kein API-Schlüssel konfiguriert. Gehe zu Einstellungen → KI-Erklärungen, um einen hinzuzufügen. + {0}-API-Fehler: {1} Keine Antwort erhalten. - Groq-API nicht erreichbar: {0} + {0} nicht erreichbar: {1} Kein API-Schlüssel konfiguriert — gehe zu Einstellungen → KI-Erklärungen. - No API key configured. Go to Settings → AI explanations to add your Groq API key (free at console.groq.com). - Groq API error: {0} + No API key configured for the selected provider. Go to Settings → AI explanations to add one. + {0} API error: {1} No response received. - Could not reach Groq API: {0} + Could not reach {0}: {1} No API key configured — go to Settings → AI explanations. + + @@ -25,49 +26,49 @@ - - + - - - + - - - - - + + - - + - - - + + - - - - + + + + - - - - - + + + + + - - + - - - - + + + - - - - - + + + + + + + - - - - + - - - + + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + + + + + - - + - - - + - - - - + + + + + - - - + - + - - - - - - - + + + + + + - - + + - - + - - - + + + - - + - - - - - - - - + + + + + - - - - - - - - + + + + + + + + + + + - - - + - - + - - - - - - - + + + + + - - - - - - - - - - + + + + + + + - - + - - - + + - - - - - - - - + + + + + + + - - + - - + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FluentCleaner/Views/ToolsPage.xaml.cs b/FluentCleaner/Views/ToolsPage.xaml.cs deleted file mode 100644 index d213b5a..0000000 --- a/FluentCleaner/Views/ToolsPage.xaml.cs +++ /dev/null @@ -1,525 +0,0 @@ -// Imported and adapted from my Winslopr app https://github.com/builtbybel/Winslopr/blob/main/docs/extensions.md -// Jut reused here with namespace changes only. -using FluentCleaner.Tools; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Text; - -namespace FluentCleaner.Views; - -public sealed partial class ToolsPage : Page, ISearchablePage -{ - private readonly List _allTools = new(); - private readonly ObservableCollection _visibleTools = new(); - private ToolsDefinition? _selectedTool; - - private ToolsCategory _category = ToolsCategory.All; - private string _searchQuery = ""; - - // Folder name next to the exe that holds .ps1 extension scripts - private static readonly string ExtensionsDir = - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Extensions"); - - // GitHub URL where the extension pack can be downloaded - private const string ExtensionsGitHub = "https://github.com/builtbybel/FluentCleaner/releases"; - - public ToolsPage() - { - InitializeComponent(); - listTools.ItemsSource = _visibleTools; - - foreach (var cat in new[] { "All", "System", "Privacy", "Network", "Apps", "Debloat" }) - comboFilter.Items.Add(cat); - comboFilter.SelectedIndex = 0; - - ClearDetails(); - LoadToolsAsync(); - } - - // ── ISearchablePage ─────────────────────────────────────────────────────── - public void OnSearch(string text) => ApplySearch(text); - - public void ApplySearch(string query) - { - _searchQuery = query ?? ""; - ApplyFilterAndSearch(); - } - - // ---------------- Loading ---------------- - - private async void LoadToolsAsync() - { - _allTools.Clear(); - _visibleTools.Clear(); - ClearDetails(); - - // Extensions folder is optional;lets check before showing any status - if (!Directory.Exists(ExtensionsDir)) - { - ShowNoFolder(); - return; - } - - lblStatus.Text = "Loading..."; - ShowList(); - - string[] files = await Task.Run(() => Directory.GetFiles(ExtensionsDir, "*.ps1")); - - var loaded = await Task.Run(() => - { - var list = new List(); - foreach (var path in files) - { - var title = Path.GetFileNameWithoutExtension(path); - var meta = ReadMetadataFromScript(path); - list.Add(new ToolsDefinition(title, PickIconForScript(title), path, meta)); - } - return list; - }); - - _allTools.AddRange(loaded); - ApplyFilterAndSearch(); - - lblStatus.Text = _allTools.Count == 1 ? "1 extension loaded." : $"{_allTools.Count} extensions loaded."; - - if (_visibleTools.Count > 0) - listTools.SelectedIndex = 0; - } - - // ---------------- Filter / Search ---------------- - - private void comboFilter_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - _category = comboFilter.SelectedItem?.ToString() switch - { - "System" => ToolsCategory.System, - "Privacy" => ToolsCategory.Privacy, - "Network" => ToolsCategory.Network, - "Apps" => ToolsCategory.Apps, - "Debloat" => ToolsCategory.Debloat, - _ => ToolsCategory.All - }; - ApplyFilterAndSearch(); - } - - private void ApplyFilterAndSearch() - { - var q = (_searchQuery ?? "").Trim().ToLowerInvariant(); - - var filtered = _allTools - .Where(t => - (_category == ToolsCategory.All || t.Category == _category) && - (string.IsNullOrEmpty(q) || - t.Title.ToLowerInvariant().Contains(q) || - t.Description.ToLowerInvariant().Contains(q))) - .OrderBy(t => t.Title) - .ToList(); - - _visibleTools.Clear(); - foreach (var t in filtered) - _visibleTools.Add(t); - - if (_visibleTools.Count == 0) - ClearDetails(); - } - - // ---------------- Selection ---------------- - - private void listTools_SelectionChanged(object sender, SelectionChangedEventArgs e) => - SetTool(listTools.SelectedItem as ToolsDefinition); - - // ---------------- Details panel ---------------- - - private void SetTool(ToolsDefinition? tool) - { - _selectedTool = tool; - - if (tool is null) { ClearDetails(); return; } - - panelPlaceholder.Visibility = Visibility.Collapsed; - scrollDetails.Visibility = Visibility.Visible; - - lblIcon.Text = tool.Icon ?? ""; - lblTitle.Text = tool.Title ?? ""; - lblDescription.Text = tool.Description ?? ""; - progressRing.IsActive = false; - - // Options dropdown - comboOptions.Items.Clear(); - if (tool.Options.Count > 0) - { - comboOptions.Visibility = Visibility.Visible; - foreach (var opt in tool.Options) - comboOptions.Items.Add(opt); - comboOptions.SelectedIndex = 0; - } - else - { - comboOptions.Visibility = Visibility.Collapsed; - } - - // Optional text input - if (tool.SupportsInput) - { - textInput.Visibility = Visibility.Visible; - textInput.PlaceholderText = string.IsNullOrWhiteSpace(tool.InputPlaceholder) - ? "Enter input (e.g. IDs or raw arguments)" - : tool.InputPlaceholder; - textInput.Text = ""; - } - else - { - textInput.Visibility = Visibility.Collapsed; - } - - // Powered-by attribution - if (!string.IsNullOrWhiteSpace(tool.PoweredByText) && !string.IsNullOrWhiteSpace(tool.PoweredByUrl)) - { - linkPoweredBy.Content = tool.PoweredByText.Trim(); - linkPoweredBy.Tag = tool.PoweredByUrl.Trim(); - linkPoweredBy.Visibility = Visibility.Visible; - } - else - { - linkPoweredBy.Visibility = Visibility.Collapsed; - } - - // Help info bar - bool hasHelp = tool.Options.Any(o => o.Contains("help", StringComparison.OrdinalIgnoreCase)); - infoHelp.Title = "Help available"; - btnShowHelp.Content = "Show help"; - infoHelp.IsOpen = hasHelp; - - btnRun.Visibility = Visibility.Visible; - btnUninstall.Visibility = Visibility.Visible; - } - - private void ClearDetails() - { - _selectedTool = null; - - lblIcon.Text = ""; - lblTitle.Text = ""; - lblDescription.Text = ""; - - comboOptions.Visibility = Visibility.Collapsed; - comboOptions.Items.Clear(); - - textInput.Visibility = Visibility.Collapsed; - textInput.Text = ""; - - linkPoweredBy.Visibility = Visibility.Collapsed; - infoHelp.IsOpen = false; - progressRing.IsActive = false; - - btnRun.Visibility = Visibility.Collapsed; - btnUninstall.Visibility = Visibility.Collapsed; - - scrollDetails.Visibility = Visibility.Collapsed; - panelPlaceholder.Visibility = Visibility.Visible; - } - - // ---------------- Button handlers ---------------- - - private async void btnRun_Click(object sender, RoutedEventArgs e) - { - if (_selectedTool is null) return; - - if (!File.Exists(_selectedTool.ScriptPath)) - { - lblStatus.Text = "Script not found: " + _selectedTool.ScriptPath; - return; - } - - btnRun.IsEnabled = false; - btnUninstall.IsEnabled = false; - progressRing.IsActive = true; - lblStatus.Text = "Running..."; - txtLog.Text = ""; - AppendLog($"── {_selectedTool.Title} ──"); - - try - { - bool useConsole = _selectedTool.UseConsole; - bool useLog = _selectedTool.UseLog; - - string? optionArg = null; - if (comboOptions.Visibility == Visibility.Visible && comboOptions.SelectedItem is not null) - { - optionArg = comboOptions.SelectedItem.ToString()!; - - if (optionArg.EndsWith(" (console)", StringComparison.Ordinal)) - { useConsole = true; useLog = false; optionArg = optionArg[..^" (console)".Length].Trim(); } - else if (optionArg.EndsWith(" (silent)", StringComparison.Ordinal)) - { useConsole = false; useLog = false; optionArg = optionArg[..^" (silent)".Length].Trim(); } - else if (optionArg.EndsWith(" (log)", StringComparison.Ordinal)) - { useLog = true; useConsole = false; optionArg = optionArg[..^" (log)".Length].Trim(); } - } - - string? inputArg = null; - if (_selectedTool.SupportsInput && textInput.Visibility == Visibility.Visible) - { - var t = textInput.Text.Trim(); - if (!string.IsNullOrEmpty(t)) inputArg = t; - } - - var extra = new StringBuilder(); - if (!string.IsNullOrWhiteSpace(optionArg)) extra.Append(' ').Append(QuoteForPs(optionArg)); - if (!string.IsNullOrWhiteSpace(inputArg)) extra.Append(' ').Append(QuoteForPs(inputArg)); - - await RunScriptAsync(_selectedTool.ScriptPath, extra.ToString(), useConsole, AppendLog); - - lblStatus.Text = useConsole ? "Opened in console." : "Done."; - } - catch (Exception ex) - { - lblStatus.Text = "Error: " + ex.Message; - } - finally - { - progressRing.IsActive = false; - btnRun.IsEnabled = true; - btnUninstall.IsEnabled = true; - } - } - - private async void btnUninstall_Click(object sender, RoutedEventArgs e) - { - if (_selectedTool is null) return; - - if (!File.Exists(_selectedTool.ScriptPath)) - { - lblStatus.Text = "File already missing."; - ClearDetails(); - LoadToolsAsync(); - return; - } - - var dialog = new ContentDialog - { - Title = "Remove script", - Content = $"Remove \"{_selectedTool.Title}\" from the Extensions folder?", - PrimaryButtonText = "Remove", - CloseButtonText = "Cancel", - XamlRoot = XamlRoot - }; - - if (await dialog.ShowAsync() != ContentDialogResult.Primary) return; - - try - { - File.Delete(_selectedTool.ScriptPath); - ClearDetails(); - LoadToolsAsync(); - } - catch (Exception ex) - { - lblStatus.Text = "Could not delete: " + ex.Message; - } - } - - private void btnOpenFolder_Click(object sender, RoutedEventArgs e) - { - // Open the Extensions folder if it exists, otherwise open the app folder - var target = Directory.Exists(ExtensionsDir) - ? ExtensionsDir - : AppDomain.CurrentDomain.BaseDirectory; - try { Process.Start("explorer.exe", target); } - catch { } - } - - private void btnGitHub_Click(object sender, RoutedEventArgs e) - { - try { Process.Start(new ProcessStartInfo(ExtensionsGitHub) { UseShellExecute = true }); } - catch { } - } - - // ---------------- Empty state helpers ---------------- - - private void ShowNoFolder() - { - panelNoFolder.Visibility = Visibility.Visible; - panelPlaceholder.Visibility = Visibility.Collapsed; // don't show both at once - listTools.Visibility = Visibility.Collapsed; - borderOutput.Visibility = Visibility.Collapsed; - comboFilter.Visibility = Visibility.Collapsed; - lblStatus.Text = ""; - } - - private void ShowList() - { - panelNoFolder.Visibility = Visibility.Collapsed; - listTools.Visibility = Visibility.Visible; - - borderOutput.Visibility = Visibility.Visible; - } - - private async void btnShowHelp_Click(object sender, RoutedEventArgs e) - { - if (_selectedTool is null) return; - var helpOpt = _selectedTool.Options - .FirstOrDefault(o => o.Contains("help", StringComparison.OrdinalIgnoreCase)); - if (helpOpt is null) return; - - txtLog.Text = ""; - AppendLog($"── {_selectedTool.Title} — Help ──"); - await RunScriptAsync(_selectedTool.ScriptPath, " " + QuoteForPs(helpOpt), false, AppendLog); - } - - private void btnClearLog_Click(object sender, RoutedEventArgs e) => - txtLog.Text = ""; - - private void linkPoweredBy_Click(object sender, RoutedEventArgs e) - { - var url = linkPoweredBy.Tag?.ToString(); - if (string.IsNullOrWhiteSpace(url)) return; - try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } - catch { } - } - - // ---------------- Log output ---------------- - - private void AppendLog(string line) => - DispatcherQueue.TryEnqueue(() => - { - txtLog.Text += line + "\n"; - txtLog.SelectionStart = txtLog.Text.Length; - }); - - // ---------------- Script execution ---------------- - - // onOutput is called live for every stdout/stderr line. - // Pass AppendLog to stream output into the log panel. - private static Task RunScriptAsync(string scriptPath, string extraArgs, bool useConsole, - Action? onOutput = null) => - Task.Run(() => - { - var args = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"{extraArgs}"; - - if (useConsole) - { - Process.Start(new ProcessStartInfo("powershell.exe", "-NoExit " + args) - { UseShellExecute = true }); - return; - } - - var psi = new ProcessStartInfo("powershell.exe", args) - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var p = new Process { StartInfo = psi }; - p.OutputDataReceived += (_, ev) => { if (!string.IsNullOrEmpty(ev.Data)) onOutput?.Invoke(ev.Data); }; - p.ErrorDataReceived += (_, ev) => { if (!string.IsNullOrEmpty(ev.Data)) onOutput?.Invoke("ERR: " + ev.Data); }; - p.Start(); - p.BeginOutputReadLine(); - p.BeginErrorReadLine(); - p.WaitForExit(); - }); - - private static string QuoteForPs(string? value) - { - if (value is null) return "\"\""; - return "\"" + value.Replace("\"", "\\\"") + "\""; - } - - // ---------------- Metadata parsing ---------------- - - private static ScriptMeta ReadMetadataFromScript(string scriptPath) - { - string description = "No description available."; - var options = new List(); - var category = ToolsCategory.All; - bool useConsole = false, useLog = false, inputEnabled = false; - string inputPh = "", poweredByText = "", poweredByUrl = ""; - - try - { - foreach (var line in File.ReadLines(scriptPath).Take(15)) - { - if (string.IsNullOrWhiteSpace(line)) continue; - - if (line.StartsWith("# Description:", StringComparison.OrdinalIgnoreCase)) - description = line[14..].Trim(); - else if (line.StartsWith("# Category:", StringComparison.OrdinalIgnoreCase)) - category = line[11..].Trim().ToLowerInvariant() switch - { - "system" => ToolsCategory.System, - "privacy" => ToolsCategory.Privacy, - "network" => ToolsCategory.Network, - "apps" => ToolsCategory.Apps, - "debloat" => ToolsCategory.Debloat, - _ => ToolsCategory.All - }; - else if (line.StartsWith("# Options:", StringComparison.OrdinalIgnoreCase)) - options = line[10..].Split(';').Select(x => x.Trim()).Where(x => x.Length > 0).ToList(); - else if (line.StartsWith("# Host:", StringComparison.OrdinalIgnoreCase)) - { - var raw = line[7..].Trim().ToLowerInvariant(); - useConsole = raw == "console"; - useLog = raw == "log"; - } - else if (line.StartsWith("# Input:", StringComparison.OrdinalIgnoreCase)) - inputEnabled = line[8..].Trim().ToLowerInvariant() is "true" or "yes" or "1"; - else if (line.StartsWith("# InputPlaceholder:", StringComparison.OrdinalIgnoreCase)) - inputPh = line[19..].Trim(); - else if (line.StartsWith("# PoweredBy:", StringComparison.OrdinalIgnoreCase)) - poweredByText = line[12..].Trim(); - else if (line.StartsWith("# PoweredUrl:", StringComparison.OrdinalIgnoreCase)) - poweredByUrl = line[13..].Trim(); - else if (line.StartsWith('#') && description == "No description available.") - description = line.TrimStart('#').Trim(); - } - } - catch { } - - return new ScriptMeta - { - Description = description, - Options = options, - Category = category, - UseConsole = useConsole, - UseLog = useLog, - SupportsInput = inputEnabled, - InputPlaceholder = inputPh, - PoweredByText = poweredByText, - PoweredByUrl = poweredByUrl - }; - } - - // Maps keywords in script names to emoji icons - private static readonly Dictionary _iconMap = new() - { - ["debloat"] = "🧹", - ["network"] = "🌐", - ["explorer"] = "📂", - ["update"] = "🔄", - ["context"] = "📋", - ["backup"] = "💾", - ["security"] = "🛡️", - ["performance"] = "⚡", - ["privacy"] = "🔒", - ["app"] = "📦", - ["setup"] = "⚙️", - ["restore"] = "♻️", - ["cache"] = "🗑️", - ["defender"] = "🛡️", - ["power"] = "🔌", - ["install"] = "📥", - ["boot"] = "🚀", - ["clean"] = "🧼" - }; - - private static string PickIconForScript(string name) - { - name = (name ?? "").ToLowerInvariant(); - foreach (var kv in _iconMap) - if (name.Contains(kv.Key)) return kv.Value; - return "🔧"; - } -} diff --git a/FluentCleaner/run.ps1 b/FluentCleaner/run.ps1 deleted file mode 100644 index f26ef61..0000000 --- a/FluentCleaner/run.ps1 +++ /dev/null @@ -1,119 +0,0 @@ -#Requires -Version 5.1 -param( - [ValidateSet('Debug', 'Release')] - [string]$Configuration = 'Release' -) - -$ErrorActionPreference = 'Stop' - -if ($IsLinux -or $IsMacOS) { - Write-Host '[!!] FluentCleaner is only available on Windows.' -ForegroundColor Red - exit 1 -} - -# -- Check Windows App Runtime ------------------------------------------------ -Write-Host '' -Write-Host '[FluentCleaner] Checking dependencies...' -ForegroundColor Cyan - -function Get-WindowsAppRuntimeVersion { - # Method 1: Check via Get-AppxPackage for Microsoft.WindowsAppRuntime - try { - $packages = Get-AppxPackage -Name 'Microsoft.WindowsAppRuntime.*' -ErrorAction SilentlyContinue - if ($packages) { - $versions = $packages | ForEach-Object { - try { [version]$_.Version } catch { $null } - } | Where-Object { $_ } | Sort-Object -Descending - if ($versions) { return $versions[0] } - } - } catch {} - - # Method 2: Check registry subkeys for versioned installations - try { - $regBase = 'HKLM:\SOFTWARE\Microsoft\WindowsAppSDK' - if (Test-Path $regBase) { - $subkeys = Get-ChildItem $regBase -ErrorAction SilentlyContinue - $versions = $subkeys | ForEach-Object { - try { [version]$_.PSChildName } catch { $null } - } | Where-Object { $_ } | Sort-Object -Descending - if ($versions) { return $versions[0] } - } - } catch {} - - # Method 3: winget fallback - try { - $line = winget list --id Microsoft.WindowsAppRuntime 2>$null | - Select-String 'Microsoft.WindowsAppRuntime' | - Select-Object -First 1 - if ($line) { - $match = [regex]::Match($line, '(\d+\.\d+\.\d+)') - if ($match.Success) { return [version]$match.Value } - } - } catch {} - - return $null -} - -$minVersion = [version]'1.8' -$runtimeVersion = Get-WindowsAppRuntimeVersion - -if (-not $runtimeVersion) { - Write-Host ' Windows App Runtime ..... NOT FOUND' -ForegroundColor Red - Write-Host '' - Write-Host '[FluentCleaner] Windows App Runtime 1.8 or higher is required.' -ForegroundColor Yellow - Write-Host ' Download from: https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads' -ForegroundColor Cyan - Write-Host ' Or use: https://aka.ms/winappsdk (GitHub)' -ForegroundColor Cyan - Read-Host 'Press Enter to exit' - exit 1 -} elseif ($runtimeVersion -lt $minVersion) { - Write-Host " Windows App Runtime ..... v$runtimeVersion (too old)" -ForegroundColor Red - Write-Host '' - Write-Host "[FluentCleaner] Windows App Runtime $minVersion or higher is required (found v$runtimeVersion)." -ForegroundColor Yellow - Write-Host ' Download from: https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads' -ForegroundColor Cyan - Read-Host 'Press Enter to exit' - exit 1 -} else { - Write-Host " Windows App Runtime ..... v$runtimeVersion OK" -ForegroundColor Green -} - -$projectRoot = $PSScriptRoot -$projectFile = Join-Path $projectRoot 'FluentCleaner.csproj' -$projectXml = [xml](Get-Content $projectFile) - -$tfm = $projectXml.Project.PropertyGroup.TargetFramework | - Where-Object { $_ } | - Select-Object -First 1 - -$assemblyName = $projectXml.Project.PropertyGroup.AssemblyName | - Where-Object { $_ } | - Select-Object -First 1 - -if (-not $assemblyName) { - $assemblyName = 'FluentCleaner' -} - -$platform = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq - [System.Runtime.InteropServices.Architecture]::Arm64) { 'arm64' } else { 'x64' } -$runtimeIdentifier = if ($platform -eq 'arm64') { 'win-arm64' } else { 'win-x64' } -$exePath = Join-Path $projectRoot "bin\$platform\$Configuration\$tfm\$runtimeIdentifier\$assemblyName.exe" - -Write-Host '' -Write-Host "[FluentCleaner] Building $Configuration for $runtimeIdentifier..." -ForegroundColor Cyan -Write-Host '' - -dotnet build $projectFile -c $Configuration -p:Platform=$platform --nologo -v minimal - -if ($LASTEXITCODE -ne 0) { - Write-Host '' - Write-Host '[!!] Build failed. See the output above for details.' -ForegroundColor Red - exit $LASTEXITCODE -} - -if (-not (Test-Path $exePath)) { - Write-Host '' - Write-Host "[!!] Build succeeded, but the app was not found at:`n $exePath" -ForegroundColor Red - exit 1 -} - -Write-Host '' -Write-Host '[FluentCleaner] Launching app...' -ForegroundColor Green -Start-Process -FilePath $exePath diff --git a/Winapp2.ini b/Winapp2.ini index a26cf32..0178597 100644 --- a/Winapp2.ini +++ b/Winapp2.ini @@ -427,7 +427,6 @@ LangSecRef=3006 DetectFile=%LocalAppData%\Microsoft\Edge* FileKey1=%LocalAppData%\Microsoft\Edge*\User Data\*\Network|Cookies*;Device Bound Sessions* Default=false -Default=false [Microsoft Edge Web Browsing History *] LangSecRef=3006