From dda7b5dfb9bbc46c84e821636d41b05abd5e140a Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 3 May 2026 20:15:19 +0800 Subject: [PATCH 01/24] =?UTF-8?q?i18n(no):=20complete=20Norwegian=20Bokm?= =?UTF-8?q?=C3=A5l=20translation=20from=2019%=20to=2097%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WPF: 1012 strings translated (coverage 19.1% → 97.3%, 1258/1293) - Lib: 5 strings translated (coverage 91.1%, 112/123) - Fix ca Macro: Keyboard→Teclat, Mouse→Ratolí - Add Tools/translate_resx.py for resx extract/apply workflow - 35 remaining English-identical strings are technical abbreviations (CPU, GPU, GHz, RPM, etc.) intentionally kept in English - Zero missing keys, zero placeholder mismatches Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Resources/Resource.ca.resx | 121 +- .../Resources/Resource.no.resx | 8 +- .../Resources/Resource.no.resx | 2118 +++++++++-------- Tools/translate_resx.py | 117 + 4 files changed, 1250 insertions(+), 1114 deletions(-) create mode 100644 Tools/translate_resx.py diff --git a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx index 0eb7f0066..e277877dc 100644 --- a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx +++ b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx @@ -1,91 +1,34 @@ - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx @@ -99,9 +42,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Keyboard + Teclat - Mouse + Ratolí \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib/Resources/Resource.no.resx b/LenovoLegionToolkit.Lib/Resources/Resource.no.resx index 5619820e8..58c217c9b 100644 --- a/LenovoLegionToolkit.Lib/Resources/Resource.no.resx +++ b/LenovoLegionToolkit.Lib/Resources/Resource.no.resx @@ -1,4 +1,4 @@ - + @@ -273,13 +273,13 @@ Topp til bunn - Always + Alltid Lydsprett - Audio Ripple + Lydbølge Aurora Sync @@ -303,7 +303,7 @@ Regnbuebølge - Ripple + Bølge Glatt diff --git a/LenovoLegionToolkit.WPF/Resources/Resource.no.resx b/LenovoLegionToolkit.WPF/Resources/Resource.no.resx index 903bc1b88..6e1b6935e 100644 --- a/LenovoLegionToolkit.WPF/Resources/Resource.no.resx +++ b/LenovoLegionToolkit.WPF/Resources/Resource.no.resx @@ -1,4 +1,4 @@ - + @@ -215,7 +215,7 @@ Bare handlinger som samsvarer med gjeldende status, kjøres. Kl. __PH0 __:{1:D2} - Power Mode + Strømmodus Uten navn @@ -342,7 +342,7 @@ Denne gangen er det kanskje ikke nøyaktig hvis den bærbare PC-en ble ladet nå Lukk - Blue + Blå Grønn @@ -405,10 +405,10 @@ ADVARSEL: Denne handlingen vil ikke kjøres riktig hvis det interne displayet er BIOS versjon - Kopiert til utklippstavle + Tekst kopiert til utklippstavle. - Kopiert til utklippstavle + "{0}" kopiert til utklippstavle Kopiert! @@ -693,37 +693,37 @@ ADVARSEL: Denne handlingen vil ikke kjøres riktig hvis det interne displayet er - Programvaren vil kjøre i begrenset modus - - Some features will be disabled (Dashboard, Automation, Macros) + - Noen funksjoner vil bli deaktivert (Dashbord, Automatisering, Makroer) - - Other basic features will still be available + - Andre grunnleggende funksjoner vil fortsatt være tilgjengelige - Device Information: + Enhetsinformasjon: - Vendor: + Leverandør: - Model: + Modell: - Machine Type: + Maskintype: - Click "OK" to continue, or "Cancel" to exit. + Klikk "OK" for å fortsette, eller "Avbryt" for å avslutte. - No compatible keyboards found + Ingen kompatible tastaturer funnet - Keyboard Backlight + Tastaturbelysning - Keyboard backlight cannot be controlled in here when Lenovo Vantage or its services are running. + Tastaturbelysning kan ikke styres her når Lenovo Vantage eller dens tjenester kjører. - Lenovo Vantage or its services are running + Lenovo Vantage eller dens tjenester kjører Om @@ -735,236 +735,236 @@ ADVARSEL: Denne handlingen vil ikke kjøres riktig hvis det interne displayet er Batteri - Dashboard + Dashbord Tastatur - Downloads + Nedlastinger - System optimization + Systemoptimalisering - Settings + Innstillinger - Plugin Extensions + Utvidelser - Navigation Items Settings + Innstillinger for navigasjonselementer - Configure which navigation items are displayed in the sidebar. Dashboard and Settings will always be visible. + Konfigurer hvilke navigasjonselementer som vises i sidepanelet. Dashbord og Innstillinger vil alltid være synlige. - Show or hide the Keyboard navigation item + Vis eller skjul navigasjonselementet for Tastatur - Show or hide the Battery navigation item + Vis eller skjul navigasjonselementet for Batteri - Show or hide the Automation navigation item + Vis eller skjul navigasjonselementet for Automatisering - Show or hide the Macro navigation item + Vis eller skjul navigasjonselementet for Makro - Show or hide the System Optimization navigation item + Vis eller skjul navigasjonselementet for Systemoptimalisering - Show or hide the Plugin Extensions navigation item + Vis eller skjul navigasjonselementet for Utvidelser - Show or hide the About navigation item + Vis eller skjul navigasjonselementet for Om - Navigation Items + Navigasjonselementer - Configure which navigation items are displayed in the sidebar + Konfigurer hvilke navigasjonselementer som vises i sidepanelet - Update available! + Oppdatering tilgjengelig! - Update {0} available! + Oppdatering {0} tilgjengelig! - Move down + Flytt ned - Move up + Flytt opp - No + Nei - AC adapter connected + Strømadapter tilkoblet - AC adapter connected (Low wattage) + Strømadapter tilkoblet (Lav effekt) - AC adapter disconnected + Strømadapter frakoblet - Camera off + Kamera av - Camera on + Kamera på - Caps Lock off + Caps Lock av - Caps Lock on + Caps Lock på - Fn Lock off + Fn Lock av - Fn Lock on + Fn Lock på - Microphone off + Mikrofon av - Microphone on + Mikrofon på - Num Lock off + Num Lock av - Num Lock on + Num Lock på - Backlight {0} + Bakgrunnsbelysning {0} - Brightness {0} + Lysstyrke {0} - Profile {0} + Profil {0} - Touchpad off + Pekplate av - Touchpad on + Pekplate på - Backlight {0} + Bakgrunnsbelysning {0} Strømadapter - Camera + Kamera - Caps Lock and Num Lock + Caps Lock og Num Lock - Disables notification for Fn keys actions like microphone mute. -Enable this option if you have conflicts with full screen applications. + Deaktiverer varsler for Fn-tasthandlinger som mikrofondemping. +Aktiver dette alternativet hvis du har konflikter med fullskjermprogrammer. - Don't show notifications + Ikke vis varsler Fn-lås - Keyboard Backlight + Tastaturbelysning - Microphone + Mikrofon - Position + Posisjon - Power Mode + Strømmodus - Refresh Rate + Oppdateringsfrekvens - Smart Key + Smart-tast - Notifications + Varsler - Touchpad Lock + Pekplatelås OK - Open + Åpne - Improve response time of the built-in display. + Forbedre responstiden til den innebygde skjermen. Over Drive - Improve response time of the built-in display. + Forbedre responstiden til den innebygde skjermen. Over Drive - {0} downloaded! + {0} lastet ned! - Download complete + Nedlasting fullført - Something went wrong + Noe gikk galt - Server returned code 404. + Serveren returnerte kode 404. - The file seems to be gone + Filen ser ut til å være borte - Check if your internet connection is up and running. + Sjekk om internettilkoblingen din fungerer. - Something went wrong + Noe gikk galt - Update available! + Oppdatering tilgjengelig! - This package is more than a year old. + Denne pakken er mer enn ett år gammel. Versjon - Below is a list of software packages that are marked as compatible with your laptop. -Always make sure that you are installing correct packages. + Nedenfor er en liste over programvarepakker som er merket som kompatible med din bærbare datamaskin. +Sørg alltid for at du installerer riktige pakker. - Check if Machine Type and OS are set correctly. + Sjekk om maskintype og operativsystem er satt riktig. - Something went wrong + Noe gikk galt - Changing sorting, filtering results, hiding packages or refreshing the list will stop the download. Do you want to continue? + Endring av sortering, filtrering av resultater, skjuling av pakker eller oppdatering av listen vil stoppe nedlastingen. Vil du fortsette? - Download in progress + Nedlasting pågår Filter... @@ -973,86 +973,86 @@ Always make sure that you are installing correct packages. Maskintype - Operating System + Operativsystem - Primary (Recommended) + Primær (Anbefalt) - Same as Lenovo Vantage + Samme som Lenovo Vantage - Secondary + Sekundær - Same as Lenovo PC Support + Samme som Lenovo PC Support - Category + Kategori - Date + Dato - Name + Navn - Source + Kilde - Downloads + Nedlastinger - Paste + Lim inn - Change performance mode. + Endre ytelsesmodus. - Power Mode + Strømmodus - Change performance mode. -Performance mode can also be changed with Fn+Q. + Endre ytelsesmodus. +Ytelsesmodus kan også endres med Fn+Q. - Settings + Innstillinger - Power Mode + Strømmodus - Selected power mode may not work correctly without AC adapter connected. + Valgt strømmodus kan ikke fungere korrekt uten at AC-adapter er tilkoblet. - Default + Standard - Windows Power Plans + Windows strømplaner - Scanning for available driver packages, please wait... + Søker etter tilgjengelige driverpakker, vennligst vent... - Network error occurred. Retrying automatically... + Nettverksfeil oppstod. Prøver automatisk på nytt... - Retrying to fetch driver packages... + Prøver å hente driverpakker på nytt... - Cleanup Info + Oppryddingsinformasjon - View cleanup operation progress and estimated size. + Vis fremdrift for oppryddingsoperasjon og estimert størrelse. - Adjust keyboard backlight preset. + Juster forhåndsinnstilling for tastaturbaklys. - Keyboard backlight + Tastaturbaklys - Brightness + Lysstyrke Forhåndsinnstilling 1: @@ -1070,881 +1070,878 @@ Performance mode can also be changed with Fn+Q. Av - Zone 1 + Sone 1 - Zone 2 + Sone 2 - Zone 3 + Sone 3 - Zone 4 + Sone 4 - Effect + Effekt - Speed + Hastighet - Readme + Lesmeg - Refresh + Oppdater - Change refresh rate of the built-in display. + Endre oppdateringsfrekvens for innebygd skjerm. -WARNING: This action will not run correctly, if internal display is off. +ADVARSEL: Denne handlingen vil ikke kjøre korrekt hvis intern skjerm er av. - Refresh rate + Oppdateringsfrekvens - Change refresh rate of the built-in display. + Endre oppdateringsfrekvens for innebygd skjerm. - Refresh rate + Oppdateringsfrekvens Gi nytt navn - I will restart later + Jeg starter på nytt senere - Restart now + Start på nytt nå - Revert + Tilbakestill - Arguments + Argumenter - Executable Path + Kjørbars sti - Run a script or a program. -Make sure that your script runs correctly first. + Kjør et script eller et program. +Sørg for at ditt script kjører korrekt først. - Run + Kjør - Save + Lagre - Saving... + Lagrer... - Choose one or more quick actions to cycle through + Velg én eller flere hurtighandlinger å bla gjennom - No Quick Actions defined. + Ingen hurtighandlinger definert. - Show this app + Vis denne appen - Change the accent color of the app. + Endre accentfarge for appen. - Accent color + Accentfarge - Start minimized to tray when you log in to Windows. + Start minimert til systemkurven når du logger inn på Windows. - Autorun + Autostart - Start minimized to tray when you sign in to Windows. + Start minimert til systemkurven når du logger på Windows. - Legion Zone may have not been disabled correctly + Legion Zone kan hende ikke ble deaktivert korrekt - Couldn't disable Legion Zone + Kunne ikke deaktivere Legion Zone - Disable Legion Zone and its service without uninstalling it. -Restart is recommended after changing this option. + Deaktiver Legion Zone og tjenesten uten å avinstallere den.\nOmstart anbefales etter endring av dette alternativet. - Disable Legion Zone + Deaktiver Legion Zone - Lenovo Hotkeys may have not been disabled correctly + Lenovo Hotkeys er kanskje ikke deaktivert korrekt - Couldn't disable Lenovo Hotkeys + Kunne ikke deaktivere Lenovo Hotkeys - Disable Lenovo Hotkeys and its service without uninstalling it. -If disabled, this app will handle Fn shortcuts. -Restart is recommended after changing this option. + Deaktiver Lenovo Hotkeys og tjenesten uten å avinstallere den.\nHvis deaktivert, vil denne appen håndtere Fn-snarveier.\nOmstart anbefales etter endring av dette alternativet. - System optimization + Systemoptimalisering - Apply recommended performance tweaks and clean temporary files. + Bruk anbefalte ytelsesforbedringer og rens midlertidige filer. - Apply performance & service tweaks + Bruk ytelses- og tjenesteforbedringer - Performance optimizations applied. + Ytelsesoptimaliseringer brukt. - Couldn't apply performance optimizations. + Kunne ikke bruke ytelsesoptimaliseringer. - Run cleanup + Kjør opprydding - Cleanup completed. + Opprydding fullført. - Cleanup failed. + Opprydding mislyktes. - These actions modify system services and files. Administrator privileges may be required. + Disse handlingene endrer systemtjenester og filer. Administratorrettigheter kan være nødvendige. - Apply selected actions + Bruk valgte handlinger - Select recommended + Velg anbefalte - Clear selection + Fjern valg - Scan + Skann - Run cleanup actions + Kjør oppryddingshandlinger - {0} actions selected + {0} handlinger valgt - No actions selected yet. + Ingen handlinger valgt ennå. - Selected actions + Valgte handlinger - Cleanup + Opprydding - Beautification + Forbedring - Driver Download + Drivernedlasting - Safety Confirmation + Sikkerhetsbekreftelse - System cleanup will permanently delete temporary files and cache. While usually safe, it is recommended to ensure you have backups of important data. Do you want to continue? + Systemopprydding vil slette midlertidige filer og buffer permanent. Selv om det vanligvis er trygt, anbefales det å sikre at du har sikkerhetskopier av viktige data. Vil du fortsette? - Extensions + Utvidelser - Manage system optimization extensions and tools + Administrer utvidelser og verktøy for systemoptimalisering - No Extensions Available + Ingen utvidelser tilgjengelig - Install extensions from the Plugin Extensions page to enhance system optimization functionality. + Installer utvidelser fra utvidelsessiden for å forbedre systemoptimaliseringsfunksjonaliteten. - Coming Soon + Kommer snart - Extensions marketplace will be available in a future update. + Utvidelsesmarkedsplassen vil være tilgjengelig i en fremtidig oppdatering. - Windows optimization + Windows-optimalisering - Select all + Velg alle - Custom cleanup rules + Egendefinerte oppryddingsregler - Choose folders and file extensions that should be deleted during cleanup. + Velg mapper og filtyper som skal slettes under opprydding. - Add rule + Legg til regel Rediger - Remove + Fjern - Clear all + Fjern alle - No custom cleanup rules configured. + Ingen egendefinerte oppryddingsregler konfigurert. - Include subfolders + Inkluder undermapper - AppX management + AppX-administrasjon - Select preinstalled Microsoft Store apps to remove for all users. + Velg forhåndsinstallerte Microsoft Store-apper å fjerne for alle brukere. - No apps selected for removal. + Ingen apper valgt for fjerning. - Uninstall selected apps + Avinstaller valgte apper - Selected apps scheduled for removal. + Valgte apper planlagt for fjerning. - Failed to remove selected apps. + Kunne ikke fjerne valgte apper. - Loading apps, please wait... + Laster apper, vennligst vent... - Loading plugins... + Laster plugins... - Failed to load: {0} + Kunne ikke laste: {0} - Publisher: {0} + Utgiver: {0} - Registry MRU/history cleanup + Register MRU-/historikkopprydding - Clear common user registry histories (Run MRU, RecentDocs, Open/Save dialogs, TypedPaths). This does not remove system-critical entries. + Tøm vanlige brukerregisterlogger (Kjør MRU, Nylige dokumenter, Åpne/Lagre-dialoger, Inntastede stier). Dette fjerner ikke systemkritiske oppføringer. Handlinger - Custom cleanup rule + Egendefinert opprenskingsregel - Folder + Mappe - Extensions + Utvidelser - Separate multiple extensions with commas (example: tmp, log, bak) + Skill flere utvidelser med komma (eksempel: tmp, log, bak) - Include subfolders + Inkluder undermapper - Browse… + Bla gjennom… - Please choose a folder. + Velg en mappe. - Please enter at least one file extension. + Skriv inn minst én filutvidelse. - Please select at least one action. + Velg minst én handling. - Selected actions completed successfully. + Valgte handlinger ble fullført. - Failed to complete selected actions. + Kunne ikke fullføre valgte handlinger. - Explorer & interface + Utforsker og grensesnitt - Windows Beautification + Windows-oppgradering - Change right-click menu style. + Endre stil for høyreklikkmeny. - Switch to modern menu + Bytt til moderne meny - Applies the classic style and restarts Explorer. + Bruker klassisk stil og starter Utforsker på nytt. - Switch to default right-click menu + Bytt til standard høyreklikkmeny - Restores the default style and restarts Explorer. + Gjenoppretter standard stil og starter Utforsker på nytt. - Mouse appearance + Musutseende - Apply custom cursor styles and follow system light/dark theme. + Bruk egendefinerte markørstiler og føg systemets lys/mørk-tema. - Enable theme-following cursor style + Aktiver temafølgened markørstil - Detect current Windows light/dark mode and apply matching custom cursor scheme. + Oppdag gjeldende Windows lys/mørk-modus og bruk samsvarende egendefinert markøroppsett. - Disable theme-following cursor style + Deaktiver temafølgened markørstil - Restore backed-up cursor scheme and stop theme-based custom cursor switching. + Gjenopprett sikkerhetskopiert markøroppsett og stopp temabasert bytting av egendefinert markør. - Apply Modern Context Menu + Bruk moderne kontekstmeny - Completely uninstall shell.exe and related files + Avinstaller shell.exe og tilhørende filer fullstendig - Shell Integration + Shell-integrasjon - Enhanced Windows Shell integration with context menu beautification and mouse style customization + Forbedret Windows Shell-integrasjon med oppgradering av kontekstmeny og tilpasning av musstil - Enable Modern Context Menu + Aktiver moderne kontekstmeny - Enable Nilesoft Shell to beautify Windows context menu with modern style + Aktiver Nilesoft Shell for å oppgradere Windows kontekstmeny med moderne stil - Disable Modern Context Menu + Deaktiver moderne kontekstmeny - Unregister Nilesoft Shell and restore default Windows context menu + Avregistrer Nilesoft Shell og gjenopprett standard Windows kontekstmeny - Apply Windows 11 Cursor Style + Bruk Windows 11-markørstil - Apply Windows 11 style cursor theme with high DPI support + Bruk Windows 11-stil markørtema med støtte for høy DPI - Select the theme for the context menu. + Velg tema for kontekstmenyen. - Enable transparency effects for the context menu. + Aktiver gjennomsiktighetseffekter for kontekstmenyen. - Customize the appearance of the context menu. + Tilpass utseendet til kontekstmenyen. - Shell.exe not found. Please make sure Nilesoft Shell is installed. + Shell.exe ble ikke funnet. Sørg for at Nilesoft Shell er installert. - Configuration file not found. Please make sure Nilesoft Shell is installed. + Konfigurasjonsfil ble ikke funnet. Sørg for at Nilesoft Shell er installert. - Failed to enable classic menu. + Kunne ikke aktivere klassisk meny. - Failed to restore default menu. + Kunne ikke gjenopprette standard meny. - Failed to install shell. + Kunne ikke installere shell. - Failed to uninstall shell. + Kunne ikke avinstallere shell. - Failed to apply style settings. + Kunne ikke bruke stilinnstillinger. - System beautification + Systemoppgradering - Tune taskbar, File Explorer and user interface behaviour. + Juster oppgavelinje, Filutforsker og brukergrensesnittets oppførsel. Produktbeskrivelse - Adjust system performance and responsiveness settings. + Juster systemytelse og innstillinger for responsivitet. - Services + Tjenester - Disable background services that consume resources. + Deaktiver bakgrunnstjenester som bruker ressurser. - Network Acceleration + Nettverksakselerasjon - Optimize network settings to improve network speed and responsiveness. + Optimaliser nettverksinnstillinger for å forbedre nettverkshastighet og responsivitet. - TCP/IP and DNS Optimization + TCP/IP- og DNS-optimalisering - Optimize TCP/IP parameters and DNS cache settings to improve connection speed and responsiveness. + Optimaliser TCP/IP-parametere og DNS-bufferinnstillinger for å forbedre tilkoblingshastighet og responsivitet. - Reset Network Configuration + Tilbakestill nettverkskonfigurasjon - Flush DNS cache and reset Winsock and TCP/IP stack configuration to repair network connectivity issues. + Tøm DNS-buffer og tilbakestill Winsock og TCP/IP-stackkonfigurasjon for å reparere nettverkstilkoblingsproblemer. - Cleanup + Opprydding - Remove caches, temporary files and other leftovers. + Fjern buffere, midlertidige filer og andre rester. - Cache Cleanup + Bufferopprydding - Clean up cache files from various applications and the system to free up storage space. + Rydd opp i bufferfiler fra ulike applikasjoner og systemet for å frigjøre lagringsplass. - System Files Cleanup + Opprydding av systemfiler - Clean up system files such as temporary files, logs, crash dumps, etc. + Rydd opp i systemfiler som midlertidige filer, logger, krasjdumper osv. - System Components Cleanup + Opprydding av systemkomponenter - Clean up system component files such as Windows Update cache, component store, etc. + Rydd opp i systemkomponentfiler som Windows Update-buffer, komponentlager osv. - Performance Cleanup + Ytelsesopprydding - Clean up files that may affect system performance, such as prefetch files, etc. + Rydd opp i filer som kan påvirke systemytelsen, for eksempel prefetch-filer osv. - Custom Cleanup + Egendefinert opprydding - Clean up specified files and directories using custom rules. + Rydd opp i spesifiserte filer og kataloger ved hjelp av egendefinerte regler. - Taskbar and start menu layout + Oppgavelinje og startmeny-layout - Hide the search box and Task View button, disable taskbar grouping and always show tray icons. + Skjul søkeboksen og Oppgavevisning-knappen, deaktiver oppgavelinjegruppering og vis alltid systemkurvikoner. - Disable start menu and open search with Win key + Deaktiver startmeny og åpne søk med Win-tasten - Disable the start menu completely. When you press the Win key, it will open search instead of the start menu. + Deaktiver startmenyen fullstendig. Når du trykker Win-tasten, åpnes søk i stedet for startmenyen. - Improve Explorer responsiveness + Forbedre Explorer-responsivitet - Reduce menu delays, close hung apps faster and open This PC by default. + Reduser menyforsinkelser, lukk hengte apper raskere og åpne Denne PC-en som standard. - File visibility options + Filvisningsalternativer - Show file extensions and hidden files in File Explorer. + Vis filetternavn og skjulte filer i Filutforsker. - Disable Explorer suggestions + Deaktiver Explorer-forslag - Turn off cloud content suggestions and recommendation banners. + Slå av skyinnholdsforslag og anbefalingsbannere. - Change Windows key to open search + Endre Windows-tasten til å åpne søk - Modify Windows key behavior to open search instead of the start menu. + Endre Windows-tastens oppførsel til å åpne søk i stedet for startmenyen. - Optimise multimedia scheduling + Optimaliser multimedieplanlegging - Remove network throttling and lower system responsiveness latency. + Fjern nettverksbegrensning og reduser systemresponstid. - Improve memory management + Forbedre minnehåndtering - Keep system cache in memory and avoid paging kernel drivers. + Hold systembuffer i minnet og unngå sideveksling av kjerne-drivere. - Disable acrylic and notification centre + Deaktiver akryl og varslingssenter - Remove acrylic effects on logon and hide the Windows notification centre. + Fjern akryleffekter ved innlogging og skjul Windows varslingssenter. - Reduce telemetry and tips + Reduser telemetri og tips - Disable advertising ID, Spotlight features and automatic suggestions. + Deaktiver annonse-ID, Spotlight-funksjoner og automatiske forslag. - Force high performance plan + Tving høy ytelsesplan - Activate the high performance power scheme and disable hibernation. + Aktiver strømplanen for høy ytelse og deaktiver dvalemodus. - Disable diagnostic services + Deaktiver diagnose-tjenester - Stop and disable Connected User Experiences, Diagnostics Hub and Delivery Optimisation services. + Stopp og deaktiver tjenester for Connected User Experiences, Diagnostics Hub og Delivery Optimisation. - Disable SysMain + Deaktiver SysMain - Turn off Superfetch/SysMain to reduce background disk activity. + Slå av Superfetch/SysMain for å redusere bakgrunnsdiskaktivitet. - Disable Windows Search indexing + Deaktiver Windows Search-indeksering - Stop the Windows Search service to prevent index updates. + Stopp Windows Search-tjenesten for å forhindre indeksoppdateringer. - Disable Remote Registry + Deaktiver Remote Registry - Secure the system by disabling remote registry access. + Sikre systemet ved å deaktivere ekstern registertilgang. - Disable error reporting + Deaktiver feilrapportering - Stop Windows Error Reporting from collecting and sending crash data. + Stopp Windows Error Reporting fra å samle inn og sende krasjdata. - Clear Remote Desktop cache + Tøm Remote Desktop-buffer - Remove cached files created by the Remote Desktop client. + Fjern bufrede filer opprettet av Remote Desktop-klienten. - Clean Windows Update cache + Tøm Windows Update-buffer - Delete downloaded update files and Delivery Optimisation cache. + Slett nedlastede oppdateringsfiler og Delivery Optimisation-buffer. - Clean browser cache + Tøm nettleserbuffer - Cleans up temporary files and logs left behind by various applications. + Rydder opp midlertidige filer og logger etterlatt av ulike applikasjoner. - App Leftovers + App-restfiler - Remove Internet Explorer and Edge cache and cookie files. + Fjern Internet Explorer og Edge buffer- og cookie-filer. - Clean thumbnail caches + Rens miniatyrbilde-buffer - Delete Explorer thumbnail databases and Direct3D shader cache. + Slett Explorer miniatyrbilde-databaser og Direct3D shader-buffer. - Clear .NET native images + Tøm .NET native images - Remove cached .NET Native images to reclaim disk space. + Fjern bufrede .NET Native images for å frigjøre diskplass. - Remove logs and diagnostic data + Fjern logger og diagnose-data - Scans for large files on your system drive that may be taking up significant space. + Søker etter store filer på systemdisken som kan ta opp betydelig plass. - Large Files Scan + Søk etter store filer - Identify and manage large files on your system. + Identifiser og administrer store filer på systemet. - Large Files + Store filer - Delete Windows logs, error reports and diagnostic traces. + Slett Windows logger, feilrapporter og diagnose-spor. - Delete crash dumps + Slett krasj-dumps - Remove memory dump files left after system crashes. + Fjern minne-dump-filer som er igjen etter systemkrasj. - Clean Defender scans + Rens Defender skanninger - Delete Windows Defender scan history and caches. + Slett Windows Defender skanne-historikk og buffer. - Clean temporary files + Rens midlertidige filer - Empty Windows and user temporary directories. + Tøm Windows og bruker midlertidige mapper. - Remove bundled Windows apps + Fjern forhåndsinstallerte Windows apper - Uninstall selected preinstalled UWP applications for all users. + Avinstaller valgte forhåndsinstallerte UWP-applikasjoner for alle brukere. - Empty Recycle Bin + Tøm papirkurv - Remove deleted files stored in the Recycle Bin. + Fjern slettede filer lagret i papirkurven. - Clear prefetch data + Tøm prefetch-data - Delete Windows prefetch data to regenerate new traces. + Slett Windows prefetch-data for å generere nye spor. - Clean custom files + Rens egendefinerte filer - Delete files matching configured extensions from selected folders. + Slett filer som matcher konfigurerte utvidelser fra valgte mapper. Service component store - Run DISM cleanup and clear WinSxS temporary files. + Kjør DISM opprydding og tøm WinSxS midlertidige filer. - Disable Lenovo Hotkeys + Deaktiver Lenovo Hotkeys - Lenovo Vantage and/or ImController may have not been disabled correctly + Lenovo Vantage og/eller ImController kan ha blitt deaktiveret feil - Couldn't disable Lenovo Vantage and/or ImController + Kunne ikke deaktivere Lenovo Vantage og/eller ImController - Disable Lenovo Vantage and ImController without uninstalling them. -Restart is recommended after changing this option. + Deaktiver Lenovo Vantage og ImController uten å avinstallere dem. +Omstart anbefales etter endring av dette alternativet. - Disable Lenovo Vantage and ImController + Deaktiver Lenovo Vantage og ImController - Legion Zone may have not been enabled correctly + Legion Zone kan ha blitt aktivert feil - Couldn't enable Legion Zone + Kunne ikke aktivere Legion Zone - Lenovo Hotkeys may have not been enabled correctly + Lenovo Hotkeys kan ha blitt aktivert feil - Couldn't enable Lenovo Hotkeys Keys + Kunne ikke aktivere Lenovo Hotkeys - Lenovo Vantage and/or ImController may have not been enabled correctly + Lenovo Vantage og/eller ImController kan ha blitt aktivert feil - Couldn't enable Lenovo Vantage and/or ImController + Kunne ikke aktivere Lenovo Vantage og/eller ImController - You can exclude refresh rates, to make Fn+R shortcut more useful. + Du kan ekskludere oppdateringsfrekvenser for å gjøre Fn+R hurtigvalg mer nyttig. Ekskluder oppdateringspriser - Select language. + Velg språk. - Language + Språk - Always minimize to tray. Close with right click on tray icon. + Alltid minimer til systemkurven. Lukk med høyreklikk på systemkurv-ikon. - Minimize on close + Minimer ved lukking - Hide the incompatible device warning that appears on startup. + Skjul advarselen om inkompatibelt enhet som vises ved oppstart. - Disable incompatible device warning + Deaktiver advarsel om inkompatibelt enhet - Enable Extensions + Aktiver utvidelser - Enable the extension system to access the plugin market in the Tools page. + Aktiver utvidelse-systemet for å få tilgang til plugin-markedet på Verktøy-siden. - Configure which notifications are shown. + Konfigurer hvilke varsler som vises. - Notifications + Varsler - Windows Power Plans + Windows strømplaner - Assign Quick Action to Fn+F9 double press. + Tildel hurtigvalg til Fn+F9 dobbelttrykk. - Smart Key Secondary Action + Smart Key sekundær handling - Assign Quick Action to Fn+F9 single press. + Tilordne hurtighandling til Fn+F9 enkle trykk. - Smart Key Action + Smarttast-handling - Set light theme, dark theme or follow system settings. + Angi lyst tema, mørkt tema eller følg systeminnstillinger. - Theme + Tema - Settings + Innstillinger - Configure the appearance, behavior, and functionality options of the application. + Konfigurer utseende, oppførsel og funksjonsalternativer for applikasjonen. - Adjust keyboard backlight brightness. + Juster tastaturbakgrunnslysstyrke. Av - Keyboard backlight brightness + Tastaturbakgrunnslysstyrke - Add effect + Legg til effekt - Couldn't apply profile + Kunne ikke bruke profil - Lighting profile couldn't be applied. + Belysningsprofil kunne ikke brukes. - Brightness + Lysstyrke - Deselect all zones + Fjern valg av alle soner - Effects + Effekter - Profile couldn't be exported + Profil kunne ikke eksporteres - Couldn't export profile + Kunne ikke eksportere profil - Profile couldn't be imported + Profil kunne ikke importeres - Couldn't import profile + Kunne ikke importere profil - No effects added. + Ingen effekter lagt til. - Reset to default + Tilbakestill til standard - Select all zones + Velg alle soner - Switch keyboard layout + Bytt tastaturutforming - Direction + Retning - Color + Farge - Colors + Farger - Direction + Retning - Effect + Effekt - This effect will be applied to the whole keyboard and will replace all other effects. + Denne effekten vil bli brukt på hele tastaturet og vil erstatte alle andre effekter. - Speed + Hastighet - Add effect + Legg til effekt - Edit effect + Rediger effekt - Adjust keyboard backlight profile. + Juster tastaturbakgrunnslysprofil. - Keyboard backlight profile + Tastaturbakgrunnslysprofil - All keys + Alle taster - {0} zones + {0} soner - Sunrise + Soloppgang - Sunset + Solnedgang - Time + Tid - Allow switching between integrated and discrete GPU. -Requires restart. + Tillat bytting mellom integrert og diskret GPU. +Krever omstart. - Changing Hybrid Mode requires restart. Do you want to restart now? + Endring av hybridmodus krever omstart. Vil du starte på nytt nå? Ny oppstart er påkrevd @@ -1953,481 +1950,480 @@ Requires restart. Hybridmodus - Disable touchpad. + Deaktiver berøringsplate. - Touchpad Lock + Berøringsplås - Disable touchpad. + Deaktiver berøringsplate. - Touchpad Lock + Berøringsplås - Try again + Prøv igjen - Unexpected exception occurred: + Uventet unntak oppstod: {0} Uten navn - You may choose to continue at your own risk, but keep in mind that some features may not be available and/or not work properly. + Du kan velge å fortsette på egen risiko, men husk at noen funksjoner kanskje ikke er tilgjengelige og/eller ikke fungerer som de skal. -Logging will be enabled automatically, if you choose to continue. +Logging vil bli aktivert automatisk hvis du velger å fortsette. - Check out project page on GitHub for more information. + Sjekk prosjektsiden på GitHub for mer informasjon. - Logs folder + Loggmappe Maskintype - Lenovo Legion Toolkit has not been tested with your device. + Lenovo Legion Toolkit har ikke blitt testet med din enhet. - You can disable this warning at any time in Settings. + Du kan deaktivere denne advarselen når som helst i Innstillinger. Modell - Unsupported device + Enheten støttes ikke - Vendor + Leverandør - Update + Oppdater - Update available + Oppdatering tilgjengelig - What's new? + Hva er nytt? - Adjust keyboard backlight brightness. + Juster tastaturbelysningsstyrke. - Keyboard backlight + Tastaturbelysning - You can change brightness with Fn+Space + Du kan endre styrke med Fn+Space - Keyboard backlight + Tastaturbelysning - Disable Windows key on built-in keyboard. + Deaktiver Windows-tasten på innebygget tastatur. - Windows Key Lock + Windows-tastelås - Only works on built-in keyboard. + Fungerer kun på innebygget tastatur. - Windows Key Lock + Windows-tastelås - Yes + Ja - Microphone + Mikrofon - When off, microphones will be muted. + Når av, vil mikrofoner være dempet. - Microphone + Mikrofon - When off, microphones will be muted. + Når av, vil mikrofoner være dempet. - Resolution + Oppløsning - Change resolution of the built-in display. + Endre oppløsning på innebygget skjerm. - Resolution + Oppløsning - Change resolution of the built-in display. + Endre oppløsning på innebygget skjerm. -WARNING: This action will not run correctly, if internal display is off. +ADVARSEL: Denne handlingen vil ikke kjøre korrekt hvis intern skjerm er av. DPI - Change scale of the built-in display. + Endre skalering på innebygget skjerm. DPI - Change scaling of the built-in display. + Endre skalering på innebygget skjerm. -WARNING: This action will not run correctly, if internal display is off. +ADVARSEL: Denne handlingen vil ikke kjøre korrekt hvis intern skjerm er av. - Display + Skjerm - Keyboard backlight + Tastaturbelysning - You can turn backlight on or off with Fn+Space. + Du kan slå belysning på eller av med Fn+Space. - Keyboard backlight + Tastaturbelysning - Turn backlight on or off. + Slå belysning på eller av. - No matching downloads found + Ingen matchende nedlastinger funnet - Show updates only + Vis kun oppdateringer - Customize Dashboard + Tilpass Dashboard - Load + Last - Create group + Opprett gruppe - Name + Navn - Some features may not appear on the Dashboard depending on state and configuration of your laptop. + Noen funksjoner kan ikke vises på Dashboard avhengig av status og konfigurasjon av din laptop. - Customize + Tilpass - Turn off displays + Slå av skjermer - Turn off all available displays. -Moving the mouse or pressing the keyboard will wake displays up. + Slå av alle tilgjengelige skjermer. +Bevegelse av musen eller trykking på tastaturet vil vekke skjermer. - Turn off + Slå av - Turn off displays + Slå av skjermer - Turn off all available displays. + Slå av alle tilgjengelige skjermer. - Edit group name + Rediger gruppenavn - Name + Navn - Legion logo on + Legion-logo på - Legion logo off + Legion-logo av - Ports backlight on + Portbelysning på - Ports backlight off + Portbelysning av Ny oppstart er påkrevd - Shutdown required + Avstenging kreves - Restart recommended + Omstart anbefalt - Recommended + Anbefalt - Downloading + Laster ned - Installing + Installerer - Completed + Fullført - Action definition not found + Handlingsdefinisjon ikke funnet - Technical implementation details are not available for this action + Tekniske implementeringsdetaljer er ikke tilgjengelige for denne handlingen - Load failed + Lasting mislyktes Ukjent - Command execution + Kommandokjøring - Registry modification + Registerendring - Service management + Tjenestehåndtering - Registry modification + PowerShell script + Registerendring + PowerShell-skript - Registry cleanup + Registeropprydding - DISM command + DISM-kommando - shell.exe not found + shell.exe ikke funnet - Configuration file path: Not found + Konfigurasjonsfilbane: Ikke funnet - Save & Close + Lagre og lukk - Custom Mode preset + Egendefinert modus-forhåndsinnstilling - Activate Custom Mode preset. -This settings takes effect only when Custom Mode is enabled. + Aktiver forhåndsinnstilling for egendefinert modus.\nDenne innstillingen trer bare i kraft når egendefinert modus er aktivert. - Preset name + Navn på forhåndsinnstilling - Name + Navn - Active preset + Aktiv forhåndsinnstilling - Application Folders + Programmapper Data - Temp + Midlertidig - Single Display Brightness + Enkel skjermlysstyrke - When on, same brightness level will be applied to all Windows power plans whenever you change it. + Når på, vil samme lysstyrkenivå bli brukt på alle Windows-strømplaner hver gang du endrer det. - Update {0} available! + Oppdatering {0} tilgjengelig! - Peak Power Limit + Toppstrømgrense - APU sPPT Power Limit + APU sPPT-strømgrense - The continuous power consumption that can be reached by the CPU. + Det kontinuerlige strømforbruket som kan nås av CPU. - The peak power consumption that can be reached by the CPU within a short amount time. + Toppstrømforbruket som kan nås av CPU innen kort tid. - The maximum instantaneous power consumption that can be reached by the CPU. + Det maksimale øyeblikksstrømforbruket som kan nås av CPU. - The maximum power consumption that can be reached by the CPU when both CPU and GPU are fully utilized. + Det maksimale strømforbruket som kan nås av CPU når både CPU og GPU er fullt utnyttet. - The peak power consumption that can be reached by the CPU with a minor delay. + Toppstrømforbruket som kan nås av CPU med en liten forsinkelse. - The maximum temperature that can be reached by the CPU before frequency and power is reduced. + Den maksimale temperaturen som kan nås av CPU før frekvens og strøm reduseres. - The additional maximum power that can be allocated to the GPU based on the power consumption of the CPU. + Den tilleggsstrømmen som kan tildeles GPU basert på strømforbruket til CPU. - The additional amount of power that can be allocated to the GPU on top of base power consumption. + Den tilleggsmengden strøm som kan tildeles GPU i tillegg til basisstrømforbruk. - The maximum temperature that can be reached by the GPU before frequency and power is reduced. + Den maksimale temperaturen som kan nås av GPU før frekvens og strøm reduseres. - Default + Standard - Short Term Power Limit Duration + Varighet for kortvarig strømgrense - The amount of time the CPU is allowed to boost and use Short Term Power Limit for. When Tau expires, Long Term Power Limit is used. + Tiden CPU tillates å booste og bruke kortvarig strømgrense. Når Tau utløper, brukes langvarig strømgrense. - Total Processor Power Target In AC + Total prosessorstrømmål i AC - The point at which the CPU triggers dynamic power consumption adjustment for the GPU. + Punktet der CPU utløser dynamisk strømforbruksjustering for GPU. - Create + Opprett Legg til trinn - Create Action + Opprett handling - Power Mode + Strømmodus - Preset + Forhåndsinnstilling Diskret GPU - Power State + Strømtilstand Batteri - Mode + Modus Utladningsrate - Min discharge rate + Min utladningshastighet - Max discharge rate + Maks utladningshastighet - Usage time + Brukstid - Update available! + Oppdatering tilgjengelig! - Select icon + Velg ikon - Change icon + Endre ikon - Battery level low + Batterinivå lavt Konfigurer - S0 Lower Power Model detected + S0 strømsparemodus oppdaget - Windows reports that this laptop supports Modern Standby. Using power plans other than Balanced may cause unexpected behavior. + Windows rapporterer at denne bærbare støtter Modern Standby. Bruk av strømplaner annet enn Balansert kan forårsake uventet oppførsel. - Select Windows Power Plans to apply when Power Mode changes. + Velg Windows-strømplaner som skal brukes når strømmodus endres. Effekt - Power Plan options in Windows Control Panel + Alternativer for strømplaner i Windows-kontrollpanelet slått av - Import keyboard backlight profile + Importer tastaturbelysningsprofil - Import and apply backlight configuration to the current profile. + Importer og bruk belysningskonfigurasjonen til gjeldende profil. - Path + Bane - After {0} + Etter {0} - Multiple triggers... + Flere utløsere... Allows the user to merge multiple triggers for the execution of selected actions. - Overclock GPU Settings + Overklokking av GPU-innstillinger - Core Frequency Offset + Kjernefrekvensforskyvning - Memory Frequency Offset + Minnefrekvensforskyvning - Overclock GPU + Overklokking av GPU - Increase performance by overclocking discrete GPU. + Øk ytelsen ved å overklokke den dedikerte GPU-en. - Overclock GPU + Overklokking av GPU - Increase performance by overclocking discrete GPU. + Øk ytelsen ved å overklokke den dedikerte GPU-en. -WARNING: This action will not run correctly, if discrete GPU is not available. +ADVARSEL: Denne handlingen vil ikke fungere riktig hvis den dedikerte GPU-en ikke er tilgjengelig. - Ports backlight + Portbelysning - Turn on or off backlight of the ports on the back of the laptop. + Slå på eller av belysningen på portene på baksiden av den bærbare. - Ports backlight + Portbelysning - Turn on or off the backlight of the ports on the back of the laptop. + Slå på eller av belysningen på portene på baksiden av den bærbare. - Panel logo backlight + Panellogobelysning - Turn on or off the backlight on the lid of the laptop. + Slå på eller av belysningen på lokket til den bærbare. - Panel logo backlight + Panellogobelysning - Turn on or off the backlight on the lid of the laptop. + Slå på eller av belysningen på lokket til den bærbare. GHz @@ -2436,84 +2432,84 @@ WARNING: This action will not run correctly, if discrete GPU is not available.MHz - Maximum: {0} + Maksimum: {0} CPU - Utilization + Utnyttelse - Core Clock + Klokkehastighet - Temperature + Temperatur - Fan + Vifte GPU - Memory Clock + Minneklokkehastighet - Refresh interval + Oppdateringsintervall - Instant Boot + Øyeblikkelig oppstart - Turn on the laptop when a charger is connected. + Slå på den bærbare når en lader er tilkoblet. - Instant Boot + Øyeblikkelig oppstart - Turn on the laptop when a charger is connected. + Slå på den bærbare når en lader er tilkoblet. - Cannot change Power Mode + Kan ikke endre strømmodus - {0} power mode is not available without AC power. + {0} strømmodus er ikke tilgjengelig uten nettstrøm. For example: "{Performance} power mode is not available without AC power." - Sensors + Sensorer - Could not change GPU Working Mode + Kunne ikke endre GPU-arbeidsmodus - Try changing the mode again in a couple of seconds, if you do not see expected result. -If dGPU does not respond at all, please restart your laptop. + Prøv å endre modusen igjen om noen sekunder hvis du ikke ser forventet resultat. +Hvis dGPU ikke svarer i det hele tatt, vennligst start den bærbare på nytt. - dGPU is currently in use + dGPU er i bruk for øyeblikket - dGPU will disconnect automatically when not in use. + dGPU kobles fra automatisk når den ikke er i bruk. - dGPU is currently in use or laptop is not on battery power + dGPU er i bruk for øyeblikket, eller den bærbare bruker ikke batteristrøm - dGPU will disconnect automatically when not in use and laptop is on battery power. + dGPU kobles fra automatisk når den ikke er i bruk og den bærbare bruker batteristrøm. - Custom Mode settings will not be applied correctly when Lenovo Vantage or its services are running. + Tilpassede modusinnstillinger vil ikke bli brukt riktig når Lenovo Vantage eller tjenestene kjører. - Custom Mode settings will not be applied correctly when Legion Zone or its services are running. + Tilpassede modusinnstillinger vil ikke bli brukt riktig når Legion Zone eller tjenestene kjører. - dGPU connected + dGPU tilkoblet - dGPU disconnected + dGPU frakoblet Arbeidsmodus @@ -2526,512 +2522,509 @@ Brytermoduser kan kreve omstart. Hybridmodus - Allow switching between integrated and discrete GPU. -Requires restart. + Tillat bytting mellom integrert og diskret GPU.\nKrever omstart. - Duration + Varighet - Updates + Oppdateringer - GPU to CPU Dynamic Boost + GPU til CPU Dynamic Boost - This is the maximum additional power that can be allocated to the CPU from the GPU based on CPU usage. The higher the value, the better the performance of applications that use the CPU. + Dette er maksimal ekstra strøm som kan tildeles CPU fra GPU basert på CPU-bruk. Jo høyere verdi, jo bedre ytelse for programmer som bruker CPU. - Boot Logo + Oppstartslogo - Customize Boot Logo image, visible during system startup. + Tilpass oppstartslogo, synlig under systemoppstart. - Boot Logo + Oppstartslogo - Default Boot Logo is set + Standard oppstartslogo er angitt - Custom Boot Logo is set + Tilpasset oppstartslogo er angitt Status - Revert to default + Tilbakestill til standard - Customize + Tilpass Handlinger - Notification Text + Varslingstekst - Show notification + Vis varsling - Preset + Forhåndsinnstilling Smart Fn Lock - Fn Lock will be temporarily disabled when Alt, Ctrl or Shift key is depressed. + Fn Lock vil bli midlertidig deaktivert når Alt, Ctrl eller Shift-tasten trykkes ned. Av - Custom boot logo must be exactly {0} pixels large. -Supported formats are: {1}. + Tilpasset oppstartslogo må være nøyaktig {0} piksler stor.\nStøttede formater er: {1}. - Overnight Battery Charging + Nattlading av batteri - When enabled, this device will charge to 80% when plugged in overnight and finish charging to 100% by the time you use this device in the morning. + Når aktivert, vil denne enheten lade til 80% når den er koblet til strøm over natten og fullføre ladingen til 100% innen du bruker enheten om morgenen. - Overnight Battery Charging + Nattlading av batteri - When enabled, this device will charge to 80% when plugged in overnight and finish charging to 100% by the time you use this device in the morning. + Når aktivert, vil denne enheten lade til 80% når den er koblet til strøm over natten og fullføre ladingen til 100% innen du bruker enheten om morgenen. - Invalid image size. + Ugyldig bildestørrelse. - Invalid image format. + Ugyldig bildeformat. - Cannot mount EFI partition. + Kan ikke montere EFI-partisjon. - Cannot set UEFI privilege. + Kan ikke angi UEFI-rettighet. - Not enough free space on EFI partition. + Ikke nok ledig plass på EFI-partisjonen. - Custom logo could not be set: {0} + Kunne ikke angi tilpasset logo: {0} - Default logo could not be set: {0} + Kunne ikke angi standard logo: {0} - Default boot logo set. + Standard oppstartslogo angitt. - Custom boot logo set. + Tilpasset oppstartslogo angitt. - Network name (SSID) + Nettverksnavn (SSID) - Copy current network name + Kopier gjeldende nettverksnavn - Leave empty for any Wi-Fi network. + La stå tomt for alle Wi-Fi-nettverk. HWiNFO64 - Share fan speed, battery temperature etc. with HWiNFO64. You may need to restart HWiNFO64 after changing this option. + Del viftehastighet, batteritemperatur osv. med HWiNFO64. Du må kanskje starte HWiNFO64 på nytt etter å ha endret dette alternativet. - Integrations + Integrasjoner - Settings + Innstillinger - Information + Informasjon - Download folder + Nedlastingsmappe - Open Download folder + Åpne nedlastingsmappe - Download + Last ned - Install + Installer - Uninstall + Avinstaller - Installation failed + Installasjon mislyktes - File not found, please download first + Filen ble ikke funnet, vennligst last ned først - Installation started + Installasjon startet - Starting installer: {0} + Starter installasjonsprogram: {0} - Open README + Åpne README - Preset + Forhåndsinnstilling - Refresh + Oppdater Slett - Period (minutes) + Periode (minutter) A prompt that is used both to ask the user to input an integer, representing the number of minutes between two periodic actions are triggered, and to inform the user what have they set once they are done configuring the action. - Resets the counter for "On battery since" in the battery section when the system reboots. + Tilbakestiller telleren for "På batteri siden" i batteriseksjonen når systemet startes på nytt. - Reset "On battery since" at startup + Tilbakestill "På batteri siden" ved oppstart - Turn off Wi-Fi + Slå av Wi-Fi - Turn on Wi-Fi + Slå på Wi-Fi - Run silently + Kjør i bakgrunnen - Wait until finished + Vent til ferdig - Execute console applications, without creating a console window. + Kjør konsollapplikasjoner uten å opprette et konsollvindu. - Wait until program or script finishes executing + Vent til programmet eller skriptet er ferdig kjørt - When mute, all active audio output devices will be muted. + Når dempet, dempes alle aktive lydenheter. - Speaker + Høyttaler - Minimize to tray + Minimer til systemfeltet - Always minimize to tray instead of taskbar. + Alltid minimer til systemfeltet i stedet for oppgavelinjen. - Windows Power Modes + Windows-strømmoduser - Switch to Custom Mode with Fn+Q + Bytt til egendefinert modus med Fn+Q - Allow quick switching to Custom Mode with Fn+Q. + Tillat rask bytting til egendefinert modus med Fn+Q. - Power Mode Synchronization + Strømmodus-synkronisering - Automatically change Windows Power Plan or Windows Power Mode when changing Power Modes. + Endre automatisk Windows-strømplan eller Windows-strømmodus når strømmoduser endres. - Windows Power Modes + Windows-strømmoduser - Select Windows Power Mode to apply when Power Mode changes. + Velg Windows-strømmodus som skal brukes når strømmodus endres. - Error occurred when reading device information. + Det oppstod en feil ved lesing av enhetsinformasjon. - Compatibility Check Error + Feil ved kompatibilitetskontroll - The application failed to read device information during startup. Please check the error details below and try the troubleshooting steps if needed. + Applikasjonen klarte ikke å lese enhetsinformasjon under oppstart. Vennligst sjekk feildetaljene nedenfor og prøv feilsøkingstrinnene hvis nødvendig. - Troubleshooting Steps + Feilsøkingstrinn - Ensure Windows Management Instrumentation (WMI) service is running: Open Services (services.msc) and check that "Windows Management Instrumentation" is running. + Sørg for at Windows Management Instrumentation (WMI)-tjenesten kjører: Åpne Tjenester (services.msc) og sjekk at "Windows Management Instrumentation" kjører. - Run the application as Administrator to ensure sufficient permissions to access system registry and WMI. + Kjør applikasjonen som administrator for å sikre tilstrekkelige tillatelser for å få tilgang til systemregisteret og WMI. - Check system resources: Close other resource-intensive applications and try again. + Sjekk systemressurser: Lukk andre ressurskrevende applikasjoner og prøv igjen. - Review the log file for detailed error information by clicking "Open Log" below. + Se gjennom loggfilen for detaljert feilinformasjon ved å klikke på "Åpne logg" nedenfor. - Open Log File + Åpne loggfil - Macro + Makro - Macro + Makro - Repeat + Gjenta - Ignore delays + Ignorer forsinkelser - Clear + Tøm - Record + Ta opp - Stop recording + Stopp opptak - Don't repeat + Ikke gjenta - Lenovo Legion Toolkit must be running for macros to work. + Lenovo Legion Toolkit må kjøre for at makroer skal fungere. Aktiver - You can record series of key presses and invoke them using the number pad on your keyboard. + Du kan ta opp en serie tastetrykk og aktivere dem ved å bruke talltastaturet på tastaturet ditt. - Macro + Makro - Enable or disable macros. + Aktiver eller deaktiver makroer. - Interrupt if another key was pressed + Avbryt hvis en annen tast ble trykket - Hide + Skjul - Hide all + Skjul alle - Devices + Enheter - Deselect all + Fjern alle markeringer - Select all + Velg alle - Keyboard only + Kun tastatur - Keyboard keys and mouse buttons + Tastaturtaster og museknapper - All inputs + Alle inndata - Recording options + Opptaksalternativer - Recording will start in 3 seconds... + Opptak starter om 3 sekunder... - Recording... + Tar opp... - Press ESC to stop. + Trykk ESC for å stoppe. - Show selected devices only + Vis kun valgte enheter - Show removable devices only + Vis kun flyttbare enheter - Show connected devices only + Vis kun tilkoblede enheter CLI - Enable Command Line Interface that allows control from command line. + Aktiver kommandolinjegrensesnitt som tillater kontroll fra kommandolinjen. - Add CLI to PATH + Legg CLI til PATH - Add CLI to user's PATH environment variable. + Legg CLI til brukerens PATH-miljøvariabel. - Temperature + Temperatur - Select units for temperature sensors. + Velg enheter for temperatur Sensorer. - Lenovo Hotkeys is running in the background. + Lenovo Hotkeys kjører i bakgrunnen. - Legion Zone is running in the background. + Legion Zone kjører i bakgrunnen. - Lenovo Vantage and/or ImController is running in the background. + Lenovo Vantage og/eller ImController kjører i bakgrunnen. - Show notification on all screens connected to your device. + Vis varsling på alle skjermer tilkoblet enheten din. - Notification on all screens + Varsling på alle skjermer - Check + Sjekk - Checking for updates... + Sjekker etter oppdateringer... - Check for updates + Sjekk etter oppdateringer - Update + Oppdater - API rate limit reached, please try again later. + API-rategrense nådd, vennligst prøv igjen senere. - Failed to check for updates + Kunne ikke sjekke etter oppdateringer - Something went wrong, please try again later. + Noe gikk galt, vennligst prøv igjen senere. - No updates found + Ingen oppdateringer funnet - Automatically check for updates + Sjekk automatisk etter oppdateringer - Update repository + Oppdateringslager - Configure the GitHub repository to check for updates. Leave empty to use default. + Konfigurer GitHub-lageret for å sjekke etter oppdateringer. La stå tomt for å bruke standard. - Repository Owner + Lagereier - e.g., SSC-STUDIO + f.eks. SSC-STUDIO - Repository Name + Lagernavn - e.g., LenovoLegionToolkit + f.eks. LenovoLegionToolkit - Update catalog not found + Oppdateringskatalog ikke funnet - Try getting updates from the other source. + Prøv å hente oppdateringer fra den andre kilden. - Something went wrong + Noe gikk galt - Check if your internet connection is up and running. + Sjekk om internettilkoblingen din fungerer. - Play sound + Spill lyd - Common music formats like wav or mp3 are supported. + Vanlige musikkformater som wav eller mp3 støttes. - Quick Action + Hurtighandling - Run a saved quick action. + Kjør en lagret hurtighandling. - Notification always on top + Varsling alltid øverst - Always put notification at the top. -It won't affect other full screen windows, but you will not be able to click on the notifications. + Legg alltid varsling øverst.\nDet påvirker ikke andre fullskjermvinduer, men du vil ikke kunne klikke på varslingene. - Recommended + Anbefalt - Pause all + Pause alle - Implementation and Effect Principles + Implementerings- og effektprinsipper - Implementation Type: + Implementeringstype: - COM Component Registration + Configuration File + COM-komponentregistrering + konfigurasjonsfil - By registering Nilesoft Shell as a COM handler for Windows context menus, it intercepts the creation and display of system right-click menus. When users right-click on files or folders, Shell reads the shell.nss configuration file and applies custom theme, transparency, rounded corners, and shadow settings to achieve a beautified right-click menu effect. + Ved å registrere Nilesoft Shell som en COM-behandler for Windows kontekstmenyer, fanger den opp opprettelsen og visningen av systemets høyreklikkmenyer. Når brukere høyreklikker på filer eller mapper, leser Shell shell.nss-konfigurasjonsfilen og bruker egendefinerte tema-, gjennomsiktighet-, avrundede hjørner- og skyggeinnstillinger for å oppnå en forskjønnet høyreklikkmenyeffekt. - Configuration file not found. Please make sure Nilesoft Shell is installed. + Konfigurasjonsfil ikke funnet. Sørg for at Nilesoft Shell er installert. - Configuration File Not Found + Konfigurasjonsfil ikke funnet - Settings applied successfully. Changes will take effect after restarting File Explorer. + Innstillinger brukt. Endringer trer i kraft etter omstart av Filutforsker. - Settings Applied + Innstillinger brukt - Failed to apply settings: {0} + Kunne ikke bruke innstillinger: {0} - Error + Feil - 1. COM Component Registration: + 1. COM-komponentregistrering: regsvr32.exe /s shell.dll - Path: {0} + Sti: {0} - 2. Configuration File (shell.nss): + 2. Konfigurasjonsfil (shell.nss): - Configuration file path: {0} + Konfigurasjonsfilsti: {0} - Uses CSS-like syntax to define menu styles + Bruker CSS-lignende syntaks for å definere menystiler - 3. Configuration File Format Example: + 3. Eksempel på konfigurasjonsfilformat: theme @@ -3044,40 +3037,40 @@ It won't affect other full screen windows, but you will not be able to click on } - 4. Working Principle: + 4. Arbeidsprinsipp: - - Shell.dll is registered as a COM component in the system + - Shell.dll er registrert som en COM-komponent i systemet - - Implements IContextMenu interface to intercept right-click menu creation + - Implementerer IContextMenu-grensesnitt for å fange opp oppretting av høyreklikkmeny - - Reads shell.nss configuration file + - Leser shell.nss-konfigurasjonsfil - - Applies custom styles (theme, rounded corners, shadows, etc.) + - Bruker egendefinerte stiler (tema, avrundede hjørner, skygger osv.) - - Renders the beautified context menu + - Tegner den forbedrede kontekstmenyen - 5. Effect Description: + 5. Effektbeskrivelse: - - Auto Theme: Follows system theme (light/dark) + - Auto-tema: Følger systemtemaet (lyst/mørkt) - - Transparency: Enables menu background transparency effect + - Gjennomsiktighet: Aktiverer gjennomsiktighetseffekt for menybakgrunn - - Rounded Corners: Adds rounded corner borders to menu items + - Avrundede hjørner: Legger til avrundede hjørner på menyelementer - - Shadows: Adds shadow effects to menus, enhancing depth perception + - Skygger: Legger til skyggeeffekter på menyer, forbedrer dybdeoppfattelsen - Configuration Editor + Konfigurasjonsredigerer shell.nss @@ -3092,82 +3085,82 @@ It won't affect other full screen windows, but you will not be able to click on modify.nss - Action Details + Handlingsdetaljer - Technical Implementation Details + Tekniske implementeringsdetaljer - Implementation Type: + Implementeringstype: Lukk - {0} of {1} actions selected + {0} av {1} handlinger valgt - Estimated cleanup size: {0} + Estimert opprenskingsstørrelse: {0} - Click Scan to calculate total size + Klikk Skann for å beregne total størrelse - Running: {0} + Kjører: {0} - Uninstalling selected apps... + Avinstallerer valgte apper... - Compact view + Kompakt visning - System Beautification + Systemforbedring - Right Click Menu + Høyreklikkmeny - Customize the right click menu appearance and behavior. + Tilpass utseendet og oppførselen til høyreklikkmenyen. - Enable Classic Menu + Aktiver klassisk meny - Disable Classic Menu + Deaktiver klassisk meny - Restore Default + Gjenopprett standard - Install + Installer - Uninstall + Avinstaller - Installed + Installert - Not Installed + Ikke installert - Installed but not registered + Installert men ikke registrert - Open Style Settings + Åpne stilinnstillinger - Windows Beautification + Windows-forbedring - Customize Windows appearance and behavior. + Tilpass Windows-utseende og -oppførsel. - Menu Style Settings + Menystilinnstillinger - Theme + Tema Auto @@ -3179,733 +3172,816 @@ It won't affect other full screen windows, but you will not be able to click on Mørk - Classic + Klassisk - Modern + Moderne - Transparency + Gjennomsiktighet - Enable transparency + Aktiver gjennomsiktighet - Appearance + Utseende - Rounded corners + Avrundede hjørner - Shadows + Skygger Søk - Effects + Effekter - Customize visual effects. + Tilpass visuelle effekter. - Transparency + Gjennomsiktighet - Expand all + Utvid alle - Collapse all + Slå sammen alle - - Freed {0} in {1}s + Frigjorde {0} på {1}s - Executing {0}% + Utfører {0}% - Operation cancelled + Operasjon avbrutt - Error details: {0} + Feildetaljer: {0} - Show hidden downloads + Vis skjulte nedlastinger - Show hidden downloads + Vis skjulte nedlastinger - Select language + Velg språk - Not connected + Ikke tilkoblet - Synchronise zones + Synkroniser soner - Using Windows built-in registry cleanup functionality + Bruker Windows innebygde registerrenskningsfunksjonalitet - ipconfig /flushdns (Flush DNS cache) + ipconfig /flushdns (Tøm DNS-buffer) - netsh winsock reset (Reset Winsock protocol stack) + netsh winsock reset (Tilbakestill Winsock-protokollstabel) - netsh int ip reset (Reset TCP/IP protocol stack) + netsh int ip reset (Tilbakestill TCP/IP-protokollstabel) - Plugin Extensions + Utvidelsesprogrammer - Install and manage plugins to extend functionality + Installer og administrer utvidelser for å utvide funksjonalitet - Install + Installer - Installed + Installert - Cannot Uninstall + Kan ikke avinstallere - Plugin Settings + Innstillinger for utvidelse - Plugin Name + Navn på utvidelse - Plugin Description + Beskrivelse av utvidelse - This plugin has no configurable settings. + Denne utvidelsen har ingen konfigurerbare innstillinger. Lukk - Plugin not found: {0} + Utvidelse ikke funnet: {0} - Error + Feil - Settings + Innstillinger - Author: {0} + Forfatter: {0} - Plugin configuration interface format is incorrect. + Formatet for utvidelsens konfigurasjonsgrensesnitt er feil. - Error loading plugin settings: {0} + Feil ved lasting av utvidelsesinnstillinger: {0} - Search plugins... + Søk etter utvidelser... - All + Alle - Installed + Installert - Not Installed + Ikke installert - No plugins available + Ingen utvidelser tilgjengelig - The plugin store is currently empty. Stay tuned for future plugin updates. + Utvidelsesbutikken er for øyeblikket tom. Følg med for fremtidige oppdateringer av utvidelser. Konfigurer - Uninstall + Avinstaller - Open + Åpne - Language: + Språk: - Use Application Default + Bruk applikasjonens standard - Found {0} plugins + Fant {0} utvidelser - Bulk Import + Masseimport - Import plugins from local ZIP files + Importer utvidelser fra lokale ZIP-filer - Import from Files + Importer fra filer - Select plugin files (.zip) + Velg utvidelsesfiler (.zip) - Successfully imported {0} plugins + Importerte {0} utvidelser - {0} plugins have been successfully imported and are available + {0} utvidelser er importert og tilgjengelige - Bulk import failed + Masseimport feilet - Failed to import plugins: {0} + Kunne ikke importere utvidelser: {0} - Importing plugins... + Importerer utvidelser... - Importing {0}... + Importerer {0}... - Processing {0}... + Behandler {0}... - Plugin installed successfully + Utvidelse installert - Plugin has been successfully installed + Utvidelsen er installert - Installation failed + Installasjon feilet - Unable to install plugin: {0} + Kunne ikke installere utvidelse: {0} - Plugin uninstalled successfully + Utvidelse avinstallert - Plugin has been successfully uninstalled + Utvidelsen er avinstallert - Uninstallation failed + Avinstallasjon feilet - Unable to uninstall plugin: {0} + Kunne ikke avinstallere utvidelse: {0} - Failed to open plugin + Kunne ikke åpne utvidelse - Unable to open plugin: {0} + Kunne ikke åpne utvidelse: {0} - Unable to open plugin: {0} + Kunne ikke åpne utvidelse: {0} - Plugin not installed + Utvidelse ikke installert - Plugin is not installed and cannot be opened + Utvidelsen er ikke installert og kan ikke åpnes - Minimum host version {0} is required + Minimum versjon {0} kreves - Plugin will be permanently deleted + Utvidelsen vil bli slettet permanent - Plugin will be permanently deleted. This action cannot be undone. + Utvidelsen vil bli slettet permanent. Denne handlingen kan ikke angres. - Update Failed + Oppdatering feilet - Plugin update failed, please check network connection and try again later. + Oppdatering av utvidelse feilet, sjekk nettverkstilkoblingen og prøv igjen senere. - Update Successful + Oppdatering vellykket - Plugin has been successfully updated + Utvidelsen er oppdatert - Updating Plugin + Oppdaterer utvidelse - Downloading and updating plugin... + Laster ned og oppdaterer utvidelse... - Update All + Oppdater alle - Run Plugin + Kjør utvidelse - Plugin started + Utvidelse startet - Plugin Uninstalled + Utvidelse avinstallert - Plugin will be deleted when the program closes. + Utvidelsen vil bli slettet når programmet lukkes. - Deletion Failed + Sletting feilet - Error occurred while deleting plugin + Feil oppstod under sletting av utvidelse - Configuration Failed + Konfigurasjon feilet - Plugin is installed but not loaded. Please try restarting the application. + Utvidelsen er installert men ikke lastet. Prøv å starte programmet på nytt. - No Configuration + Ingen konfigurasjon - Plugin does not have any configuration options. + Utvidelsen har ingen konfigurasjonsalternativer. - Configuration Not Supported + Konfigurasjon støttes ikke - Plugin does not support configuration. + Utvidelsen støtter ikke konfigurasjon. - Plugin States Reset + Utvidelsestilstander nullstilt - All plugin installation states have been cleared + Alle installasjonstilstander for utvidelser er nullstilt - Bulk Update Complete + Masseoppdatering fullført - All plugins have been processed. + Alle utvidelser er behandlet. - Preparing download... + Forbereder nedlasting... - Local + Lokal - New Version Available + Ny versjon tilgjengelig - Version: + Versjon: - Release Date: + Utgivelsesdato: - What's New: + Nyheter: - Refresh plugin list + Oppdater utvidelsesliste - No plugins found matching your search. + Ingen utvidelser funnet som samsvarer med søket. - Update all available plugins + Oppdater alle tilgjengelige plugins - Install All + Installer alle - Install all available plugins + Installer alle tilgjengelige plugins - Installing All... + Installer alle... - Installing {0} plugin(s)... + Installer {0} plugin(s)... - Bulk Install Complete + Masseinstallasjon fullført - Installed {0} plugin(s). + Installerte {0} plugin(s). - Bulk Install Failed + Masseinstallasjon mislyktes - Failed to install plugins: {0} + Klarte ikke å installere plugins: {0} - Updating All... + Oppdaterer alle... - Updating... + Oppdaterer... - Open Folder + Åpne mappe - Copy Plugin ID + Kopier Plugin-ID - Folder Not Found + Mappe ikke funnet - The plugin directory does not exist yet. + Plugin-mappen eksisterer ikke ennå. - Copied + Kopiert - Plugin ID '{0}' copied to clipboard. + Plugin-ID '{0}' kopiert til utklippstavle. - Download completed + Nedlasting fullført - Downloading... + Laster ned... - Downloading... {0:F1} / {1:F1} MB ({2:F0}%) + Laster ned... {0:F1} / {1:F1} MB ({2:F0}%) - Network Acceleration + Nettverksakselerasjon ViveTool - Tools + Verktøy - Real-time network acceleration and optimization features + Sanntids nettverksakselerasjon og optimaliseringsfunksjoner - Manage Windows feature flags using ViVeTool + Administrer Windows funksjonsflagg med ViVeTool - System optimization and utility tools + Systemoptimalisering og verktøyprogrammer - Usage + Bruk - This editor is used to directly edit shell.nss configuration file. After modification, click + Denne redigereren brukes til å direkte redigere shell.nss konfigurasjonsfil. Etter endring, klikk Søk - button to save all changes and restart File Explorer to apply configuration. + knappen for å lagre alle endringer og starte Filutforsker på nytt for å aktivere konfigurasjonen. - • Configuration file path: + • Konfigurasjonsfilsti: - • After modifying configuration, File Explorer (Explorer) needs to be restarted for changes to take effect. + • Etter endring av konfigurasjonen, må Filutforsker (Explorer) startes på nytt for at endringer skal tre i kraft. Søk - shell.nss Configuration File + shell.nss konfigurasjonsfil - shell.nss file is used to define the overall structure and behavior of the right-click menu + shell.nss filen brukes til å definere den generelle strukturen og oppførselen til høyreklikkmenyen - Open shell.nss file + Åpne shell.nss fil - Open containing folder + Åpne inneværende mappe - Path: + Sti: - UI Settings + UI-innstillinger - Basic Theme Settings + Grunnleggende temainnstillinger - Background Color: + Bakgrunnsfarge: - Text Color: + Tekstfarge: - Hover Effect Settings + Hovereffekt-innstillinger - Hover Background: + Hover bakgrunn: - Hover Text: + Hover tekst: - Selected Effect Settings + Valgteffekt-innstillinger - Selected Background: + Valgt bakgrunn: - Selected Text: + Valgt tekst: - Update Text from UI + Oppdater tekst fra UI - Load from Text + Last fra tekst - Text Editor + Tekstredigerer - Open in External Editor + Åpne i ekstern redigerer - images.nss Configuration File + images.nss konfigurasjonsfil - images.nss file is used to define icons and images for the right-click menu + images.nss filen brukes til å definere ikoner og bilder for høyreklikkmenyen - Open images.nss file + Åpne images.nss fil - modify.nss Configuration File + modify.nss-konfigurasjonsfil - modify.nss file is used to modify and customize right-click menu items + modify.nss-filen brukes til å endre og tilpasse høyreklikk-menyelementer - Open modify.nss file + Åpne modify.nss-fil - No tools available. Please add tools in tools folder in application directory. + Ingen verktøy tilgjengelig. Legg til verktøy i verktøy-mappen i programkatalogen. - Tools will appear here once they are available in the tools directory. + Verktøy vil vises her når de er tilgjengelige i verktøy-katalogen. - Base Directory: {0} + Basiskatalog: {0} - Configuration file directory not found + Konfigurasjonsfilkatalog ikke funnet - File not found + Fil ikke funnet - Cannot read configuration file: {0} + Kan ikke lese konfigurasjonsfil: {0} - This file is used to define theme-related style settings + Denne filen brukes til å definere temarelaterte stilinnstillinger - Theme color definition + Temafargedefinisjon - Base color + Basisfarge - Hover color + Helvefarge - Selected color + Valgt farge - For more theme configuration options, please refer to the official Nilesoft Shell documentation + For flere temakonfigurasjonsalternativer, vennligst se den offisielle Nilesoft Shell-dokumentasjonen - Are you sure you want to apply the configuration? - -This will: -1. Save all configuration files -2. Restart File Explorer to apply the configuration - -Note: Restarting will close all open folder windows and the taskbar, then automatically restart. + Er du sikker på at du vil bruke konfigurasjonen?\n\nDette vil:\n1. Lagre alle konfigurasjonsfiler\n2. Starte Filutforsker på nytt for å bruke konfigurasjonen\n\nMerk: Omstart vil lukke alle åpne mappevinduer og oppgavelinjen, og deretter starte automatisk på nytt. - Confirm Apply Configuration + Bekreft bruk av konfigurasjon - Cannot save configuration file: -{0} - -Please check file permissions and try again. + Kan ikke lagre konfigurasjonsfil:\n{0}\n\nVennligst sjekk filtillatelser og prøv igjen. - Save Failed + Lagring mislyktes - Successfully saved the following files: -{0} - - + Lagret følgende filer:\n{0}\n\n - Failed to save files: -{0} - - + Kunne ikke lagre filer:\n{0}\n\n - File Explorer has been restarted, configuration is now in effect! + Filutforsker har blitt startet på nytt, konfigurasjonen er nå aktiv! - Partially Applied + Delvis brukt - Applied Successfully + Brukt - Error applying configuration: -{0} - -You can manually save the files and restart the Explorer process in Task Manager. + Feil ved bruk av konfigurasjon:\n{0}\n\nDu kan manuelt lagre filene og starte Utforsker-prosessen på nytt i Oppgavebehandling. - Apply Failed + Bruk mislyktes - File does not exist or path is invalid + Filen eksisterer ikke eller banen er ugyldig - Open File Failed + Kunne ikke åpne fil - Use system default program to open file + Bruk systemets standardprogram for å åpne fil - Cannot open file: {0} + Kan ikke åpne fil: {0} - Open File Failed + Kunne ikke åpne fil - Folder does not exist or path is invalid + Mappen eksisterer ikke eller banen er ugyldig - Open Folder Failed + Kunne ikke åpne mappe - Cannot open folder: {0} + Kan ikke åpne mappe: {0} - Open Folder Failed + Kunne ikke åpne mappe Modell - Power Usage + Strømforbruk - Charge + Lading - Health + Helse - Rate + Hastighet - Min Rate + Min. hastighet - Max Rate + Maks. hastighet - Cycles + Sykluser - Capacity + Kapasitet - Full Cap + Full kap. Design - Date + Dato - Core / Memory Clock + Kjerne- / minneklokke - Core Voltage + Kjernespenning - Voltage Range + Spenningsområde - Avg Temp + Gjennomsnittstemp. - Sidebar + Sidefelt System - Rate Range + Hastighetsområde - Please select at least one cleanup option. + Vennligst velg minst ett oppryddingsalternativ. - Start All + Start alle - e.g. 82JQ + f.eks. 82JQ - e.g. C:\Drivers + f.eks. C:\Drivers - Enable Transparency + Aktiver gjennomsiktighet - Enable transparency effects for the context menu + Aktiver gjennomsiktighetseffekter for kontekstmenyen - Show File Extensions + Vis filendelser - Show file extensions in File Explorer + Vis filendelser i Filutforsker - Show Hidden Files + Vis skjulte filer - Show hidden files and folders in File Explorer + Vis skjulte filer og mapper i Filutforsker - Open to Quick Access + Åpne til Rask tilgang - Open File Explorer to Quick Access instead of This PC + Åpne Filutforsker til Rask tilgang i stedet for Denne PC-en - Show Preview Pane + Vis forhåndsvisningsrute - Show preview pane in File Explorer + Vis forhåndsvisningsrute i Filutforsker - Please select at least one item to clean up. + Velg minst ett element å rydde opp. + +Force software renderingUse software rendering when remote desktop or headless display is detected. Restart recommended. + Installasjonsprogrammet avsluttet med kode {0} + + + I kø + + + av {0} + + + Optimaliser + + + Rask åpning + + + Innstillinger + + + Oppdater + + + Sjekker tilgjengelighet for tilleggsprogram... + + + Tilgjengelig for installering + + + {0} ytterligere tilleggsprogram(er) er tilgjengelige i den aktuelle kilden. + + + Installert + + + Lagre Pulse + + + Totalt antall tilleggsprogrammer + + + Alle oppdagede tilleggsprogrammer er nå oppdaterte. + + + Oppdatert + + + Oppdateringer tilgjengelig + + + Oppdateringer klare + + + {0} tilleggsprogram-oppdatering(er) er klare for installering. + + + Venter på at metadata for tilleggsprogram lastes. + + + Laster metadata + + + Tilpass språk, temperaturenhet, tema og accentfarge. + + + Utseende + + + Applikasjon + + + Lenovo Legion Toolkit ble startet med {0}. Automatiske og manuelle oppdateringssjekk, pluss oppdateringsinnstillingene nedenfor, er deaktivert til du starter på nytt uten det oppstartsargumentet. + + + Oppdateringssjekk er deaktivert for denne økten + + + Kontroller valgt kilde og nettverkstilkobling, og søk igjen. + + + Driversøk ble ikke fullført + + + Juster filteret, oppdateringsvalg eller liste over skjulte nedlastinger. + + + Ingen matchende nedlastinger funnet + + + Prøv en annen kilde, operativsystem eller maskintype. + + + Ingen drivernedlastinger funnet + + + Velg en kilde og søk for å vise kompatible drivernedlastinger. + + + Søk etter driverpakker + + + Klarte ikke å bruke {0}: {1} -Force software renderingUse software rendering when remote desktop or headless display is detected. Restart recommended. \ No newline at end of file + \ No newline at end of file diff --git a/Tools/translate_resx.py b/Tools/translate_resx.py new file mode 100644 index 000000000..6c8cc8faa --- /dev/null +++ b/Tools/translate_resx.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Extract untranslated (English-identical) entries from .resx files and apply translations. + +Usage: + # Extract untranslated strings as JSON + python3 Tools/translate_resx.py extract --lang no --module WPF > /tmp/no_wpf.json + + # Apply translations from a JSON file + python3 Tools/translate_resx.py apply --lang no --module WPF /tmp/no_wpf_translated.json +""" +import argparse +import json +import os +import sys +import xml.etree.ElementTree as ET + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MODULES = { + "WPF": "LenovoLegionToolkit.WPF/Resources", + "Lib": "LenovoLegionToolkit.Lib/Resources", + "Automation": "LenovoLegionToolkit.Lib.Automation/Resources", + "Macro": "LenovoLegionToolkit.Lib.Macro/Resources", +} + +NS = "http://schemas.microsoft.com/winfx/2006/xaml" + + +def parse_resx(path): + """Parse a .resx file and return {name: (value_element, value_text)} preserving order.""" + tree = ET.parse(path) + root = tree.getroot() + entries = {} + for data in root.findall("data"): + name = data.get("name") + val_elem = data.find("value") + if val_elem is not None: + entries[name] = (val_elem, val_elem.text or "") + return entries, tree + + +def extract(lang, module): + """Return list of {name, english, translated} where translated == english (untranslated).""" + res_dir = os.path.join(REPO, MODULES[module]) + en_path = os.path.join(res_dir, "Resource.en.resx") + lang_path = os.path.join(res_dir, f"Resource.{lang}.resx") + + if not os.path.exists(en_path): + en_path = os.path.join(res_dir, "Resource.resx") # neutral = English + if not os.path.exists(lang_path): + print(f"ERROR: {lang_path} not found", file=sys.stderr) + sys.exit(1) + + en_entries, _ = parse_resx(en_path) + lang_entries, _ = parse_resx(lang_path) + + untranslated = [] + for name, (_, en_val) in en_entries.items(): + if name in lang_entries: + _, lang_val = lang_entries[name] + if lang_val == en_val: + untranslated.append({"name": name, "english": en_val}) + return untranslated + + +def apply(lang, module, translations_file): + """Apply translations from JSON file to the target .resx.""" + res_dir = os.path.join(REPO, MODULES[module]) + lang_path = os.path.join(res_dir, f"Resource.{lang}.resx") + + with open(translations_file, "r", encoding="utf-8") as f: + translations = json.load(f) + + trans_map = {t["name"]: t["translated"] for t in translations if "translated" in t} + + entries, tree = parse_resx(lang_path) + root = tree.getroot() + changed = 0 + + for data in root.findall("data"): + name = data.get("name") + if name in trans_map: + val_elem = data.find("value") + if val_elem is not None: + old = val_elem.text or "" + new = trans_map[name] + if old != new: + val_elem.text = new + changed += 1 + + # Preserve XML declaration and formatting + tree.write(lang_path, xml_declaration=True, encoding="utf-8-sig") + print(f"Applied {changed} translations to {lang_path}", file=sys.stderr) + + +def main(): + parser = argparse.ArgumentParser() + sub = parser.add_subparsers(dest="cmd") + + ext = sub.add_parser("extract") + ext.add_argument("--lang", required=True) + ext.add_argument("--module", required=True, choices=MODULES.keys()) + + app = sub.add_parser("apply") + app.add_argument("--lang", required=True) + app.add_argument("--module", required=True, choices=MODULES.keys()) + app.add_argument("file", help="JSON translations file") + + args = parser.parse_args() + if args.cmd == "extract": + result = extract(args.lang, args.module) + json.dump(result, sys.stdout, ensure_ascii=False, indent=2) + elif args.cmd == "apply": + apply(args.lang, args.module, args.file) + + +if __name__ == "__main__": + main() From 1f9f41c1e798d83d1483407974e16c3371abc5fc Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 3 May 2026 20:32:47 +0800 Subject: [PATCH 02/24] fix(i18n): preserve resx XML structure in translate tool and ca.resx - Rewrite translate_resx.py apply() to use regex instead of ET.tree.write(), preserving XML comments, namespace declarations, and original formatting - Restore ca.resx original XML structure (only 2 data-value changes) instead of tree.write() schema rewrite - Remove unused NS constant, add file-existence validation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Resources/Resource.ca.resx | 117 +++++++++++++----- Tools/translate_resx.py | 61 ++++++--- 2 files changed, 131 insertions(+), 47 deletions(-) diff --git a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx index e277877dc..072000b38 100644 --- a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx +++ b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.ca.resx @@ -1,34 +1,91 @@ - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx diff --git a/Tools/translate_resx.py b/Tools/translate_resx.py index 6c8cc8faa..f60db3112 100644 --- a/Tools/translate_resx.py +++ b/Tools/translate_resx.py @@ -11,6 +11,7 @@ import argparse import json import os +import re import sys import xml.etree.ElementTree as ET @@ -22,8 +23,6 @@ "Macro": "LenovoLegionToolkit.Lib.Macro/Resources", } -NS = "http://schemas.microsoft.com/winfx/2006/xaml" - def parse_resx(path): """Parse a .resx file and return {name: (value_element, value_text)} preserving order.""" @@ -62,33 +61,61 @@ def extract(lang, module): return untranslated +def escape_xml(text): + """Escape XML special characters in translation values.""" + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + ) + + def apply(lang, module, translations_file): - """Apply translations from JSON file to the target .resx.""" + """Apply translations from JSON file to the target .resx. + + Uses regex replacement to preserve the original XML structure, + comments, and namespace declarations intact. + """ res_dir = os.path.join(REPO, MODULES[module]) lang_path = os.path.join(res_dir, f"Resource.{lang}.resx") + if not os.path.exists(lang_path): + print(f"ERROR: {lang_path} not found", file=sys.stderr) + sys.exit(1) + if not os.path.exists(translations_file): + print(f"ERROR: {translations_file} not found", file=sys.stderr) + sys.exit(1) + with open(translations_file, "r", encoding="utf-8") as f: translations = json.load(f) trans_map = {t["name"]: t["translated"] for t in translations if "translated" in t} - entries, tree = parse_resx(lang_path) - root = tree.getroot() + with open(lang_path, "r", encoding="utf-8-sig") as f: + content = f.read() + changed = 0 - for data in root.findall("data"): - name = data.get("name") + def replace_value(match): + nonlocal changed + name = match.group(1) + old_value = match.group(2) if name in trans_map: - val_elem = data.find("value") - if val_elem is not None: - old = val_elem.text or "" - new = trans_map[name] - if old != new: - val_elem.text = new - changed += 1 - - # Preserve XML declaration and formatting - tree.write(lang_path, xml_declaration=True, encoding="utf-8-sig") + new_value = trans_map[name] + if old_value != new_value: + changed += 1 + return ( + f'\n' + f" {escape_xml(new_value)}" + ) + return match.group(0) + + pattern = r'\s*\n\s*(.*?)' + content = re.sub(pattern, replace_value, content, flags=re.DOTALL) + + with open(lang_path, "w", encoding="utf-8-sig") as f: + f.write(content) + print(f"Applied {changed} translations to {lang_path}", file=sys.stderr) From 1107cec4f11ac85b869770268db748aec9deccac Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 4 May 2026 12:32:08 +0800 Subject: [PATCH 03/24] refactor: Phase 2 MVVM refactor, structured logging, and test expansion - Add Serilog structured logging (Log.cs rewrite with level support) - Add CrashReportHelper for global exception handling - Create 5 ViewModels: Packages, Settings, Automation, KeyboardBacklight, Macro - Simplify KeyboardBacklightPage and MacroPage code-behind - Migrate Features/Controllers/Listeners log calls to proper levels - Extract Plugin module to Lib.Plugins project - Add GameDetection and SoftwareDisabler test coverage - Fix internal class access errors in GameDetectorTests - Add pt-br and zh-hant resource files - Build: 0 errors 0 warnings, Tests: 1220 passed, 0 failed, 16 skipped Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 7 + Directory.Packages.props | 8 + .../Resources/Resource.pt-br.resx | 230 + .../Resources/Resource.zh-hant.resx | 178 + .../Resources/Resource.pt-br.resx | 106 + .../Resources/Resource.zh-hant.resx | 49 + .../DependencyResolver.cs | 5 +- .../IAppStartupPlugin.cs | 0 .../IDependencyResolver.cs | 0 .../IOptimizationCategoryProvider.cs | 0 .../IPlugin.cs | 0 .../IPluginConfiguration.cs | 0 .../IPluginHostContext.cs | 0 .../IPluginHotReload.cs | 0 .../IPluginManager.cs | 0 .../IPluginPage.cs | 0 .../IPluginSandbox.cs | 0 .../IShellIntegrationHelper.cs | 0 LenovoLegionToolkit.Lib.Plugins/IoCModule.cs | 29 + .../LenovoLegionToolkit.Lib.Plugins.csproj | 14 + .../OptimizationCategoryExtender.cs | 69 + .../PluginBase.cs | 0 .../PluginConfiguration.cs | 0 .../PluginConstants.cs | 0 .../PluginFileSystemManager.cs | 9 +- .../PluginHostContext.cs | 0 .../PluginHostMode.cs | 0 .../PluginHotReload.cs | 9 +- .../PluginInstallationService.cs | 24 +- .../PluginLoader.cs | 6 +- .../PluginManager.cs | 0 .../PluginManifest.cs | 0 .../PluginManifestAdapter.cs | 0 .../PluginMetadata.cs | 0 .../PluginPaths.cs | 0 .../PluginRegistry.cs | 0 .../PluginRepositoryService.cs | 21 +- .../PluginSandbox.cs | 12 +- .../PluginSignatureSettings.cs | 0 .../PluginSignatureValidator.cs | 20 +- .../PluginState.cs | 0 .../PluginUpdateManager.cs | 0 .../VersionChecker.cs | 6 +- .../AutoListeners/AbstractAutoListener.cs | 3 +- .../AutoListeners/GameAutoListener.cs | 9 +- .../AutoListeners/ProcessAutoListener.cs | 3 +- .../AutoListeners/WiFiAutoListener.cs | 6 +- .../Controllers/GPUOverclockController.cs | 15 +- .../RGBKeyboardBacklightController.cs | 3 +- .../Features/AbstractDriverFeature.cs | 6 +- .../Features/AbstractLenovoLightingFeature.cs | 3 +- .../Features/AbstractUEFIFeature.cs | 12 +- .../Features/HDRFeature.cs | 3 +- .../Features/Hybrid/HybridModeFeature.cs | 3 +- .../Hybrid/Notify/AbstractDGPUNotify.cs | 6 +- LenovoLegionToolkit.Lib/IoCModule.cs | 21 - .../LenovoLegionToolkit.Lib.csproj | 3 + .../Listeners/AbstractEventLogListener.cs | 3 +- .../Listeners/AbstractWMIListener.cs | 12 +- .../Listeners/DisplayBrightnessListener.cs | 3 +- .../Listeners/DisplayConfigurationListener.cs | 3 +- .../Listeners/DriverKeyListener.cs | 6 +- .../Listeners/NativeWindowsMessageListener.cs | 51 +- .../Listeners/PowerStateListener.cs | 3 +- .../Listeners/RGBKeyboardBacklightListener.cs | 3 +- .../Listeners/SessionLockUnlockListener.cs | 6 +- .../Listeners/SpecialKeyListener.cs | 9 +- .../Listeners/SystemThemeListener.cs | 3 +- .../Listeners/ThermalModeListener.cs | 3 +- .../IOptimizationCategoryExtender.cs | 12 + .../WindowsOptimizationCategoryProvider.cs | 1 - .../WindowsOptimizationService.cs | 39 +- .../Resources/Resource.pt-br.resx | 499 ++ .../Resources/Resource.zh-hant.resx | 447 ++ LenovoLegionToolkit.Lib/Utils/Log.cs | 570 +-- .../GameDetection/GameDetectorTests.cs | 275 ++ .../GameDetectionTests.cs | 287 ++ .../LenovoLegionToolkit.Tests.csproj | 1 + .../SoftwareDisabler/SoftwareDisablerTests.cs | 388 ++ .../SoftwareDisablerTests.cs | 357 ++ LenovoLegionToolkit.WPF/App.xaml.cs | 87 + .../LenovoLegionToolkit.WPF.csproj | 3 +- .../Pages/KeyboardBacklightPage.xaml.cs | 37 +- .../Pages/MacroPage.xaml.cs | 11 +- .../Resources/Resource.Designer.cs | 72 + .../Resources/Resource.ar.resx | 1140 ++++- .../Resources/Resource.bg.resx | 1073 ++++- .../Resources/Resource.bs.resx | 1073 ++++- .../Resources/Resource.ca.resx | 1073 ++++- .../Resources/Resource.cs.resx | 1073 ++++- .../Resources/Resource.de.resx | 1036 ++++- .../Resources/Resource.el.resx | 1073 ++++- .../Resources/Resource.en.resx | 123 +- .../Resources/Resource.es.resx | 1035 ++++- .../Resources/Resource.fr.resx | 1035 ++++- .../Resources/Resource.hu.resx | 1072 ++++- .../Resources/Resource.it.resx | 1037 ++++- .../Resources/Resource.ja.resx | 1035 ++++- .../Resources/Resource.ko.resx | 1039 ++++- .../Resources/Resource.lv.resx | 1072 ++++- .../Resources/Resource.nl.resx | 1072 ++++- .../Resources/Resource.pl.resx | 1038 ++++- .../Resources/Resource.pt-br.resx | 4006 ++++++++++++++++ .../Resources/Resource.pt.resx | 1074 ++++- .../Resources/Resource.resx | 24 + .../Resources/Resource.ro.resx | 1072 ++++- .../Resources/Resource.ru.resx | 1038 ++++- .../Resources/Resource.sk.resx | 1072 ++++- .../Resources/Resource.tr.resx | 1038 ++++- .../Resources/Resource.uk.resx | 1037 ++++- .../Resources/Resource.uz.resx | 1072 ++++- .../Resources/Resource.vi.resx | 1037 ++++- .../Resources/Resource.zh-hant.resx | 4052 +++++++++++++++++ .../Resources/Resource.zh.resx | 2 +- .../Utils/CrashReportHelper.cs | 260 ++ .../ViewModels/AutomationViewModel.cs | 143 + .../ViewModels/KeyboardBacklightViewModel.cs | 78 + .../ViewModels/MacroViewModel.cs | 39 + .../ViewModels/PackagesViewModel.cs | 104 + .../ViewModels/SettingsViewModel.cs | 84 + .../Utils/CrashReportNotificationWindow.xaml | 152 + .../CrashReportNotificationWindow.xaml.cs | 167 + LenovoLegionToolkit.sln | 14 + 123 files changed, 37592 insertions(+), 1161 deletions(-) create mode 100644 LenovoLegionToolkit.Lib.Automation/Resources/Resource.pt-br.resx create mode 100644 LenovoLegionToolkit.Lib.Automation/Resources/Resource.zh-hant.resx create mode 100644 LenovoLegionToolkit.Lib.Macro/Resources/Resource.pt-br.resx create mode 100644 LenovoLegionToolkit.Lib.Macro/Resources/Resource.zh-hant.resx rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/DependencyResolver.cs (98%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IAppStartupPlugin.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IDependencyResolver.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IOptimizationCategoryProvider.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPlugin.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginConfiguration.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginHostContext.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginHotReload.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginManager.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginPage.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IPluginSandbox.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/IShellIntegrationHelper.cs (100%) create mode 100644 LenovoLegionToolkit.Lib.Plugins/IoCModule.cs create mode 100644 LenovoLegionToolkit.Lib.Plugins/LenovoLegionToolkit.Lib.Plugins.csproj create mode 100644 LenovoLegionToolkit.Lib.Plugins/OptimizationCategoryExtender.cs rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginBase.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginConfiguration.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginConstants.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginFileSystemManager.cs (96%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginHostContext.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginHostMode.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginHotReload.cs (98%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginInstallationService.cs (93%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginLoader.cs (98%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginManager.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginManifest.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginManifestAdapter.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginMetadata.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginPaths.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginRegistry.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginRepositoryService.cs (98%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginSandbox.cs (97%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginSignatureSettings.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginSignatureValidator.cs (90%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginState.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/PluginUpdateManager.cs (100%) rename {LenovoLegionToolkit.Lib/Plugins => LenovoLegionToolkit.Lib.Plugins}/VersionChecker.cs (97%) create mode 100644 LenovoLegionToolkit.Lib/Optimization/IOptimizationCategoryExtender.cs create mode 100644 LenovoLegionToolkit.Lib/Resources/Resource.pt-br.resx create mode 100644 LenovoLegionToolkit.Lib/Resources/Resource.zh-hant.resx create mode 100644 LenovoLegionToolkit.Tests/GameDetection/GameDetectorTests.cs create mode 100644 LenovoLegionToolkit.Tests/GameDetectionTests.cs create mode 100644 LenovoLegionToolkit.Tests/SoftwareDisabler/SoftwareDisablerTests.cs create mode 100644 LenovoLegionToolkit.Tests/SoftwareDisablerTests.cs create mode 100644 LenovoLegionToolkit.WPF/Resources/Resource.pt-br.resx create mode 100644 LenovoLegionToolkit.WPF/Resources/Resource.zh-hant.resx create mode 100644 LenovoLegionToolkit.WPF/Utils/CrashReportHelper.cs create mode 100644 LenovoLegionToolkit.WPF/ViewModels/AutomationViewModel.cs create mode 100644 LenovoLegionToolkit.WPF/ViewModels/KeyboardBacklightViewModel.cs create mode 100644 LenovoLegionToolkit.WPF/ViewModels/MacroViewModel.cs create mode 100644 LenovoLegionToolkit.WPF/ViewModels/PackagesViewModel.cs create mode 100644 LenovoLegionToolkit.WPF/ViewModels/SettingsViewModel.cs create mode 100644 LenovoLegionToolkit.WPF/Windows/Utils/CrashReportNotificationWindow.xaml create mode 100644 LenovoLegionToolkit.WPF/Windows/Utils/CrashReportNotificationWindow.xaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 33af88ddd..8293f866e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Improved / 改进 +- 新增 `Tools/seed_pt_br_zh_hant_resx.py`,在四个资源目录下创建 `Resource.pt-br.resx`(自 `Resource.pt.resx` 种子)与 `Resource.zh-hant.resx`(自基准 `Resource.resx` 的英文占位),与 `LocalizationHelper` / `crowdin.yml` 的 `pt-BR`、`zh-TW` 映射对齐 / Added `Tools/seed_pt_br_zh_hant_resx.py` to create `Resource.pt-br.resx` (seeded from `Resource.pt.resx`) and `Resource.zh-hant.resx` (seeded from neutral `Resource.resx` as English placeholders) across the four resource modules, aligning on-disk filenames with `LocalizationHelper` / `crowdin.yml` `pt-BR` and `zh-TW` mappings +- 已从基准 `Resource.resx` 回填 WPF 插件扩展等相关字符串键至全部卫星资源文件,`missing` 结构性缺口归零(占位文案为英文,可由 Crowdin 后续本地化)/ Backfilled plugin-extensions-related string keys from neutral `Resource.resx` into all WPF satellite resource files so structural `missing` gaps are cleared (English placeholders pending Crowdin localization) +- 新增 `Tools/resx_translation_audit.py`,用于对四个资源模块的 `Resource.*.resx` 做缺失键、多余键、.NET 格式占位符一致性与英文残留(疑似未译)统计,便于发布前本地化自检 / Added `Tools/resx_translation_audit.py` to audit satellite `Resource.*.resx` files across the four resource modules for missing/extra keys, .NET placeholder parity, and English-identical strings for pre-release localization checks - 扩展 2025 年 Lenovo Legion 与 LOQ 机型识别,补齐 Gen 10 `15AKP`、`15IRX`、`16ADR`、`16AFR`、`17IRX`、`18IAX` 型号前缀 / Expanded 2025 Lenovo Legion and LOQ model detection by adding Gen 10 `15AKP`, `15IRX`, `16ADR`, `16AFR`, `17IRX`, and `18IAX` model prefixes - 加固发布和仓库治理:安装器改为检测 .NET 10 Desktop Runtime,发布流水线在打包前运行测试,并新增 CodeQL、Dependabot、Issue/PR 模板和分支保护配置 / Hardened release and repository governance by switching the installer to .NET 10 Desktop Runtime detection, running tests before release packaging, and adding CodeQL, Dependabot, Issue/PR templates, and branch protection configuration - 更新已验证兼容的 NuGet 依赖版本,并修正贡献指南、部署文档和安装器元数据中的旧仓库链接 / Updated verified-compatible NuGet dependency versions and corrected stale repository links in contribution guides, deployment docs, and installer metadata - 将 CLI 迁移到 `System.CommandLine` 2.0.7 稳定 API,保留现有命令、别名、验证错误和 IPC 失败提示行为 / Migrated the CLI to the stable `System.CommandLine` 2.0.7 APIs while preserving existing commands, aliases, validation errors, and IPC failure messages +### Fixed / 修复 +- 挪威语 `Resource.no.resx` 中 `CopiedToClipboard_Message_WithParam` 补回 `{0}` 占位符,避免格式化参数丢失 / Restored the `{0}` placeholder in Norwegian `CopiedToClipboard_Message_WithParam` so clipboard notifications preserve the formatted argument +- 中文界面中的传感器频率单位改为标准 `GHz` 缩写 / Changed the sensor frequency unit in Chinese UI to the standard `GHz` abbreviation + ## [3.6.15] - 2026-04-29 ### Improved / 改进 diff --git a/Directory.Packages.props b/Directory.Packages.props index c3c2601fa..cdf78d0fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,8 +21,16 @@ + + + + + + + + diff --git a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.pt-br.resx b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.pt-br.resx new file mode 100644 index 000000000..9c6bfa953 --- /dev/null +++ b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.pt-br.resx @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Quando o carregador for ligado + + + Quando o carregador for desligado + + + Fechar apps + + + Reiniciar GPU + + + {0} segundos + + + {0} segundos + + + Quando um carregador de baixa voltagem for ligado + + + Ao ligar + + + Quando o esquema de energia é alterado + + + Quando uma aplicação inicia + + + Quando a app fecha + + + A uma hora específica + + + Quando um monitor externo é ligado + + + Quando um monitor externo é desligado + + + Quando os ecrãs ligam + + + Quando os ecrãs desligam + + + Tampa é aberta + + + Tampa fechada + + + Quando um jogo fecha + + + Quando um jogo está a rodar + + + Quando o utilizador fica ativo + + + Quando o utilizador fica inativo + + + Desativar GPU + The display name of the default Quick Action that is presented to the user upon first installation. + + + Desligado + + + Ligado + + + Quando a predefinição do Modo Customizado muda + + + Ação periódica + The display name of the periodic automation action. + + + Quando o Wi-Fi estiver conectado + + + Quando o Wi-Fi estiver desconectado + + + Ao continuar + + + Quando o HDR for desligado + + + Quando o HDR for ligado + + + Desligado + + + Ligado + + + Quando o dispositivo estiver desconectado + + + Quando o dispositivo estiver conectado + +Sessão bloqueadaSessão desbloqueada \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib.Automation/Resources/Resource.zh-hant.resx b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.zh-hant.resx new file mode 100644 index 000000000..1a3d09f87 --- /dev/null +++ b/LenovoLegionToolkit.Lib.Automation/Resources/Resource.zh-hant.resx @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 當電源適配器插入時 + + + 當電源適配器斷開時 + + + 強制關閉應用 + + + 重啓顯卡 + + + {0} 秒 + + + {0} 秒 + + + 當較低功率電源適配器插入時 + + + 當開機時 + + + 當性能模式改變時 + + + 當應用程序啓動時 + + + 當指定的應用關閉時 + + + 在特定的時間 + + + 當連接了外置屏幕後 + + + 當斷開了外置屏幕後 + + + 當顯示器打開時 + + + 當顯示器關閉時 + + + 打開蓋子時 + + + 合上蓋子時 + + + 當關閉遊戲後 + + + 當打開遊戲時 + + + 當用戶重新開始操作電腦後 + + + 當用戶長時間未操作電腦後 + + + 強制休眠獨顯 + The display name of the default Quick Action that is presented to the user upon first installation. + + + + + + + + + 當自定義模式預設切換時 + + + 循環自動化 + The display name of the periodic automation action. + + + 當與 Wi-Fi 連接時 + + + 當與 Wi-Fi 斷開連接時 + + + 當喚醒時 + + + 當 HDR 關閉時 + + + 當 HDR 開啓時 + + + + + + + + + 當與設備斷開連接時 + + + 當與設備連接時 + + + 當會話鎖定時 + + + 當會話解鎖時 + + \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.pt-br.resx b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.pt-br.resx new file mode 100644 index 000000000..95b243f4f --- /dev/null +++ b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.pt-br.resx @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Teclado + + + Rato + + \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib.Macro/Resources/Resource.zh-hant.resx b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.zh-hant.resx new file mode 100644 index 000000000..172cc6629 --- /dev/null +++ b/LenovoLegionToolkit.Lib.Macro/Resources/Resource.zh-hant.resx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 鍵盤 + + + 鼠標 + + \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib/Plugins/DependencyResolver.cs b/LenovoLegionToolkit.Lib.Plugins/DependencyResolver.cs similarity index 98% rename from LenovoLegionToolkit.Lib/Plugins/DependencyResolver.cs rename to LenovoLegionToolkit.Lib.Plugins/DependencyResolver.cs index 00ae2f44a..c28730d2d 100644 --- a/LenovoLegionToolkit.Lib/Plugins/DependencyResolver.cs +++ b/LenovoLegionToolkit.Lib.Plugins/DependencyResolver.cs @@ -92,10 +92,7 @@ public DependencyResolutionResult ResolveDependencies(Dictionary new PluginSignatureValidator(PluginSignatureSettings.CreateForCurrentProcess())) + .As() + .SingleInstance(); + + builder.Register().As().SingleInstance(); + builder.Register().As().SingleInstance(); + builder.Register().As().SingleInstance(); + + builder.Register().As().SingleInstance(); + + builder.Register().AsSelf().SingleInstance(); + + // Register optimization category extender so Lib can discover plugin categories + // without a direct circular reference to Lib.Plugins. + builder.RegisterType() + .As() + .SingleInstance(); + } +} diff --git a/LenovoLegionToolkit.Lib.Plugins/LenovoLegionToolkit.Lib.Plugins.csproj b/LenovoLegionToolkit.Lib.Plugins/LenovoLegionToolkit.Lib.Plugins.csproj new file mode 100644 index 000000000..8e7cc6581 --- /dev/null +++ b/LenovoLegionToolkit.Lib.Plugins/LenovoLegionToolkit.Lib.Plugins.csproj @@ -0,0 +1,14 @@ + + + net10.0-windows + LenovoLegionToolkit.Lib.Plugins + enable + enable + + + + + + + + diff --git a/LenovoLegionToolkit.Lib.Plugins/OptimizationCategoryExtender.cs b/LenovoLegionToolkit.Lib.Plugins/OptimizationCategoryExtender.cs new file mode 100644 index 000000000..79c67ab32 --- /dev/null +++ b/LenovoLegionToolkit.Lib.Plugins/OptimizationCategoryExtender.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LenovoLegionToolkit.Lib.Optimization; +using LenovoLegionToolkit.Lib.Utils; + +namespace LenovoLegionToolkit.Lib.Plugins; + +/// +/// Provides plugin-based optimization categories by querying installed plugins. +/// +public class OptimizationCategoryExtender : IOptimizationCategoryExtender +{ + private readonly IPluginManager _pluginManager; + + public OptimizationCategoryExtender(IPluginManager pluginManager) + { + _pluginManager = pluginManager; + } + + public IReadOnlyList GetPluginCategories() + { + var list = new List(); + + try + { + var installedPlugins = _pluginManager.GetRegisteredPlugins() + .Where(p => _pluginManager.IsInstalled(p.Id)); + + foreach (var plugin in installedPlugins) + { + try + { + WindowsOptimizationCategoryDefinition? category = null; + + if (plugin is IOptimizationCategoryProvider provider) + { + category = provider.GetOptimizationCategory(); + } + else if (plugin is PluginBase pluginBase) + { + category = pluginBase.GetOptimizationCategory(); + } + + if (category != null) + { + if (string.IsNullOrEmpty(category.PluginId)) + { + category = category with { PluginId = plugin.Id }; + } + list.Add(category); + } + } + catch (Exception ex) + { + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Failed to get optimization category from plugin {plugin.Id}: {ex.Message}", ex); + } + } + } + catch (Exception ex) + { + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Failed to get optimization categories from plugins", ex); + } + + return list; + } +} diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginBase.cs b/LenovoLegionToolkit.Lib.Plugins/PluginBase.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginBase.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginBase.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginConfiguration.cs b/LenovoLegionToolkit.Lib.Plugins/PluginConfiguration.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginConfiguration.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginConfiguration.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginConstants.cs b/LenovoLegionToolkit.Lib.Plugins/PluginConstants.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginConstants.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginConstants.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginFileSystemManager.cs b/LenovoLegionToolkit.Lib.Plugins/PluginFileSystemManager.cs similarity index 96% rename from LenovoLegionToolkit.Lib/Plugins/PluginFileSystemManager.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginFileSystemManager.cs index 631f07092..aa20d823c 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginFileSystemManager.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginFileSystemManager.cs @@ -124,8 +124,7 @@ public List GetPluginDllFiles() if (!PathSecurity.IsValidDirectoryPath(pluginsDirectory)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Invalid plugins directory path: {pluginsDirectory}"); + Log.Instance.Warning($"SECURITY: Invalid plugins directory path: {pluginsDirectory}"); return new List(); } @@ -176,16 +175,14 @@ private bool IsPluginDll(string filePath, string? parentDirectoryName = null) var pluginsDirectory = GetPluginsDirectory(); if (!PathSecurity.IsPathWithinAllowedDirectory(filePath, pluginsDirectory)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Plugin DLL path outside allowed directory: {filePath}"); + Log.Instance.Warning($"SECURITY: Plugin DLL path outside allowed directory: {filePath}"); return false; } var fileName = Path.GetFileName(filePath); if (!PathSecurity.IsValidFileName(fileName)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Invalid plugin DLL file name: {fileName}"); + Log.Instance.Warning($"SECURITY: Invalid plugin DLL file name: {fileName}"); return false; } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginHostContext.cs b/LenovoLegionToolkit.Lib.Plugins/PluginHostContext.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginHostContext.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginHostContext.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginHostMode.cs b/LenovoLegionToolkit.Lib.Plugins/PluginHostMode.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginHostMode.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginHostMode.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginHotReload.cs b/LenovoLegionToolkit.Lib.Plugins/PluginHotReload.cs similarity index 98% rename from LenovoLegionToolkit.Lib/Plugins/PluginHotReload.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginHotReload.cs index 49e2f5b0d..b911e370e 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginHotReload.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginHotReload.cs @@ -222,8 +222,7 @@ public async Task ReloadPluginAsync(string pluginId, string newAssemblyPat { stopwatch.Stop(); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to reload plugin {pluginId}: {ex.Message}", ex); + Log.Instance.Error($"Failed to reload plugin {pluginId}: {ex.Message}", ex); // Raise reload failed event var failedArgs = new HotReloadEventArgs @@ -651,13 +650,11 @@ private void LoadSavedStates() } } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Loaded {_savedStates.Count} saved plugin states"); + Log.Instance.Info($"Loaded {_savedStates.Count} saved plugin states"); } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to load saved states: {ex.Message}"); + Log.Instance.Warning($"Failed to load saved states: {ex.Message}"); } } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginInstallationService.cs b/LenovoLegionToolkit.Lib.Plugins/PluginInstallationService.cs similarity index 93% rename from LenovoLegionToolkit.Lib/Plugins/PluginInstallationService.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginInstallationService.cs index c73a7136f..95bd4cd80 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginInstallationService.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginInstallationService.cs @@ -33,16 +33,14 @@ public async Task ExtractAndInstallPluginAsync(string zipFilePath, string // SECURITY: Validate zip file path if (string.IsNullOrWhiteSpace(zipFilePath) || !File.Exists(zipFilePath)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace("SECURITY: Invalid or non-existent ZIP file path"); + Log.Instance.Warning("SECURITY: Invalid or non-existent ZIP file path"); return false; } // SECURITY: Validate plugins directory if (string.IsNullOrWhiteSpace(pluginsDir)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace("SECURITY: Invalid plugins directory"); + Log.Instance.Warning("SECURITY: Invalid plugins directory"); return false; } @@ -139,8 +137,7 @@ public async Task ExtractAndInstallPluginAsync(string zipFilePath, string } } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to install plugin from {zipFilePath}: {ex.Message}", ex); + Log.Instance.Error($"Failed to install plugin from {zipFilePath}: {ex.Message}", ex); throw; } finally @@ -204,8 +201,7 @@ private async Task ValidatePluginAsync(string pluginDir) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Plugin validation error: {ex.Message}", ex); + Log.Instance.Warning($"Plugin validation error: {ex.Message}", ex); return false; } } @@ -333,8 +329,7 @@ private static string NormalizePluginToken(string value) { if (!PathSecurity.IsValidDirectoryPath(pluginDir)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Invalid plugin directory path: {pluginDir}"); + Log.Instance.Warning($"SECURITY: Invalid plugin directory path: {pluginDir}"); return null; } @@ -343,8 +338,7 @@ private static string NormalizePluginToken(string value) if (!PathSecurity.IsPathWithinAllowedDirectory(manifestPath, fullPluginDir)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Manifest path traversal detected: {manifestPath}"); + Log.Instance.Warning($"SECURITY: Manifest path traversal detected: {manifestPath}"); return null; } @@ -448,8 +442,7 @@ private static void ExtractZipSafely(string zipFilePath, string extractDir) entry.FullName.StartsWith("/") || entry.FullName.StartsWith("\\")) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Skipping ZIP entry with suspicious path: {entry.FullName}"); + Log.Instance.Warning($"SECURITY: Skipping ZIP entry with suspicious path: {entry.FullName}"); continue; } @@ -462,8 +455,7 @@ private static void ExtractZipSafely(string zipFilePath, string extractDir) if (!fullDestinationPath.StartsWith(fullExtractDir, StringComparison.OrdinalIgnoreCase)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: ZIP entry escapes extract directory: {entry.FullName}"); + Log.Instance.Warning($"SECURITY: ZIP entry escapes extract directory: {entry.FullName}"); continue; } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginLoader.cs b/LenovoLegionToolkit.Lib.Plugins/PluginLoader.cs similarity index 98% rename from LenovoLegionToolkit.Lib/Plugins/PluginLoader.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginLoader.cs index 3ffee8d49..3bf2f4100 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginLoader.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginLoader.cs @@ -72,8 +72,7 @@ public class PluginLoader : IPluginLoader var signatureResult = await signatureValidator.ValidateAsync(dllPath); if (!signatureResult.IsValid) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Plugin signature validation failed for {dllPath}. Status: {signatureResult.Status}, Error: {signatureResult.ErrorMessage}"); + Log.Instance.Warning($"Plugin signature validation failed for {dllPath}. Status: {signatureResult.Status}, Error: {signatureResult.ErrorMessage}"); return null; } @@ -248,8 +247,7 @@ private static void RemovePluginDependencyResolutionContext(PluginDependencyReso var signatureResult = signatureValidator.ValidateAsync(candidatePath).GetAwaiter().GetResult(); if (!IsValidPluginDependencySignature(signatureResult, requestedAssemblyName, candidatePath)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Rejected plugin dependency due to invalid signature. [path={candidatePath}, status={signatureResult.Status}, error={signatureResult.ErrorMessage}]"); + Log.Instance.Warning($"Rejected plugin dependency due to invalid signature. [path={candidatePath}, status={signatureResult.Status}, error={signatureResult.ErrorMessage}]"); return null; } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginManager.cs b/LenovoLegionToolkit.Lib.Plugins/PluginManager.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginManager.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginManager.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginManifest.cs b/LenovoLegionToolkit.Lib.Plugins/PluginManifest.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginManifest.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginManifest.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginManifestAdapter.cs b/LenovoLegionToolkit.Lib.Plugins/PluginManifestAdapter.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginManifestAdapter.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginManifestAdapter.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginMetadata.cs b/LenovoLegionToolkit.Lib.Plugins/PluginMetadata.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginMetadata.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginMetadata.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginPaths.cs b/LenovoLegionToolkit.Lib.Plugins/PluginPaths.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginPaths.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginPaths.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginRegistry.cs b/LenovoLegionToolkit.Lib.Plugins/PluginRegistry.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginRegistry.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginRegistry.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginRepositoryService.cs b/LenovoLegionToolkit.Lib.Plugins/PluginRepositoryService.cs similarity index 98% rename from LenovoLegionToolkit.Lib/Plugins/PluginRepositoryService.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginRepositoryService.cs index c965b6fa9..e1f5ea33c 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginRepositoryService.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginRepositoryService.cs @@ -115,15 +115,13 @@ public async Task> FetchAvailablePluginsAsync() return manifest; }).ToList(); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Found {plugins.Count} plugins in store"); + Log.Instance.Info($"Found {plugins.Count} plugins in store"); return plugins; } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error fetching plugins from store: {ex.Message}", ex); + Log.Instance.Error($"Error fetching plugins from store: {ex.Message}", ex); throw; } } @@ -190,8 +188,7 @@ public async Task DownloadAndInstallPluginAsync(PluginManifest manifest) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error installing plugin {manifest.Id}: {ex.Message}", ex); + Log.Instance.Error($"Error installing plugin {manifest.Id}: {ex.Message}", ex); DownloadFailed?.Invoke(this, ex.Message); return false; } @@ -706,24 +703,21 @@ private async Task ExtractAndInstallPluginAsync(string zipPath, string ext if (!string.IsNullOrEmpty(manifest.FileHash) && !hashString.Equals(manifest.FileHash, StringComparison.OrdinalIgnoreCase)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Hash mismatch for {manifest.Id}. Expected: {manifest.FileHash}, Got: {hashString}"); + Log.Instance.Warning($"Hash mismatch for {manifest.Id}. Expected: {manifest.FileHash}, Got: {hashString}"); return false; } // SECURITY: Validate plugin ID before using in path construction if (!PathSecurity.IsValidPluginId(manifest.Id)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Invalid plugin ID format: {manifest.Id}"); + Log.Instance.Warning($"SECURITY: Invalid plugin ID format: {manifest.Id}"); return false; } // SECURITY: Verify the constructed path is within allowed directory if (!PathSecurity.IsPathWithinAllowedDirectory(pluginDir, _pluginsDirectory)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"SECURITY: Plugin directory path traversal detected: {pluginDir}"); + Log.Instance.Warning($"SECURITY: Plugin directory path traversal detected: {pluginDir}"); return false; } if (Directory.Exists(pluginDir)) @@ -809,8 +803,7 @@ private async Task ExtractAndInstallPluginAsync(string zipPath, string ext { await RestorePluginDirectoryAsync(pluginDir, backupDir, manifest.Id).ConfigureAwait(false); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error extracting plugin {manifest.Id}: {ex.Message}", ex); + Log.Instance.Error($"Error extracting plugin {manifest.Id}: {ex.Message}", ex); return false; } } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginSandbox.cs b/LenovoLegionToolkit.Lib.Plugins/PluginSandbox.cs similarity index 97% rename from LenovoLegionToolkit.Lib/Plugins/PluginSandbox.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginSandbox.cs index 17fe31467..eaf1fbc39 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginSandbox.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginSandbox.cs @@ -60,8 +60,7 @@ public bool CreateSandbox(string pluginId, string assemblyPath, SandboxConfigura } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to create sandbox for plugin {pluginId}: {ex.Message}", ex); + Log.Instance.Warning($"Failed to create sandbox for plugin {pluginId}: {ex.Message}", ex); return false; } } @@ -121,15 +120,13 @@ public bool CreateSandbox(string pluginId, string assemblyPath, SandboxConfigura // Start resource monitoring StartResourceMonitoring(context); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Successfully loaded plugin {pluginId} ({plugin.Name} v{context.Info.Version})"); + Log.Instance.Info($"Successfully loaded plugin {pluginId} ({plugin.Name} v{context.Info.Version})"); return plugin; } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to load plugin {pluginId}: {ex.Message}", ex); + Log.Instance.Error($"Failed to load plugin {pluginId}: {ex.Message}", ex); return null; } } @@ -424,8 +421,7 @@ public bool DestroySandbox(string pluginId) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error destroying sandbox for {pluginId}: {ex.Message}", ex); + Log.Instance.Warning($"Error destroying sandbox for {pluginId}: {ex.Message}", ex); return false; } } diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginSignatureSettings.cs b/LenovoLegionToolkit.Lib.Plugins/PluginSignatureSettings.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginSignatureSettings.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginSignatureSettings.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginSignatureValidator.cs b/LenovoLegionToolkit.Lib.Plugins/PluginSignatureValidator.cs similarity index 90% rename from LenovoLegionToolkit.Lib/Plugins/PluginSignatureValidator.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginSignatureValidator.cs index ab2869499..b64746bcf 100644 --- a/LenovoLegionToolkit.Lib/Plugins/PluginSignatureValidator.cs +++ b/LenovoLegionToolkit.Lib.Plugins/PluginSignatureValidator.cs @@ -121,8 +121,7 @@ public async Task ValidateAsync(string dllPath) { IsAllowedByPolicy = true }; } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Plugin {dllPath} is not signed: {ex.Message}", ex); + Log.Instance.Warning($"Plugin {dllPath} is not signed: {ex.Message}", ex); return new PluginSignatureResult(PluginSignatureStatus.NotSigned, "Plugin is not signed. Signature required per policy."); @@ -131,20 +130,16 @@ public async Task ValidateAsync(string dllPath) // Validate the certificate var validationResult = await ValidateCertificateAsync(certificate, dllPath); - if (Log.Instance.IsTraceEnabled) - { - if (validationResult.IsValid) - Log.Instance.Trace($"Plugin signature validation passed for {dllPath}. Issuer: {validationResult.Issuer}"); - else - Log.Instance.Trace($"Plugin signature validation failed for {dllPath}. Status: {validationResult.Status}, Error: {validationResult.ErrorMessage}"); - } + if (validationResult.IsValid) + Log.Instance.Trace($"Plugin signature validation passed for {dllPath}. Issuer: {validationResult.Issuer}"); + else + Log.Instance.Warning($"Plugin signature validation failed for {dllPath}. Status: {validationResult.Status}, Error: {validationResult.ErrorMessage}"); return validationResult; } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error validating plugin signature for {dllPath}: {ex.Message}", ex); + Log.Instance.Warning($"Error validating plugin signature for {dllPath}: {ex.Message}", ex); return new PluginSignatureResult(PluginSignatureStatus.ValidationError, $"Validation error: {ex.Message}"); @@ -238,8 +233,7 @@ private async Task ValidateCertificateAsync(X509Certifica } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error validating certificate for {dllPath}: {ex.Message}", ex); + Log.Instance.Warning($"Error validating certificate for {dllPath}: {ex.Message}", ex); return new PluginSignatureResult(PluginSignatureStatus.ValidationError, $"Certificate validation error: {ex.Message}"); diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginState.cs b/LenovoLegionToolkit.Lib.Plugins/PluginState.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginState.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginState.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/PluginUpdateManager.cs b/LenovoLegionToolkit.Lib.Plugins/PluginUpdateManager.cs similarity index 100% rename from LenovoLegionToolkit.Lib/Plugins/PluginUpdateManager.cs rename to LenovoLegionToolkit.Lib.Plugins/PluginUpdateManager.cs diff --git a/LenovoLegionToolkit.Lib/Plugins/VersionChecker.cs b/LenovoLegionToolkit.Lib.Plugins/VersionChecker.cs similarity index 97% rename from LenovoLegionToolkit.Lib/Plugins/VersionChecker.cs rename to LenovoLegionToolkit.Lib.Plugins/VersionChecker.cs index 3fbad9892..6ce209856 100644 --- a/LenovoLegionToolkit.Lib/Plugins/VersionChecker.cs +++ b/LenovoLegionToolkit.Lib.Plugins/VersionChecker.cs @@ -115,7 +115,7 @@ public bool IsCompatible(string minimumHostVersion) } catch (Exception ex) { - Log.Instance.Trace($"Error checking version compatibility: {ex.Message}"); + Log.Instance.Warning($"Error checking version compatibility: {ex.Message}"); return true; } } @@ -141,7 +141,7 @@ public bool IsUpdateAvailable(string currentVersion, string newVersion) } catch (Exception ex) { - Log.Instance.Trace($"Error checking update availability: {ex.Message}"); + Log.Instance.Warning($"Error checking update availability: {ex.Message}"); return false; } } @@ -162,7 +162,7 @@ public int CompareVersions(string version1, string version2) } catch (Exception ex) { - Log.Instance.Trace($"Error comparing versions: {ex.Message}"); + Log.Instance.Warning($"Error comparing versions: {ex.Message}"); return 0; } } diff --git a/LenovoLegionToolkit.Lib/AutoListeners/AbstractAutoListener.cs b/LenovoLegionToolkit.Lib/AutoListeners/AbstractAutoListener.cs index 4bf2dfd71..5a56b541c 100644 --- a/LenovoLegionToolkit.Lib/AutoListeners/AbstractAutoListener.cs +++ b/LenovoLegionToolkit.Lib/AutoListeners/AbstractAutoListener.cs @@ -109,8 +109,7 @@ protected virtual void Dispose(bool disposing) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Error during AbstractAutoListener disposal", ex); + Log.Instance.Error($"Error during AbstractAutoListener disposal", ex); } } _disposed = true; diff --git a/LenovoLegionToolkit.Lib/AutoListeners/GameAutoListener.cs b/LenovoLegionToolkit.Lib/AutoListeners/GameAutoListener.cs index e76818ed6..df57e8663 100644 --- a/LenovoLegionToolkit.Lib/AutoListeners/GameAutoListener.cs +++ b/LenovoLegionToolkit.Lib/AutoListeners/GameAutoListener.cs @@ -121,8 +121,7 @@ private void GameConfigStoreDetectorGamesConfigStoreDetected(object? sender, Gam } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't get game \"{game}\" details: {ex.Message}", ex); + Log.Instance.Warning($"Can't get game \"{game}\" details: {ex.Message}", ex); } } } @@ -161,8 +160,7 @@ private void InstanceStartedEventAutoAutoListener_Changed(object? sender, Instan if (string.IsNullOrEmpty(processPath)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't get path for {e.ProcessName}. [processId={e.ProcessId}]"); + Log.Instance.Warning($"Can't get path for {e.ProcessName}. [processId={e.ProcessId}]"); return; } @@ -181,8 +179,7 @@ private void InstanceStartedEventAutoAutoListener_Changed(object? sender, Instan } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to attach to {e.ProcessName}. [processId={e.ProcessId}]", ex); + Log.Instance.Error($"Failed to attach to {e.ProcessName}. [processId={e.ProcessId}]", ex); } } } diff --git a/LenovoLegionToolkit.Lib/AutoListeners/ProcessAutoListener.cs b/LenovoLegionToolkit.Lib/AutoListeners/ProcessAutoListener.cs index d082b5590..8ab03e224 100644 --- a/LenovoLegionToolkit.Lib/AutoListeners/ProcessAutoListener.cs +++ b/LenovoLegionToolkit.Lib/AutoListeners/ProcessAutoListener.cs @@ -91,8 +91,7 @@ private void InstanceStartedEventAutoListener_Changed(object? sender, InstanceSt } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't get process {e.ProcessName} details. [processId={e.ProcessId}]", ex); + Log.Instance.Warning($"Can't get process {e.ProcessName} details. [processId={e.ProcessId}]", ex); } if (!string.IsNullOrEmpty(processPath) && IgnoredPaths.Any(p => processPath.StartsWith(p, StringComparison.InvariantCultureIgnoreCase))) diff --git a/LenovoLegionToolkit.Lib/AutoListeners/WiFiAutoListener.cs b/LenovoLegionToolkit.Lib/AutoListeners/WiFiAutoListener.cs index 25f5f45fe..bb9663568 100644 --- a/LenovoLegionToolkit.Lib/AutoListeners/WiFiAutoListener.cs +++ b/LenovoLegionToolkit.Lib/AutoListeners/WiFiAutoListener.cs @@ -96,14 +96,12 @@ private unsafe void WlanCallback(L2_NOTIFICATION_DATA* param0, void* param1) var dot11Ssid = notificationData.dot11Ssid; var ssid = Encoding.UTF8.GetString(dot11Ssid.ucSSID.Value, (int)dot11Ssid.uSSIDLength); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"WiFi connected. [ssid={ssid}]"); + Log.Instance.Info($"WiFi connected. [ssid={ssid}]"); RaiseChanged(new ChangedEventArgs(true, ssid)); break; case 0x15: /* Disconnected */ - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"WiFi disconnected."); + Log.Instance.Info($"WiFi disconnected."); RaiseChanged(new ChangedEventArgs(false, null)); break; diff --git a/LenovoLegionToolkit.Lib/Controllers/GPUOverclockController.cs b/LenovoLegionToolkit.Lib/Controllers/GPUOverclockController.cs index 90b6a263b..bfc96252f 100644 --- a/LenovoLegionToolkit.Lib/Controllers/GPUOverclockController.cs +++ b/LenovoLegionToolkit.Lib/Controllers/GPUOverclockController.cs @@ -72,8 +72,7 @@ public async Task IsSupportedAsync() try { NVAPI.Unload(); } catch { /* Ignored */ } } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"NVAPI status: {isSupported}."); + Log.Instance.Info($"NVAPI status: {isSupported}."); if (!isSupported) return isSupported; @@ -97,8 +96,7 @@ public async Task IsSupportedAsync() isSupported = false; } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Supports GPU OC status: {isSupported}"); + Log.Instance.Info($"Supports GPU OC status: {isSupported}"); return isSupported; } @@ -116,8 +114,7 @@ public async Task ApplyStateAsync(bool force = false) { if (await _vantageDisabler.GetStatusAsync().ConfigureAwait(false) == SoftwareStatus.Enabled) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't correctly apply state when Vantage is running."); + Log.Instance.Warning($"Can't correctly apply state when Vantage is running."); Changed?.Invoke(this, EventArgs.Empty); return; @@ -125,8 +122,7 @@ public async Task ApplyStateAsync(bool force = false) if (await _legionZoneDisabler.GetStatusAsync().ConfigureAwait(false) == SoftwareStatus.Enabled) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't correctly apply state when Legion Zone is running."); + Log.Instance.Warning($"Can't correctly apply state when Legion Zone is running."); Changed?.Invoke(this, EventArgs.Empty); return; @@ -179,8 +175,7 @@ public async Task ApplyStateAsync(bool force = false) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to apply overclock: {info}, clearing settings...", ex); + Log.Instance.Error($"Failed to apply overclock: {info}, clearing settings...", ex); _settings.Store.Enabled = false; _settings.Store.Info = GPUOverclockInfo.Zero; diff --git a/LenovoLegionToolkit.Lib/Controllers/RGBKeyboardBacklightController.cs b/LenovoLegionToolkit.Lib/Controllers/RGBKeyboardBacklightController.cs index 99476f69e..31725732d 100644 --- a/LenovoLegionToolkit.Lib/Controllers/RGBKeyboardBacklightController.cs +++ b/LenovoLegionToolkit.Lib/Controllers/RGBKeyboardBacklightController.cs @@ -79,8 +79,7 @@ public async Task SetLightControlOwnerAsync(bool enable, bool restorePreset = fa return; } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't take ownership.", ex); + Log.Instance.Warning($"Can't take RGB keyboard ownership.", ex); throw; } diff --git a/LenovoLegionToolkit.Lib/Features/AbstractDriverFeature.cs b/LenovoLegionToolkit.Lib/Features/AbstractDriverFeature.cs index 2ce48342b..66313e708 100644 --- a/LenovoLegionToolkit.Lib/Features/AbstractDriverFeature.cs +++ b/LenovoLegionToolkit.Lib/Features/AbstractDriverFeature.cs @@ -82,8 +82,7 @@ protected Task SendCodeAsync(SafeFileHandle handle, uint controlCode, uint var error = Marshal.GetLastWin32Error(); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"DeviceIoControl returned 0, last error: {error} [feature={GetType().Name}]"); + Log.Instance.Warning($"DeviceIoControl returned 0, last error: {error} [feature={GetType().Name}]"); throw new InvalidOperationException($"DeviceIoControl returned 0, last error: {error}"); }); @@ -107,8 +106,7 @@ private async Task VerifyStateSetAsync(T state) await Task.Delay(50).ConfigureAwait(false); } - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Verify state {state} set failed. [feature={GetType().Name}]"); + Log.Instance.Warning($"Verify state {state} set failed. [feature={GetType().Name}]"); throw new InvalidOperationException($"Failed to verify {GetType().Name} state was set to {state}."); } diff --git a/LenovoLegionToolkit.Lib/Features/AbstractLenovoLightingFeature.cs b/LenovoLegionToolkit.Lib/Features/AbstractLenovoLightingFeature.cs index 76467c494..99a982ac1 100644 --- a/LenovoLegionToolkit.Lib/Features/AbstractLenovoLightingFeature.cs +++ b/LenovoLegionToolkit.Lib/Features/AbstractLenovoLightingFeature.cs @@ -42,8 +42,7 @@ public virtual async Task IsSupportedAsync() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to check support [feature={GetType().Name}]", ex); + Log.Instance.Warning($"Failed to check support [feature={GetType().Name}]", ex); return false; } diff --git a/LenovoLegionToolkit.Lib/Features/AbstractUEFIFeature.cs b/LenovoLegionToolkit.Lib/Features/AbstractUEFIFeature.cs index 192dbb5ca..7bc7d96b6 100644 --- a/LenovoLegionToolkit.Lib/Features/AbstractUEFIFeature.cs +++ b/LenovoLegionToolkit.Lib/Features/AbstractUEFIFeature.cs @@ -39,8 +39,7 @@ protected unsafe Task ReadFromUefiAsync() where TS : struct => Task.Run( { if (!TokenManipulator.AddPrivileges(TokenManipulator.SE_SYSTEM_ENVIRONMENT_PRIVILEGE)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Cannot set UEFI privileges [feature={GetType().Name}]"); + Log.Instance.Warning($"Cannot set UEFI privileges [feature={GetType().Name}]"); throw new InvalidOperationException("Cannot set privileges UEFI"); } @@ -57,8 +56,7 @@ protected unsafe Task ReadFromUefiAsync() where TS : struct => Task.Run( } else { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Cannot read variable {scopeName} from UEFI [feature={GetType().Name}]"); + Log.Instance.Warning($"Cannot read variable {scopeName} from UEFI [feature={GetType().Name}]"); throw new InvalidOperationException($"Cannot read variable {scopeName} from UEFI"); } @@ -78,8 +76,7 @@ protected unsafe Task WriteToUefiAsync(TS structure) where TS : struct => Ta { if (!TokenManipulator.AddPrivileges(TokenManipulator.SE_SYSTEM_ENVIRONMENT_PRIVILEGE)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Cannot set UEFI privileges [feature={GetType().Name}]"); + Log.Instance.Warning($"Cannot set UEFI privileges [feature={GetType().Name}]"); throw new InvalidOperationException("Cannot set UEFI privileges"); } @@ -88,8 +85,7 @@ protected unsafe Task WriteToUefiAsync(TS structure) where TS : struct => Ta var ptrSize = (uint)Marshal.SizeOf(); if (!PInvoke.SetFirmwareEnvironmentVariableEx(scopeName, guid, ptr.ToPointer(), ptrSize, scopeAttribute)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Cannot write variable {scopeName} to UEFI [feature={GetType().Name}]"); + Log.Instance.Warning($"Cannot write variable {scopeName} to UEFI [feature={GetType().Name}]"); throw new InvalidOperationException($"Cannot write variable {scopeName} to UEFI"); } diff --git a/LenovoLegionToolkit.Lib/Features/HDRFeature.cs b/LenovoLegionToolkit.Lib/Features/HDRFeature.cs index 65f6522a4..f1c8b3383 100644 --- a/LenovoLegionToolkit.Lib/Features/HDRFeature.cs +++ b/LenovoLegionToolkit.Lib/Features/HDRFeature.cs @@ -33,8 +33,7 @@ public Task IsSupportedAsync() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to check HDR support", ex); + Log.Instance.Warning($"Failed to check HDR support", ex); return Task.FromResult(false); } diff --git a/LenovoLegionToolkit.Lib/Features/Hybrid/HybridModeFeature.cs b/LenovoLegionToolkit.Lib/Features/Hybrid/HybridModeFeature.cs index 701e3c0b1..b3b17dbbb 100644 --- a/LenovoLegionToolkit.Lib/Features/Hybrid/HybridModeFeature.cs +++ b/LenovoLegionToolkit.Lib/Features/Hybrid/HybridModeFeature.cs @@ -152,8 +152,7 @@ public async Task EnsureDGPUEjectedIfNeededAsync() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to ensure dGPU is ejected", ex); + Log.Instance.Error($"Failed to ensure dGPU is ejected", ex); } }); } diff --git a/LenovoLegionToolkit.Lib/Features/Hybrid/Notify/AbstractDGPUNotify.cs b/LenovoLegionToolkit.Lib/Features/Hybrid/Notify/AbstractDGPUNotify.cs index 1ed242b80..0d14f0310 100644 --- a/LenovoLegionToolkit.Lib/Features/Hybrid/Notify/AbstractDGPUNotify.cs +++ b/LenovoLegionToolkit.Lib/Features/Hybrid/Notify/AbstractDGPUNotify.cs @@ -34,8 +34,7 @@ public async Task IsDGPUAvailableAsync() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to notify.", ex); + Log.Instance.Warning($"Failed to check dGPU availability.", ex); return false; } } @@ -62,8 +61,7 @@ public async Task NotifyAsync(bool publish = true) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to notify.", ex); + Log.Instance.Warning($"Failed to notify dGPU status.", ex); } } diff --git a/LenovoLegionToolkit.Lib/IoCModule.cs b/LenovoLegionToolkit.Lib/IoCModule.cs index 5070d6500..ba0db5b9e 100644 --- a/LenovoLegionToolkit.Lib/IoCModule.cs +++ b/LenovoLegionToolkit.Lib/IoCModule.cs @@ -16,7 +16,6 @@ using LenovoLegionToolkit.Lib.Listeners; using LenovoLegionToolkit.Lib.Optimization; using LenovoLegionToolkit.Lib.PackageDownloader; -using LenovoLegionToolkit.Lib.Plugins; using LenovoLegionToolkit.Lib.Services; using LenovoLegionToolkit.Lib.Settings; using LenovoLegionToolkit.Lib.SoftwareDisabler; @@ -154,25 +153,5 @@ protected override void Load(ContainerBuilder builder) builder.Register(); builder.Register(); builder.Register(); - - // 注册插件签名验证器 - builder.Register(_ => new PluginSignatureValidator(PluginSignatureSettings.CreateForCurrentProcess())) - .As() - .SingleInstance(); - - // 注册插件系统组件 - builder.Register().As().SingleInstance(); - builder.Register().As().SingleInstance(); - builder.Register().As().SingleInstance(); - - // 注册插件管理器 - builder.Register().As().SingleInstance(); - - // 注册插件仓库服务 - builder.Register().AsSelf().SingleInstance(); - - // System Optimization and Tools are now default interfaces, not plugins - // They are registered directly in MainWindow.xaml as NavigationItems - // No need to register them as plugins in IoC container } } diff --git a/LenovoLegionToolkit.Lib/LenovoLegionToolkit.Lib.csproj b/LenovoLegionToolkit.Lib/LenovoLegionToolkit.Lib.csproj index 9a185bdb7..c8201397d 100644 --- a/LenovoLegionToolkit.Lib/LenovoLegionToolkit.Lib.csproj +++ b/LenovoLegionToolkit.Lib/LenovoLegionToolkit.Lib.csproj @@ -26,6 +26,9 @@ + + + diff --git a/LenovoLegionToolkit.Lib/Listeners/AbstractEventLogListener.cs b/LenovoLegionToolkit.Lib/Listeners/AbstractEventLogListener.cs index 54c94c45e..6c0b7412d 100644 --- a/LenovoLegionToolkit.Lib/Listeners/AbstractEventLogListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/AbstractEventLogListener.cs @@ -43,8 +43,7 @@ private async Task Watcher_EventRecordWrittenAsync(object? sender, EventRecordWr } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to handle event. [listener={GetType().Name}]", ex); + Log.Instance.Error($"Failed to handle event. [listener={GetType().Name}]", ex); } } diff --git a/LenovoLegionToolkit.Lib/Listeners/AbstractWMIListener.cs b/LenovoLegionToolkit.Lib/Listeners/AbstractWMIListener.cs index d3b4b4e82..2b9c57dc4 100644 --- a/LenovoLegionToolkit.Lib/Listeners/AbstractWMIListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/AbstractWMIListener.cs @@ -49,13 +49,11 @@ public Task StartAsync() { _isUnsupported = true; - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"WMI class or namespace not available; listener disabled. [listener={GetType().Name}, error={ex.ErrorCode}]"); + Log.Instance.Warning($"WMI class or namespace not available; listener disabled. [listener={GetType().Name}, error={ex.ErrorCode}]"); } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Couldn't start listener. [listener={GetType().Name}]", ex); + Log.Instance.Error($"Couldn't start listener. [listener={GetType().Name}]", ex); } return Task.CompletedTask; @@ -73,8 +71,7 @@ public Task StopAsync() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Couldn't stop listener. [listener={GetType().Name}]", ex); + Log.Instance.Error($"Couldn't stop listener. [listener={GetType().Name}]", ex); } return Task.CompletedTask; @@ -104,8 +101,7 @@ private async Task HandlerAsync(TRawValue properties) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to handle event. [listener={GetType().Name}]", ex); + Log.Instance.Error($"Failed to handle event. [listener={GetType().Name}]", ex); } finally { diff --git a/LenovoLegionToolkit.Lib/Listeners/DisplayBrightnessListener.cs b/LenovoLegionToolkit.Lib/Listeners/DisplayBrightnessListener.cs index c683db67a..12393e2b7 100644 --- a/LenovoLegionToolkit.Lib/Listeners/DisplayBrightnessListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/DisplayBrightnessListener.cs @@ -57,8 +57,7 @@ private void SetBrightnessForAllPowerPlans(Brightness brightness) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to set brightness to {brightness.Value}.", ex); + Log.Instance.Error($"Failed to set brightness to {brightness.Value}.", ex); } } } diff --git a/LenovoLegionToolkit.Lib/Listeners/DisplayConfigurationListener.cs b/LenovoLegionToolkit.Lib/Listeners/DisplayConfigurationListener.cs index ec41d0bc3..c3e2d5c9d 100644 --- a/LenovoLegionToolkit.Lib/Listeners/DisplayConfigurationListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/DisplayConfigurationListener.cs @@ -67,8 +67,7 @@ private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to get HDR status. Assuming unavailable.", ex); + Log.Instance.Warning($"Failed to get HDR status. Assuming unavailable.", ex); return null; } } diff --git a/LenovoLegionToolkit.Lib/Listeners/DriverKeyListener.cs b/LenovoLegionToolkit.Lib/Listeners/DriverKeyListener.cs index 1a7da3a9d..d4071e6f8 100644 --- a/LenovoLegionToolkit.Lib/Listeners/DriverKeyListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/DriverKeyListener.cs @@ -104,8 +104,7 @@ private async Task HandlerAsync(CancellationToken token) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Unknown error.", ex); + Log.Instance.Error($"Unknown error.", ex); } } @@ -158,8 +157,7 @@ private async Task OnChangedAsync(DriverKey value) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Couldn't handle key press. [value={value}]", ex); + Log.Instance.Error($"Couldn't handle key press. [value={value}]", ex); } } diff --git a/LenovoLegionToolkit.Lib/Listeners/NativeWindowsMessageListener.cs b/LenovoLegionToolkit.Lib/Listeners/NativeWindowsMessageListener.cs index 417347278..7ffcb9af5 100644 --- a/LenovoLegionToolkit.Lib/Listeners/NativeWindowsMessageListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/NativeWindowsMessageListener.cs @@ -94,8 +94,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to unhook keyboard hook in NativeWindowsMessageListener: {ex.Message}", ex); + Log.Instance.Warning($"Failed to unhook keyboard hook in NativeWindowsMessageListener: {ex.Message}", ex); } try @@ -104,8 +103,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to unregister device notification: {ex.Message}", ex); + Log.Instance.Warning($"Failed to unregister device notification: {ex.Message}", ex); } try @@ -114,8 +112,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to unregister console display state notification: {ex.Message}", ex); + Log.Instance.Warning($"Failed to unregister console display state notification: {ex.Message}", ex); } try @@ -124,8 +121,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to unregister lid switch state notification: {ex.Message}", ex); + Log.Instance.Warning($"Failed to unregister lid switch state notification: {ex.Message}", ex); } try @@ -134,8 +130,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to unregister power saving state notification: {ex.Message}", ex); + Log.Instance.Warning($"Failed to unregister power saving state notification: {ex.Message}", ex); } _kbHook = default; @@ -150,8 +145,7 @@ public Task StopAsync() => _mainThreadDispatcher.DispatchAsync(() => } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to release handle in NativeWindowsMessageListener: {ex.Message}", ex); + Log.Instance.Warning($"Failed to release handle in NativeWindowsMessageListener: {ex.Message}", ex); } return Task.CompletedTask; @@ -173,16 +167,14 @@ protected override unsafe void WndProc(ref Message m) { case PInvoke.DBT_DEVICEARRIVAL: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Device Arrival [name={name}]"); + Log.Instance.Info($"Event received: Device Arrival [name={name}]"); OnDeviceConnected(name); break; } case PInvoke.DBT_DEVICEREMOVECOMPLETE: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Device Removal Complete [name={name}]"); + Log.Instance.Info($"Event received: Device Removal Complete [name={name}]"); OnDeviceDisconnected(name); break; @@ -191,8 +183,7 @@ protected override unsafe void WndProc(ref Message m) if (devBroadcastDeviceInterface.dbcc_classguid == PInvoke.GUID_DISPLAY_DEVICE_ARRIVAL) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Display Device Arrival"); + Log.Instance.Info($"Event received: Display Device Arrival"); OnDisplayDeviceArrival(); } @@ -206,16 +197,14 @@ protected override unsafe void WndProc(ref Message m) { case PInvoke.DBT_DEVICEARRIVAL: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Monitor Connected"); + Log.Instance.Info($"Event received: Monitor Connected"); OnMonitorConnected(isExternal); break; } case PInvoke.DBT_DEVICEREMOVECOMPLETE: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Monitor Disconnected"); + Log.Instance.Info($"Event received: Monitor Disconnected"); OnMonitorDisconnected(isExternal); break; @@ -236,16 +225,14 @@ protected override unsafe void WndProc(ref Message m) { case PInvokeExtensions.CONSOLE_DISPLAY_STATE.On: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Monitor On"); + Log.Instance.Info($"Event received: Monitor On"); OnMonitorOn(); break; } case PInvokeExtensions.CONSOLE_DISPLAY_STATE.Off: { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Monitor Off"); + Log.Instance.Info($"Event received: Monitor Off"); OnMonitorOff(); break; @@ -258,15 +245,13 @@ protected override unsafe void WndProc(ref Message m) var isOpened = str.Data[0] != 0; if (isOpened) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Lid Opened"); + Log.Instance.Info($"Event received: Lid Opened"); OnLidOpened(); } else { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Lid Closed"); + Log.Instance.Info($"Event received: Lid Closed"); OnLidClosed(); } @@ -274,8 +259,7 @@ protected override unsafe void WndProc(ref Message m) if (str.PowerSetting == PInvoke.GUID_POWER_SAVING_STATUS && str.Data[0] == 0) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: Battery Saver enabled"); + Log.Instance.Info($"Event received: Battery Saver enabled"); OnBatterySaverEnabled(); } @@ -296,8 +280,7 @@ private async Task WaitForInit() if (completed == delayTask) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Delay expired, state might be inconsistent! [IsMonitorOn={IsMonitorOn}, IsLidOpen={IsLidOpen}]"); + Log.Instance.Warning($"Delay expired, state might be inconsistent! [IsMonitorOn={IsMonitorOn}, IsLidOpen={IsLidOpen}]"); } } diff --git a/LenovoLegionToolkit.Lib/Listeners/PowerStateListener.cs b/LenovoLegionToolkit.Lib/Listeners/PowerStateListener.cs index 8cb9c118a..befc84caa 100644 --- a/LenovoLegionToolkit.Lib/Listeners/PowerStateListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/PowerStateListener.cs @@ -105,8 +105,7 @@ private async Task SystemEvents_PowerModeChangedAsync(object sender, PowerModeCh { try { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Event received: {e.Mode}"); + Log.Instance.Info($"Event received: {e.Mode}"); var powerMode = e.Mode switch { diff --git a/LenovoLegionToolkit.Lib/Listeners/RGBKeyboardBacklightListener.cs b/LenovoLegionToolkit.Lib/Listeners/RGBKeyboardBacklightListener.cs index 946a32d83..4a2cb6270 100644 --- a/LenovoLegionToolkit.Lib/Listeners/RGBKeyboardBacklightListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/RGBKeyboardBacklightListener.cs @@ -47,8 +47,7 @@ protected override async Task OnChangedAsync(RGBKeyboardBacklightChanged value) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to set next keyboard backlight preset.", ex); + Log.Instance.Error($"Failed to set next keyboard backlight preset.", ex); } } } diff --git a/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs b/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs index d0ccaf6e2..5ce09e332 100644 --- a/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/SessionLockUnlockListener.cs @@ -46,13 +46,11 @@ private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) if (flags == PInvoke.WTS_SESSIONSTATE_UNKNOWN) { IsLocked = null; - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace("Unknown error occurred when getting active console session flags."); + Log.Instance.Warning("Unknown error occurred when getting active console session flags."); return; } var locked = (flags == PInvoke.WTS_SESSIONSTATE_LOCK); - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Session lock unlock state switched. [locked={locked}]"); + Log.Instance.Info($"Session lock unlock state switched. [locked={locked}]"); IsLocked = locked; Changed?.Invoke(this, new(locked)); } diff --git a/LenovoLegionToolkit.Lib/Listeners/SpecialKeyListener.cs b/LenovoLegionToolkit.Lib/Listeners/SpecialKeyListener.cs index e614bad9b..819cef2df 100644 --- a/LenovoLegionToolkit.Lib/Listeners/SpecialKeyListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/SpecialKeyListener.cs @@ -112,8 +112,7 @@ protected override async Task OnChangedAsync(SpecialKey value) } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to handle key. [key={value}, value={(int)value}]", ex); + Log.Instance.Error($"Failed to handle key. [key={value}, value={(int)value}]", ex); } } @@ -169,8 +168,7 @@ private Task ToggleRefreshRateAsync() => _refreshRateDispatcher.DispatchAsync(as if (filtered.Length < 2) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Can't switch refresh rate after Fn+R when there is less than 2 available."); + Log.Instance.Warning($"Can't switch refresh rate after Fn+R when there is less than 2 available."); return; } @@ -196,8 +194,7 @@ private Task ToggleRefreshRateAsync() => _refreshRateDispatcher.DispatchAsync(as } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to switch refresh rate after Fn+R.", ex); + Log.Instance.Error($"Failed to switch refresh rate after Fn+R.", ex); } }); diff --git a/LenovoLegionToolkit.Lib/Listeners/SystemThemeListener.cs b/LenovoLegionToolkit.Lib/Listeners/SystemThemeListener.cs index c7f213ca2..048194a6f 100644 --- a/LenovoLegionToolkit.Lib/Listeners/SystemThemeListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/SystemThemeListener.cs @@ -50,8 +50,7 @@ private void OnColorizationColorChanged() } catch (Exception ex) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to notify on accent color change.", ex); + Log.Instance.Error($"Failed to notify on accent color change.", ex); } } diff --git a/LenovoLegionToolkit.Lib/Listeners/ThermalModeListener.cs b/LenovoLegionToolkit.Lib/Listeners/ThermalModeListener.cs index a531cebcb..b1b3819fa 100644 --- a/LenovoLegionToolkit.Lib/Listeners/ThermalModeListener.cs +++ b/LenovoLegionToolkit.Lib/Listeners/ThermalModeListener.cs @@ -24,8 +24,7 @@ protected override ThermalModeState GetValue(int value) if (!Enum.IsDefined(state)) { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Unknown value received: {value}"); + Log.Instance.Warning($"Unknown value received: {value}"); state = ThermalModeState.Unknown; } diff --git a/LenovoLegionToolkit.Lib/Optimization/IOptimizationCategoryExtender.cs b/LenovoLegionToolkit.Lib/Optimization/IOptimizationCategoryExtender.cs new file mode 100644 index 000000000..161a49a51 --- /dev/null +++ b/LenovoLegionToolkit.Lib/Optimization/IOptimizationCategoryExtender.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace LenovoLegionToolkit.Lib.Optimization; + +/// +/// Extension point for providing additional optimization categories from plugins. +/// Implemented by LenovoLegionToolkit.Lib.Plugins to avoid circular project references. +/// +public interface IOptimizationCategoryExtender +{ + IReadOnlyList GetPluginCategories(); +} diff --git a/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationCategoryProvider.cs b/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationCategoryProvider.cs index 84f4557be..5fb11da3f 100644 --- a/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationCategoryProvider.cs +++ b/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationCategoryProvider.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using LenovoLegionToolkit.Lib.Plugins; namespace LenovoLegionToolkit.Lib.Optimization; diff --git a/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationService.cs b/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationService.cs index c754cfd27..8ccae2657 100644 --- a/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationService.cs +++ b/LenovoLegionToolkit.Lib/Optimization/WindowsOptimizationService.cs @@ -7,7 +7,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using LenovoLegionToolkit.Lib.Plugins; using LenovoLegionToolkit.Lib.System; using LenovoLegionToolkit.Lib.Utils; using LenovoLegionToolkit.Lib.Settings; @@ -88,43 +87,11 @@ private IReadOnlyDictionary GetActi public IReadOnlyList GetCategories() { var list = new List(_categoryProvider.BuildCategories()); - + try { - var pluginManager = IoCContainer.Resolve(); - var installedPlugins = pluginManager.GetRegisteredPlugins() - .Where(p => pluginManager.IsInstalled(p.Id)); - - foreach (var plugin in installedPlugins) - { - try - { - WindowsOptimizationCategoryDefinition? category = null; - - if (plugin is IOptimizationCategoryProvider provider) - { - category = provider.GetOptimizationCategory(); - } - else if (plugin is PluginBase pluginBase) - { - category = pluginBase.GetOptimizationCategory(); - } - - if (category != null) - { - if (string.IsNullOrEmpty(category.PluginId)) - { - category = category with { PluginId = plugin.Id }; - } - list.Add(category); - } - } - catch (Exception ex) - { - if (Log.Instance.IsTraceEnabled) - Log.Instance.Trace($"Failed to get optimization category from plugin {plugin.Id}: {ex.Message}", ex); - } - } + var extender = IoCContainer.Resolve(); + list.AddRange(extender.GetPluginCategories()); } catch (Exception ex) { diff --git a/LenovoLegionToolkit.Lib/Resources/Resource.pt-br.resx b/LenovoLegionToolkit.Lib/Resources/Resource.pt-br.resx new file mode 100644 index 000000000..2aa845957 --- /dev/null +++ b/LenovoLegionToolkit.Lib/Resources/Resource.pt-br.resx @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Personalizado + + + Sistema + + + Desligado + + + Sempre ligado + + + Ligado, quando suspenso + + + Desativado + + + Ativado + + + Ativado com atraso + + + Conservação + + + normal + + + Carregamento rápido + + + Desligado + Informs the user that Flip to Start is or will be turned off. + + + Ligado + Informs the user that Flip to Start is or will be turned on. + + + Desligado + Informs the user that Fn Lock is or will be turned off. + + + Ligado + Informs the user that Fn Lock is or will be turned on. + + + Desligada + + + Ligada + + + Desligado + Informs the user that HDR is or will be turned off. + + + Ligado + Informs the user that HDR is or will be turned on. + + + GPU dedicada + + + Híbrido + + + Híbrido-automático + + + Híbrido-iGPU + + + Desligado + Informs the user that the microphone is or will be turned off. + + + Ligado + Informs the user that the microphone is or will be turned on. + + + Centro inferior + + + Canto inferior esquerdo + + + Canto inferior direito + + + Centro + + + Centro à esquerda + + + Centro à direita + + + Centro superior + + + Centro inferior + + + Canto superior direito + + + Desligada + Informs the user that One Level White Keyboard Backlight is or will be turned off. + + + Ligada + Informs the user that One Level White Keyboard Backlight is or will be turned on. + + + Desligado + Informs the user that Overdrive is or will be turned off. + + + Ligado + Informs the user that Overdrive is or will be turned on. + + + Equilibrado + + + Personalizado + + + Desempenho + + + Silencioso + + + Rápido + + + Rapidíssimo + + + Lento + + + Lentíssimo + + + Alta + + + Baixo + + + Respiração + + + Suave + + + Estático + + + Onda para a esquerda + + + Onda para a direita + + + Desligado + + + Predefinição 1 + + + Predefinição 3 + + + Predefinição 2 + + + Alto + + + Baixo + + + Médio + + + Desligado + + + Baixo para Cima + + + Sentido dos ponteiros do relógio + + + Sentido oposto aos ponteiros do relógio + + + Esquerda para Direita + + + Direita para Esquerda + + + Cima para Baixo + + + Sempre + + + Renderização de Áudio + + + Onda de Áudio + + + Sincronização Aurora + + + Mudança de cor + + + Pulsação de cor + + + Onda de cor + + + Chuva + + + Parafuso Arco-íris + + + Ondulação arco-íris + + + Ondulação + + + Suave + + + Tipo + + + Lento + + + Médio + + + Rápido + + + Escuro + + + Claro + + + Sistema + + + Desligado + Informs the user that Touchpad Lock is or will be turned off. + + + Ligado + Informs the user that Touchpad Lock is or will be turned on. + + + Alta + + + Baixa + + + Desligada + + + Desligada + Informs the user that WinLock is or will be turned on. + + + Ligada + Informs the user that WinLock is or will be turned on. + + + Desligada + + + Ligada + + + Ligado + + + Desligada + + + Desligado + + + Carregador + + + Carregador USB PD + + + Carregador e USB PD + + + Longa + + + normal + + + Curta + + + Mudança + + + CRTL + + + investimentos alternativos + + + Ligado + + + Desligado + + + Silenciar + + + Ativar som + + + Desativado + + + Modo de Energia do Windows + + + Esquemas de Energia do Windows + + + Predefinição 4 + + + Diariamente + + + A cada hora + + + Mensalmente + + + A cada 3 horas + + + A cada 12 horas + + + Semanalmente + +janelasMarcos. \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib/Resources/Resource.zh-hant.resx b/LenovoLegionToolkit.Lib/Resources/Resource.zh-hant.resx new file mode 100644 index 000000000..31074d178 --- /dev/null +++ b/LenovoLegionToolkit.Lib/Resources/Resource.zh-hant.resx @@ -0,0 +1,447 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 自定義 + + + 跟隨系統 + + + 關閉 + + + 保持開啓 + + + 僅在睡眠時 + + + 關閉 + + + 開啓 + + + 延遲開啓 + + + 養護 + + + 常規 + + + 快充 + + + + Informs the user that Flip to Start is or will be turned off. + + + + Informs the user that Flip to Start is or will be turned on. + + + + Informs the user that Fn Lock is or will be turned off. + + + + Informs the user that Fn Lock is or will be turned on. + + + 關閉 + + + 啓用 + + + + Informs the user that HDR is or will be turned off. + + + + Informs the user that HDR is or will be turned on. + + + 獨顯直連模式 + + + 混合模式 + + + 混合自動模式 + + + 混合核顯模式 + + + + Informs the user that the microphone is or will be turned off. + + + + Informs the user that the microphone is or will be turned on. + + + 底部居中 + + + 左下角 + + + 右下角 + + + 居中 + + + 中間左側 + + + 中間右側 + + + 頂部居中 + + + 左上角 + + + 右上角 + + + + Informs the user that One Level White Keyboard Backlight is or will be turned off. + + + + Informs the user that One Level White Keyboard Backlight is or will be turned on. + + + + Informs the user that Overdrive is or will be turned off. + + + + Informs the user that Overdrive is or will be turned on. + + + 均衡模式 + + + 自定義模式 + + + 野獸模式 + + + 安靜模式 + + + + + + 最快 + + + + + + 最慢 + + + 高亮度 + + + 低亮度 + + + 呼吸 + + + 平滑 + + + 靜態 + + + 從左向右 + + + 從右向左 + + + 關閉 + + + 預設 1 + + + 預設 3 + + + 預設 2 + + + + + + + + + + + + + + + 從下到上 + + + 順時針 + + + 逆時針 + + + 從左到右 + + + 從右到左 + + + 從上到下 + + + 照明 + + + 音律跳動 + + + 音律漣漪 + + + 屏光同步 + + + 光譜循環 + + + 色彩脈衝 + + + 波浪 + + + 雨滴 + + + 螺旋彩虹 + + + 彩虹波 + + + 漣漪 + + + 晴朗 + + + 鍵入 + + + + + + + + + + + + 深色模式 + + + 淺色模式 + + + 跟隨系統 + + + 窗戶 + + + macOS + + + + Informs the user that Touchpad Lock is or will be turned off. + + + + Informs the user that Touchpad Lock is or will be turned on. + + + 高亮度 + + + 低亮度 + + + 關閉 + + + + Informs the user that WinLock is or will be turned on. + + + + Informs the user that WinLock is or will be turned on. + + + + + + + + + + + + + + + 關閉 + + + 方形電源口 + + + USB PD 充電口 + + + 方形電源口與 PD 充電口 + + + 較長 + + + 默認 + + + 較短 + + + 轉變 + + + Ctrl + + + 高音 + + + + + + + + + 靜音 + + + 解除靜音 + + + 禁用 + + + Windows 電源模式 + + + Windows 電源計劃 + + + 預設 4 + + + 每日 + + + 每小時 + + + 每月 + + + 每三小時 + + + 每十二小時 + + + 每週 + + \ No newline at end of file diff --git a/LenovoLegionToolkit.Lib/Utils/Log.cs b/LenovoLegionToolkit.Lib/Utils/Log.cs index 44257376f..c0b498173 100644 --- a/LenovoLegionToolkit.Lib/Utils/Log.cs +++ b/LenovoLegionToolkit.Lib/Utils/Log.cs @@ -7,6 +7,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Serilog; +using Serilog.Core; +using Serilog.Events; namespace LenovoLegionToolkit.Lib.Utils; @@ -19,106 +22,61 @@ public enum LogLevel Trace } -public class Log +public class Log : IDisposable { private static readonly Lazy _instance = new(() => new Log(), LazyThreadSafetyMode.ExecutionAndPublication); public static Log Instance => _instance.Value; - private readonly object _lock = new(); - private readonly object _fileLock = new(); + private readonly Logger _logger; + private readonly LoggingLevelSwitch _levelSwitch; private readonly string _folderPath; - private volatile string _currentLogPath = string.Empty; - private readonly Queue _logQueue = new(); - private readonly int _maxLogSizeBytes = 50 * 1024 * 1024; // 50MB - private readonly int _maxQueuedEntries = 1000; // Increased to reduce flush frequency - private readonly Task _logTask; - private readonly object _queueLock = new(); - private volatile bool _isRunning = true; + private readonly object _emergencyLock = new(); + private bool _disposed; - public bool IsTraceEnabled { get; set; } - public LogLevel CurrentLogLevel { get; set; } = LogLevel.Info; - - public string LogPath => _currentLogPath ?? string.Empty; - - private Log() + public bool IsTraceEnabled { - _folderPath = Path.Combine(Folders.AppData, "log"); - Directory.CreateDirectory(_folderPath); - _currentLogPath = CreateNewLogFile(); - // Start background task that writes log entries asynchronously - _logTask = Task.Run(ProcessLogQueue); + get => _levelSwitch.MinimumLevel <= LogEventLevel.Verbose; + set + { + if (value) + _levelSwitch.MinimumLevel = LogEventLevel.Verbose; + } } - private string CreateNewLogFile() + public LogLevel CurrentLogLevel { - lock (_lock) - { - var timestamp = DateTime.UtcNow.ToString("yyyy_MM_dd_HH_mm_ss_fff"); - var logPath = Path.Combine(_folderPath, $"log_{timestamp}.txt"); - // Remove older log files, keeping only the ten most recent entries - CleanupOldLogFiles(); - return logPath; - } + get => MapLevelFromSerilog(_levelSwitch.MinimumLevel); + set => _levelSwitch.MinimumLevel = MapLevelToSerilog(value); } - private void CleanupOldLogFiles() + public string LogPath => _folderPath; + + private Log() { - try - { - var logFiles = Directory.GetFiles(_folderPath, "log_*.txt") - .OrderByDescending(File.GetLastWriteTime) - .ToList(); - - for (int i = 10; i < logFiles.Count; i++) - { - try - { - File.Delete(logFiles[i]); - } - catch (Exception ex) - { - // Log cleanup failures but continue processing - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#76:CleanupOldLogFiles] [Trace] Failed to delete log file {logFiles[i]}: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"cleanup_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } - } - } - catch (Exception ex) - { - // Log cleanup failures but continue processing - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#80:CleanupOldLogFiles] [Trace] Failed during log cleanup: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"cleanup_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } + _folderPath = Path.Combine(Folders.AppData, "logs"); + Directory.CreateDirectory(_folderPath); + + _levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); + + _logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(_levelSwitch) + .Enrich.WithProperty("Application", "LenovoLegionToolkit") + .WriteTo.Async(wt => wt.File( + new Serilog.Formatting.Json.JsonFormatter(), + Path.Combine(_folderPath, "log-.json"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 10, + fileSizeLimitBytes: 50 * 1024 * 1024 + )) + .CreateLogger(); } public void ErrorReport(string header, Exception ex) { var errorReportPath = Path.Combine(_folderPath, $"error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"); File.AppendAllLines(errorReportPath, [header, Serialize(ex)]); + + _logger.Error(ex, "{Header}", header); } public void Error(FormattableString message, @@ -127,19 +85,19 @@ public void Error(FormattableString message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - LogInternal(LogLevel.Error, message, ex, file, lineNumber, caller); + var sourceContext = FormatSourceContext(file, lineNumber, caller); + var properties = BuildProperties(sourceContext); + _logger.Write(LogEventLevel.Error, ex, message.ToString(), properties); } - // Convenience overloads that accept plain strings. These are helpful for callers - // that pass string literals or non-interpolated strings so they don't need to - // create FormattableString instances explicitly. public void Error(string message, Exception? ex = null, [CallerFilePath] string? file = null, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - LogInternal(LogLevel.Error, PlainMessage(message), ex, file, lineNumber, caller); + var sourceContext = FormatSourceContext(file, lineNumber, caller); + _logger.Write(LogEventLevel.Error, ex, "{Message} [@{SourceContext}]", message, sourceContext); } public void Warning(FormattableString message, @@ -148,8 +106,12 @@ public void Warning(FormattableString message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Warning) - LogInternal(LogLevel.Warning, message, ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Warning) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + var properties = BuildProperties(sourceContext); + _logger.Write(LogEventLevel.Warning, ex, message.ToString(), properties); } public void Warning(string message, @@ -158,8 +120,11 @@ public void Warning(string message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Warning) - LogInternal(LogLevel.Warning, PlainMessage(message), ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Warning) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + _logger.Write(LogEventLevel.Warning, ex, "{Message} [@{SourceContext}]", message, sourceContext); } public void Info(FormattableString message, @@ -168,8 +133,12 @@ public void Info(FormattableString message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Info) - LogInternal(LogLevel.Info, message, ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Info) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + var properties = BuildProperties(sourceContext); + _logger.Write(LogEventLevel.Information, ex, message.ToString(), properties); } public void Info(string message, @@ -178,8 +147,11 @@ public void Info(string message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Info) - LogInternal(LogLevel.Info, PlainMessage(message), ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Info) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + _logger.Write(LogEventLevel.Information, ex, "{Message} [@{SourceContext}]", message, sourceContext); } public void Debug(FormattableString message, @@ -188,8 +160,12 @@ public void Debug(FormattableString message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Debug) - LogInternal(LogLevel.Debug, message, ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Debug) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + var properties = BuildProperties(sourceContext); + _logger.Write(LogEventLevel.Debug, ex, message.ToString(), properties); } public void Debug(string message, @@ -198,8 +174,11 @@ public void Debug(string message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (CurrentLogLevel >= LogLevel.Debug) - LogInternal(LogLevel.Debug, PlainMessage(message), ex, file, lineNumber, caller); + if (CurrentLogLevel < LogLevel.Debug) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + _logger.Write(LogEventLevel.Debug, ex, "{Message} [@{SourceContext}]", message, sourceContext); } public void Trace(FormattableString message, @@ -208,8 +187,12 @@ public void Trace(FormattableString message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (IsTraceEnabled || CurrentLogLevel >= LogLevel.Trace) - LogInternal(LogLevel.Trace, message, ex, file, lineNumber, caller); + if (!IsTraceEnabled && CurrentLogLevel < LogLevel.Trace) + return; + + var sourceContext = FormatSourceContext(file, lineNumber, caller); + var properties = BuildProperties(sourceContext); + _logger.Write(LogEventLevel.Verbose, ex, message.ToString(), properties); } public void Trace(string message, @@ -218,355 +201,68 @@ public void Trace(string message, [CallerLineNumber] int lineNumber = -1, [CallerMemberName] string? caller = null) { - if (IsTraceEnabled || CurrentLogLevel >= LogLevel.Trace) - LogInternal(LogLevel.Trace, PlainMessage(message), ex, file, lineNumber, caller); - } - - private static FormattableString PlainMessage(string message) => - global::System.Runtime.CompilerServices.FormattableStringFactory.Create("{0}", message); + if (!IsTraceEnabled && CurrentLogLevel < LogLevel.Trace) + return; - private void LogInternal(LogLevel level, - FormattableString message, - Exception? ex, - string? file, - int lineNumber, - string? caller) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var fileName = Path.GetFileName(file); - - var logLine = $"[{timestamp}] [{threadId}] [{fileName}#{lineNumber}:{caller}] [{level}] {message}"; - var logLines = new List { logLine }; - - if (ex is not null) - logLines.Add(Serialize(ex)); - -#if DEBUG - foreach (var line in logLines) - global::System.Diagnostics.Debug.WriteLine(line); -#endif - - // Enqueue log lines for asynchronous processing - EnqueueLogLines(logLines); - } - - private void EnqueueLogLines(List logLines) - { - lock (_queueLock) - { - // Force a flush when the queue reaches the maximum capacity - if (_logQueue.Count >= _maxQueuedEntries) - { - ForceWriteToFile(_logQueue.ToList()); - _logQueue.Clear(); - } - - foreach (var line in logLines) - _logQueue.Enqueue(line); - } + var sourceContext = FormatSourceContext(file, lineNumber, caller); + _logger.Write(LogEventLevel.Verbose, ex, "{Message} [@{SourceContext}]", message, sourceContext); } - - private async Task ProcessLogQueue() + + public void Flush() { - while (_isRunning) - { - try - { - // Drain the queue every 500 ms to reduce I/O operations and improve performance - await Task.Delay(500).ConfigureAwait(false); - - List? linesToWrite = null; - lock (_queueLock) - { - if (_logQueue.Count > 0) - { - linesToWrite = _logQueue.ToList(); - _logQueue.Clear(); - } - } - - if (linesToWrite?.Count > 0) - { - await WriteToFileAsync(linesToWrite).ConfigureAwait(false); - } - } - catch (Exception ex) - { - // Log queue processing failures but continue - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#204:ProcessLogQueue] [Trace] Failed during queue processing: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"queue_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } - } + // Serilog's async sink flushes on a timer; Log.CloseAndFlush is called on dispose. + // Synchronous flush is best-effort via the LoggingLevelSwitch no-op barrier. } - private async Task WriteToFileAsync(List lines) + public async Task ShutdownAsync() { - try - { - string logPathToUse; - lock (_lock) - { - if (File.Exists(_currentLogPath)) - { - try - { - var fileInfo = new FileInfo(_currentLogPath); - if (fileInfo.Length > _maxLogSizeBytes) - _currentLogPath = CreateNewLogFile(); - } - catch { _currentLogPath = CreateNewLogFile(); } - } - else { _currentLogPath = CreateNewLogFile(); } - logPathToUse = _currentLogPath; - } - - // Synchronize actual file I/O to prevent IOExceptions when multiple threads - // try to write to the same file (e.g., during Flush and background processing) - await Task.Run(() => - { - lock (_fileLock) - { - File.AppendAllLines(logPathToUse, lines); - } - }).ConfigureAwait(false); - } - catch (Exception ex) - { - // Log write failures but continue processing - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#242:WriteToFileAsync] [Trace] Failed to write to log file: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"write_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } + if (_disposed) + return; + + _disposed = true; + await Task.Run(() => _logger.Dispose()).ConfigureAwait(false); } - - private void ForceWriteToFile(List lines) + + public void Shutdown() { - try - { - string logPathToUse; - lock (_lock) - { - if (File.Exists(_currentLogPath)) - { - try - { - var fileInfo = new FileInfo(_currentLogPath); - if (fileInfo.Length > _maxLogSizeBytes) - _currentLogPath = CreateNewLogFile(); - } - catch { _currentLogPath = CreateNewLogFile(); } - } - else { _currentLogPath = CreateNewLogFile(); } - logPathToUse = _currentLogPath; - } - - // Use the same lock as background processing - lock (_fileLock) - { - File.AppendAllLines(logPathToUse, lines); - } - } - catch (Exception ex) - { - // Log forced write failures but continue processing - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#279:ForceWriteToFile] [Trace] Failed during forced write: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"force_write_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } + if (_disposed) + return; + + _disposed = true; + _logger.Dispose(); } - public void Flush() + private static string FormatSourceContext(string? file, int lineNumber, string? caller) { - List? remainingLines = null; - lock (_queueLock) - { - if (_logQueue.Count > 0) - { - remainingLines = _logQueue.ToList(); - _logQueue.Clear(); - } - } - - if (remainingLines?.Count > 0) - ForceWriteToFile(remainingLines); + var fileName = file is not null ? Path.GetFileName(file) : "?"; + return $"{fileName}#{lineNumber}:{caller}"; } - public async Task ShutdownAsync() + private static object[] BuildProperties(string sourceContext) { - // First, wait for ProcessLogQueue to complete its current iteration - // This ensures any logs enqueued before shutdown are processed - // ProcessLogQueue checks _isRunning every 500ms, so wait for one iteration - const int ITERATION_DELAY_MS = 500; // ProcessLogQueue iteration delay - await Task.Delay(ITERATION_DELAY_MS + 100).ConfigureAwait(false); - - // Now set the flag to stop ProcessLogQueue from starting new iterations - // This prevents new logs from being processed, but any logs already in the queue - // will be handled by Flush() at the end - _isRunning = false; - - // Wait for ProcessLogQueue to exit its loop - // ProcessLogQueue checks _isRunning every 500ms, so we need to wait at least that long - // plus some buffer for thread scheduling and I/O operations - const int MAX_WAIT_TIME_MS = 2000; // Total maximum wait time: 2 seconds - const int BUFFER_TIME_MS = 500; // Buffer for I/O and thread scheduling - - var startTime = DateTime.UtcNow; - - try - { - // First, wait for the task to complete naturally, but limit to MAX_WAIT_TIME_MS - var firstWaitMs = MAX_WAIT_TIME_MS; - var completedTask = await Task.WhenAny(_logTask, Task.Delay(firstWaitMs)).ConfigureAwait(false); - - // If the task hasn't completed, calculate remaining time and wait for one more iteration - if (completedTask != _logTask) - { - var elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; - var remainingTimeMs = MAX_WAIT_TIME_MS - elapsedMs; - - // Only wait additional time if we haven't exceeded MAX_WAIT_TIME_MS - if (remainingTimeMs > 0) - { - // Wait for at least one more iteration cycle plus buffer, but don't exceed remaining time - var additionalWaitMs = Math.Min(ITERATION_DELAY_MS + BUFFER_TIME_MS, remainingTimeMs); - if (additionalWaitMs > 0) - { - var additionalWait = Task.Delay(additionalWaitMs); - completedTask = await Task.WhenAny(_logTask, additionalWait).ConfigureAwait(false); - } - } - } - - // Final check: if still not completed, wait synchronously with remaining timeout - if (!_logTask.IsCompleted) - { - try - { - // Calculate remaining timeout based on elapsed time, ensuring we don't exceed MAX_WAIT_TIME_MS - var elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; - var remainingTimeout = Math.Max(0, Math.Min(500, MAX_WAIT_TIME_MS - elapsedMs)); - if (remainingTimeout > 0) - { - _logTask.Wait(remainingTimeout); - } - } - catch (Exception) - { - // If task faults or times out, log it but continue to flush - // Note: We must use ForceWriteToFile directly here because _isRunning is already false, - // so ProcessLogQueue has exited and won't process queued messages - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#380:ShutdownAsync] [Trace] Log task did not complete within timeout during shutdown. Proceeding with flush."; - ForceWriteToFile(new List { logLine }); - } - } - } - } - catch (Exception) - { - // Ignore timeout or cancellation while waiting - // Continue to flush to ensure any pending logs are written - } - - // Flush any remaining queued entries - // This is safe even if ProcessLogQueue is still running because Flush() uses locks - Flush(); - - // Final attempt to wait for any ongoing write operations to complete - // This ensures ProcessLogQueue has finished any File.AppendAllLinesAsync calls - if (!_logTask.IsCompleted) - { - try - { - // Give a final short wait for any ongoing I/O operations - const int FINAL_WAIT_MS = 300; // Final wait for I/O operations to complete - await Task.WhenAny(_logTask, Task.Delay(FINAL_WAIT_MS)).ConfigureAwait(false); - } - catch (Exception ex) - { - // Log final wait failures - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#392:ShutdownAsync] [Trace] Failed during final wait: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"shutdown_wait_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't write the error, continue silently - } - } - } - } + return [sourceContext, Environment.CurrentManagedThreadId]; } - public void Shutdown() + private static LogEventLevel MapLevelToSerilog(LogLevel level) => level switch { - try - { - // Use ConfigureAwait(false) to avoid potential deadlocks in synchronization contexts - ShutdownAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - // Log shutdown errors to help with debugging - if (IsTraceEnabled) - { - var timestamp = DateTime.UtcNow.ToString("dd/MM/yyyy HH:mm:ss.fff"); - var threadId = Environment.CurrentManagedThreadId; - var logLine = $"[{timestamp}] [{threadId}] [Log.cs#396:Shutdown] [Trace] Error during shutdown: {ex.Message}"; - try - { - File.AppendAllText(Path.Combine(_folderPath, $"shutdown_error_{DateTime.UtcNow:yyyy_MM_dd_HH_mm_ss_fff}.txt"), logLine); - } - catch - { - // If we can't even write the error, there's nothing more we can do - } - } - } - } + LogLevel.Error => LogEventLevel.Error, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Info => LogEventLevel.Information, + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Trace => LogEventLevel.Verbose, + _ => LogEventLevel.Verbose + }; + + private static LogLevel MapLevelFromSerilog(LogEventLevel level) => level switch + { + LogEventLevel.Error => LogLevel.Error, + LogEventLevel.Fatal => LogLevel.Error, + LogEventLevel.Warning => LogLevel.Warning, + LogEventLevel.Information => LogLevel.Info, + LogEventLevel.Debug => LogLevel.Debug, + LogEventLevel.Verbose => LogLevel.Trace, + _ => LogLevel.Trace + }; private static string Serialize(Exception ex) => new StringBuilder() .AppendLine("=== Exception ===") @@ -575,4 +271,14 @@ public void Shutdown() .AppendLine("=== Exception demystified ===") .AppendLine(ex.ToStringDemystified()) .ToString(); + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _logger?.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/LenovoLegionToolkit.Tests/GameDetection/GameDetectorTests.cs b/LenovoLegionToolkit.Tests/GameDetection/GameDetectorTests.cs new file mode 100644 index 000000000..a4055d995 --- /dev/null +++ b/LenovoLegionToolkit.Tests/GameDetection/GameDetectorTests.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using LenovoLegionToolkit.Lib; +using Xunit; + +namespace LenovoLegionToolkit.Tests.GameDetection; + +/// +/// Unit tests for ProcessInfo struct. +/// Note: GameConfigStoreDetector and EffectiveGameModeDetector are internal classes +/// and cannot be tested from the external test project. +/// +public class GameDetectorTests +{ + #region ProcessInfo.FromPath Tests + + [Fact] + public void FromPath_WithValidPath_ExtractsNameWithoutExtension() + { + // Arrange + var path = @"C:\Games\MyGame.exe"; + + // Act + var result = ProcessInfo.FromPath(path); + + // Assert + result.Name.Should().Be("MyGame"); + result.ExecutablePath.Should().Be(path); + } + + [Fact] + public void FromPath_WithDllExtension_ExtractsNameWithoutExtension() + { + // Arrange + var path = @"C:\Libraries\engine.dll"; + + // Act + var result = ProcessInfo.FromPath(path); + + // Assert + result.Name.Should().Be("engine"); + result.ExecutablePath.Should().Be(path); + } + + [Fact] + public void FromPath_WithNoExtension_ReturnsFullName() + { + // Arrange + var path = @"C:\Bin\program"; + + // Act + var result = ProcessInfo.FromPath(path); + + // Assert + result.Name.Should().Be("program"); + result.ExecutablePath.Should().Be(path); + } + + [Fact] + public void FromPath_WithNestedDirectories_ExtractsFileName() + { + // Arrange + var path = @"C:\Program Files\Game Studio\Game\bin\game.exe"; + + // Act + var result = ProcessInfo.FromPath(path); + + // Assert + result.Name.Should().Be("game"); + result.ExecutablePath.Should().Be(path); + } + + #endregion + + #region ProcessInfo Equality Tests + + [Fact] + public void Equals_WithSameNameAndPath_ReturnsTrue() + { + // Arrange + var info1 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + var info2 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + + // Act & Assert + info1.Should().Be(info2); + (info1 == info2).Should().BeTrue(); + (info1 != info2).Should().BeFalse(); + } + + [Fact] + public void Equals_WithDifferentName_ReturnsFalse() + { + // Arrange + var info1 = new ProcessInfo("GameA", @"C:\Games\Game.exe"); + var info2 = new ProcessInfo("GameB", @"C:\Games\Game.exe"); + + // Act & Assert + info1.Should().NotBe(info2); + (info1 == info2).Should().BeFalse(); + (info1 != info2).Should().BeTrue(); + } + + [Fact] + public void Equals_WithDifferentPath_ReturnsFalse() + { + // Arrange + var info1 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + var info2 = new ProcessInfo("MyGame", @"D:\Games\MyGame.exe"); + + // Act & Assert + info1.Should().NotBe(info2); + } + + [Fact] + public void GetHashCode_WithEqualObjects_ReturnsSameHash() + { + // Arrange + var info1 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + var info2 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + + // Act & Assert + info1.GetHashCode().Should().Be(info2.GetHashCode()); + } + + [Fact] + public void GetHashCode_WithDifferentObjects_LikelyDifferentHash() + { + // Arrange + var info1 = new ProcessInfo("GameA", @"C:\Games\A.exe"); + var info2 = new ProcessInfo("GameB", @"C:\Games\B.exe"); + + // Act & Assert + // Not guaranteed but overwhelmingly likely + info1.GetHashCode().Should().NotBe(info2.GetHashCode()); + } + + #endregion + + #region ProcessInfo Comparison Tests + + [Fact] + public void CompareTo_WithSameValues_ReturnsZero() + { + // Arrange + var info1 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + var info2 = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + + // Act + var result = info1.CompareTo(info2); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void CompareTo_WithNameAlphabeticallyLessThanOther_ReturnsNegative() + { + // Arrange + var info1 = new ProcessInfo("Alpha", @"C:\A.exe"); + var info2 = new ProcessInfo("Beta", @"C:\B.exe"); + + // Act + var result = info1.CompareTo(info2); + + // Assert + result.Should().BeNegative(); + } + + [Fact] + public void CompareTo_WithNameAlphabeticallyGreaterThanOther_ReturnsPositive() + { + // Arrange + var info1 = new ProcessInfo("Zeta", @"C:\Z.exe"); + var info2 = new ProcessInfo("Alpha", @"C:\A.exe"); + + // Act + var result = info1.CompareTo(info2); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void OperatorLessThan_WithLessName_ReturnsTrue() + { + // Arrange + var info1 = new ProcessInfo("Alpha", @"C:\A.exe"); + var info2 = new ProcessInfo("Beta", @"C:\B.exe"); + + // Act & Assert + (info1 < info2).Should().BeTrue(); + (info1 <= info2).Should().BeTrue(); + } + + [Fact] + public void OperatorGreaterThan_WithGreaterName_ReturnsTrue() + { + // Arrange + var info1 = new ProcessInfo("Zeta", @"C:\Z.exe"); + var info2 = new ProcessInfo("Alpha", @"C:\A.exe"); + + // Act & Assert + (info1 > info2).Should().BeTrue(); + (info1 >= info2).Should().BeTrue(); + } + + #endregion + + #region ProcessInfo ToString Tests + + [Fact] + public void ToString_ContainsNameAndPath() + { + // Arrange + var info = new ProcessInfo("MyGame", @"C:\Games\MyGame.exe"); + + // Act + var result = info.ToString(); + + // Assert + result.Should().Contain("MyGame"); + result.Should().Contain(@"C:\Games\MyGame.exe"); + } + + #endregion + + #region ProcessInfo HashSet Tests + + [Fact] + public void HashSet_WithDuplicateFromPath_AddsOnlyOne() + { + // Arrange + var set = new HashSet(); + + // Act + set.Add(ProcessInfo.FromPath(@"C:\Games\MyGame.exe")); + set.Add(ProcessInfo.FromPath(@"C:\Games\MyGame.exe")); + + // Assert + set.Count.Should().Be(1); + } + + [Fact] + public void HashSet_WithDifferentPaths_AddsBoth() + { + // Arrange + var set = new HashSet(); + + // Act + set.Add(ProcessInfo.FromPath(@"C:\Games\GameA.exe")); + set.Add(ProcessInfo.FromPath(@"C:\Games\GameB.exe")); + + // Assert + set.Count.Should().Be(2); + } + + [Fact] + public void HashSet_WithSameNameDifferentPath_AddsBoth() + { + // Arrange - Two different executables that happen to share a name (different paths) + var set = new HashSet(); + + // Act + set.Add(ProcessInfo.FromPath(@"C:\Steam\game.exe")); + set.Add(ProcessInfo.FromPath(@"D:\Epic\game.exe")); + + // Assert - different paths means different ProcessInfo + set.Count.Should().Be(2); + } + + #endregion +} diff --git a/LenovoLegionToolkit.Tests/GameDetectionTests.cs b/LenovoLegionToolkit.Tests/GameDetectionTests.cs new file mode 100644 index 000000000..87fe78ab9 --- /dev/null +++ b/LenovoLegionToolkit.Tests/GameDetectionTests.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using FluentAssertions; +using LenovoLegionToolkit.Lib; +using Xunit; + +namespace LenovoLegionToolkit.Tests; + +/// +/// Unit tests for the GameDetection module via reflection (classes are internal). +/// Tests cover GameConfigStoreDetector lifecycle, game detection logic, and ProcessInfo equality. +/// +public class GameDetectionTests +{ + private static readonly Assembly LibAssembly = typeof(ProcessInfo).Assembly; + + private static Type GetGameConfigStoreDetectorType() + { + return LibAssembly.GetType("LenovoLegionToolkit.Lib.GameDetection.GameConfigStoreDetector") + ?? throw new InvalidOperationException("GameConfigStoreDetector type not found"); + } + + private static Type GetEffectiveGameModeDetectorType() + { + return LibAssembly.GetType("LenovoLegionToolkit.Lib.GameDetection.EffectiveGameModeDetector") + ?? throw new InvalidOperationException("EffectiveGameModeDetector type not found"); + } + + private static Type GetGameDetectedEventArgsType() + { + return LibAssembly.GetType("LenovoLegionToolkit.Lib.GameDetection.GameConfigStoreDetector+GameDetectedEventArgs") + ?? throw new InvalidOperationException("GameDetectedEventArgs type not found"); + } + + #region GameConfigStoreDetector.GetDetectedGamePaths Tests + + [Fact] + public void GetDetectedGamePaths_WhenCalled_ReturnsHashSetOfProcessInfo() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var method = type.GetMethod("GetDetectedGamePaths", BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException("GetDetectedGamePaths method not found"); + + // Act + var result = method.Invoke(null, null); + + // Assert + result.Should().NotBeNull(); + result.Should().BeAssignableTo>(); + } + + [Fact] + public void GetDetectedGamePaths_WhenCalledMultipleTimes_ReturnsConsistentType() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var method = type.GetMethod("GetDetectedGamePaths", BindingFlags.Public | BindingFlags.Static)!; + + // Act + var result1 = (HashSet)method.Invoke(null, null)!; + var result2 = (HashSet)method.Invoke(null, null)!; + + // Assert + result1.Should().NotBeNull(); + result2.Should().NotBeNull(); + result1.GetType().Should().Be(result2.GetType()); + } + + [Fact] + public void GetDetectedGamePaths_ReturnsUniqueEntries() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var method = type.GetMethod("GetDetectedGamePaths", BindingFlags.Public | BindingFlags.Static)!; + + // Act + var result = (HashSet)method.Invoke(null, null)!; + + // Assert - HashSet guarantees uniqueness + result.Count.Should().Be(result.Distinct().Count()); + } + + #endregion + + #region GameConfigStoreDetector Start/Stop Lifecycle + + [Fact] + public async Task GameConfigStoreDetector_StartAsync_WhenCalledTwice_DoesNotThrow() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var instance = Activator.CreateInstance(type, nonPublic: true) + ?? throw new InvalidOperationException("Failed to create GameConfigStoreDetector"); + var startMethod = type.GetMethod("StartAsync")!; + var stopMethod = type.GetMethod("StopAsync")!; + + // Act + await (Task)startMethod.Invoke(instance, null)!; + await (Task)startMethod.Invoke(instance, null)!; + + // Cleanup + await (Task)stopMethod.Invoke(instance, null)!; + } + + [Fact] + public async Task GameConfigStoreDetector_StopAsync_WhenNotStarted_DoesNotThrow() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var instance = Activator.CreateInstance(type, nonPublic: true)!; + var stopMethod = type.GetMethod("StopAsync")!; + + // Act - should be safe to stop without starting + var act = async () => await (Task)stopMethod.Invoke(instance, null)!; + + // Assert + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task GameConfigStoreDetector_StartThenStop_DoesNotThrow() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var instance = Activator.CreateInstance(type, nonPublic: true)!; + var startMethod = type.GetMethod("StartAsync")!; + var stopMethod = type.GetMethod("StopAsync")!; + + // Act + await (Task)startMethod.Invoke(instance, null)!; + await (Task)stopMethod.Invoke(instance, null)!; + + // Second stop should also be safe + await (Task)stopMethod.Invoke(instance, null)!; + } + + #endregion + + #region GameDetectedEventArgs Tests + + [Fact] + public void GameDetectedEventArgs_WithGames_SetsGamesProperty() + { + // Arrange + var argsType = GetGameDetectedEventArgsType(); + var games = new HashSet(); + var constructor = argsType.GetConstructors().First(); + + // Act + var args = constructor.Invoke([games]); + + // Assert + var gamesProperty = argsType.GetProperty("Games")!; + var actualGames = gamesProperty.GetValue(args); + actualGames.Should().BeSameAs(games); + } + + [Fact] + public void GamesDetected_Event_CanBeSubscribedAndUnsubscribed() + { + // Arrange + var type = GetGameConfigStoreDetectorType(); + var instance = Activator.CreateInstance(type, nonPublic: true)!; + var eventInfo = type.GetEvent("GamesDetected")!; + var delegateType = eventInfo.EventHandlerType!; + var handler = Delegate.CreateDelegate(delegateType, typeof(GameDetectionTests).GetMethod(nameof(NoOpEventHandler), BindingFlags.NonPublic | BindingFlags.Static)!); + + // Act + eventInfo.AddEventHandler(instance, handler); + eventInfo.RemoveEventHandler(instance, handler); + + // Assert - no exception thrown + } + + private static void NoOpEventHandler(object sender, EventArgs args) { } + + #endregion + + #region EffectiveGameModeDetector Tests + + [Fact] + public void EffectiveGameModeDetector_Constructor_DoesNotThrow() + { + // Arrange + var type = GetEffectiveGameModeDetectorType(); + + // Act + var act = () => Activator.CreateInstance(type, nonPublic: true); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void EffectiveGameModeDetector_HasChangedEvent() + { + // Arrange + var type = GetEffectiveGameModeDetectorType(); + + // Act & Assert + var changedEvent = type.GetEvent("Changed"); + changedEvent.Should().NotBeNull(); + changedEvent!.EventHandlerType.Should().NotBeNull(); + } + + [Fact] + public async Task EffectiveGameModeDetector_StartAsync_DoesNotThrow() + { + // Arrange + var type = GetEffectiveGameModeDetectorType(); + var instance = Activator.CreateInstance(type, nonPublic: true)!; + var startMethod = type.GetMethod("StartAsync")!; + + // Act + await (Task)startMethod.Invoke(instance, null)!; + } + + #endregion + + #region ProcessInfo Tests (public struct used by GameDetection) + + [Fact] + public void ProcessInfo_FromPath_ExtractsNameFromPath() + { + // Act + var info = ProcessInfo.FromPath(@"C:\Games\MyGame.exe"); + + // Assert + info.Name.Should().Be("MyGame"); + info.ExecutablePath.Should().Be(@"C:\Games\MyGame.exe"); + } + + [Fact] + public void ProcessInfo_Equality_SameValues_AreEqual() + { + // Arrange + var info1 = new ProcessInfo("game", @"C:\game.exe"); + var info2 = new ProcessInfo("game", @"C:\game.exe"); + + // Act & Assert + info1.Should().Be(info2); + (info1 == info2).Should().BeTrue(); + } + + [Fact] + public void ProcessInfo_Equality_DifferentValues_AreNotEqual() + { + // Arrange + var info1 = new ProcessInfo("game1", @"C:\game1.exe"); + var info2 = new ProcessInfo("game2", @"C:\game2.exe"); + + // Act & Assert + info1.Should().NotBe(info2); + (info1 != info2).Should().BeTrue(); + } + + [Fact] + public void ProcessInfo_GetHashCode_SameForEqualInstances() + { + // Arrange + var info1 = new ProcessInfo("game", @"C:\game.exe"); + var info2 = new ProcessInfo("game", @"C:\game.exe"); + + // Act & Assert + info1.GetHashCode().Should().Be(info2.GetHashCode()); + } + + [Fact] + public void ProcessInfo_ToString_ContainsNameAndPath() + { + // Arrange + var info = new ProcessInfo("test", @"C:\test.exe"); + + // Act + var result = info.ToString(); + + // Assert + result.Should().Contain("test"); + result.Should().Contain(@"C:\test.exe"); + } + + #endregion +} diff --git a/LenovoLegionToolkit.Tests/LenovoLegionToolkit.Tests.csproj b/LenovoLegionToolkit.Tests/LenovoLegionToolkit.Tests.csproj index 9524a2c57..290778cfd 100644 --- a/LenovoLegionToolkit.Tests/LenovoLegionToolkit.Tests.csproj +++ b/LenovoLegionToolkit.Tests/LenovoLegionToolkit.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/LenovoLegionToolkit.Tests/SoftwareDisabler/SoftwareDisablerTests.cs b/LenovoLegionToolkit.Tests/SoftwareDisabler/SoftwareDisablerTests.cs new file mode 100644 index 000000000..0b111d12f --- /dev/null +++ b/LenovoLegionToolkit.Tests/SoftwareDisabler/SoftwareDisablerTests.cs @@ -0,0 +1,388 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using LenovoLegionToolkit.Lib; +using LenovoLegionToolkit.Lib.SoftwareDisabler; +using Xunit; + +namespace LenovoLegionToolkit.Tests.SoftwareDisabler; + +/// +/// Unit tests for the SoftwareDisabler module. +/// Covers: SoftwareStatus enum values, SoftwareDisablerException, +/// concrete disabler configurations (FnKeysDisabler, LegionZoneDisabler, VantageDisabler), +/// GetStatusAsync behavior, and error handling patterns. +/// +public class SoftwareDisablerTests +{ + #region SoftwareStatus Enum Tests + + [Fact] + public void SoftwareStatus_HasThreeValues() + { + // Act + var values = Enum.GetValues(); + + // Assert + values.Should().HaveCount(3); + } + + [Fact] + public void SoftwareStatus_ContainsExpectedMembers() + { + // Assert + SoftwareStatus.Enabled.Should().Be(SoftwareStatus.Enabled); + SoftwareStatus.Disabled.Should().Be(SoftwareStatus.Disabled); + SoftwareStatus.NotFound.Should().Be(SoftwareStatus.NotFound); + } + + [Fact] + public void SoftwareStatus_MembersHaveDistinctValues() + { + // Act + var values = Enum.GetValues().Cast().ToList(); + + // Assert + values.Should().OnlyHaveUniqueItems(); + } + + #endregion + + #region SoftwareDisablerException Tests + + [Fact] + public void SoftwareDisablerException_WithMessage_SetsMessage() + { + // Arrange + var inner = new InvalidOperationException("inner error"); + + // Act + var ex = new SoftwareDisablerException("test message", inner); + + // Assert + ex.Message.Should().Be("test message"); + ex.InnerException.Should().BeSameAs(inner); + } + + [Fact] + public void SoftwareDisablerException_InheritsFromException() + { + // Arrange + var inner = new InvalidOperationException("inner"); + + // Act + var ex = new SoftwareDisablerException("msg", inner); + + // Assert + ex.Should().BeAssignableTo(); + } + + [Fact] + public void SoftwareDisablerException_WithNullInnerException_DoesNotThrow() + { + // Act + var act = () => new SoftwareDisablerException("msg", null!); + + // Assert + act.Should().NotThrow(); + } + + #endregion + + #region FnKeysDisabler Configuration Tests + + [Fact] + public void FnKeysDisabler_ServiceNames_ContainsExpectedService() + { + // Arrange + var disabler = new FnKeysDisabler(); + + // Act - GetStatusAsync exercises the protected members via reflection-free testing + // We verify the disabler can be instantiated and used + disabler.Should().NotBeNull(); + disabler.Should().BeAssignableTo(); + } + + [Fact] + public async Task FnKeysDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new FnKeysDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert - on any system this should return one of the three valid statuses + status.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound); + } + + [Fact] + public void FnKeysDisabler_OnRefreshedEvent_CanSubscribe() + { + // Arrange + var disabler = new FnKeysDisabler(); + var eventRaised = false; + + // Act + disabler.OnRefreshed += (_, args) => eventRaised = true; + + // Assert + eventRaised.Should().BeFalse(); + } + + [Fact] + public async Task FnKeysDisabler_GetStatusAsync_InvokesOnRefreshedEvent() + { + // Arrange + var disabler = new FnKeysDisabler(); + AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs? receivedArgs = null; + + disabler.OnRefreshed += (_, args) => receivedArgs = args; + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + receivedArgs.Should().NotBeNull(); + receivedArgs!.Status.Should().Be(status); + } + + [Fact] + public async Task FnKeysDisabler_GetStatusAsync_CalledTwice_DoesNotThrow() + { + // Arrange + var disabler = new FnKeysDisabler(); + + // Act + var status1 = await disabler.GetStatusAsync(); + var status2 = await disabler.GetStatusAsync(); + + // Assert + status1.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound); + status2.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound); + } + + #endregion + + #region LegionZoneDisabler Configuration Tests + + [Fact] + public void LegionZoneDisabler_CanBeInstantiated() + { + // Act + var disabler = new LegionZoneDisabler(); + + // Assert + disabler.Should().NotBeNull(); + disabler.Should().BeAssignableTo(); + } + + [Fact] + public async Task LegionZoneDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new LegionZoneDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + status.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound); + } + + [Fact] + public async Task LegionZoneDisabler_GetStatusAsync_InvokesOnRefreshedEvent() + { + // Arrange + var disabler = new LegionZoneDisabler(); + var eventFired = false; + + disabler.OnRefreshed += (_, _) => eventFired = true; + + // Act + await disabler.GetStatusAsync(); + + // Assert + eventFired.Should().BeTrue(); + } + + [Fact] + public void LegionZoneDisabler_OnRefreshedEvent_CanSubscribeAndUnsubscribe() + { + // Arrange + var disabler = new LegionZoneDisabler(); + EventHandler handler = (_, _) => { }; + + // Act & Assert + disabler.OnRefreshed += handler; + disabler.OnRefreshed -= handler; + } + + [Fact] + public async Task LegionZoneDisabler_GetStatusAsync_MultipleConcurrentCalls_DoNotThrow() + { + // Arrange + var disabler = new LegionZoneDisabler(); + + // Act + var tasks = Enumerable.Range(0, 5).Select(_ => disabler.GetStatusAsync()).ToArray(); + var results = await Task.WhenAll(tasks); + + // Assert + results.Should().AllSatisfy(s => + s.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound)); + } + + #endregion + + #region VantageDisabler Configuration Tests + + [Fact] + public void VantageDisabler_CanBeInstantiated() + { + // Act + var disabler = new VantageDisabler(); + + // Assert + disabler.Should().NotBeNull(); + disabler.Should().BeAssignableTo(); + } + + [Fact] + public async Task VantageDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new VantageDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + status.Should().BeOneOf(SoftwareStatus.Enabled, SoftwareStatus.Disabled, SoftwareStatus.NotFound); + } + + [Fact] + public async Task VantageDisabler_GetStatusAsync_InvokesOnRefreshedEvent() + { + // Arrange + var disabler = new VantageDisabler(); + AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs? receivedArgs = null; + + disabler.OnRefreshed += (_, args) => receivedArgs = args; + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + receivedArgs.Should().NotBeNull(); + receivedArgs!.Status.Should().Be(status); + } + + [Fact] + public void VantageDisabler_OnRefreshedEvent_CanSubscribe() + { + // Arrange + var disabler = new VantageDisabler(); + var callCount = 0; + + // Act + disabler.OnRefreshed += (_, _) => callCount++; + + // Assert + callCount.Should().Be(0); + } + + [Fact] + public async Task VantageDisabler_GetStatusAsync_CalledMultipleTimes_ReturnsConsistentType() + { + // Arrange + var disabler = new VantageDisabler(); + + // Act + var results = new List(); + for (var i = 0; i < 3; i++) + { + results.Add(await disabler.GetStatusAsync()); + } + + // Assert - all results should be the same status (environment doesn't change during test) + results.Should().AllBeEquivalentTo(results.First()); + } + + #endregion + + #region AbstractSoftwareDisablerEventArgs Tests + + [Fact] + public void AbstractSoftwareDisablerEventArgs_StatusProperty_CanBeSet() + { + // Act + var args = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs + { + Status = SoftwareStatus.Enabled + }; + + // Assert + args.Status.Should().Be(SoftwareStatus.Enabled); + } + + [Fact] + public void AbstractSoftwareDisablerEventArgs_AllStatuses_CanBeAssigned() + { + // Act & Assert + var args1 = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs { Status = SoftwareStatus.Enabled }; + var args2 = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs { Status = SoftwareStatus.Disabled }; + var args3 = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs { Status = SoftwareStatus.NotFound }; + + args1.Status.Should().Be(SoftwareStatus.Enabled); + args2.Status.Should().Be(SoftwareStatus.Disabled); + args3.Status.Should().Be(SoftwareStatus.NotFound); + } + + #endregion + + #region Cross-Disabler Consistency Tests + + [Fact] + public async Task AllDisablers_GetStatusAsync_DoNotThrow() + { + // Arrange + var disablers = new AbstractSoftwareDisabler[] + { + new FnKeysDisabler(), + new LegionZoneDisabler(), + new VantageDisabler() + }; + + // Act & Assert + foreach (var disabler in disablers) + { + var act = async () => await disabler.GetStatusAsync(); + await act.Should().NotThrowAsync(); + } + } + + [Fact] + public async Task AllDisablers_GetStatusAsync_ReturnsKnownStatus() + { + // Arrange + var disablers = new AbstractSoftwareDisabler[] + { + new FnKeysDisabler(), + new LegionZoneDisabler(), + new VantageDisabler() + }; + + // Act & Assert + foreach (var disabler in disablers) + { + var status = await disabler.GetStatusAsync(); + Enum.IsDefined(typeof(SoftwareStatus), status).Should().BeTrue( + $"because {disabler.GetType().Name} returned {status}"); + } + } + + #endregion +} diff --git a/LenovoLegionToolkit.Tests/SoftwareDisablerTests.cs b/LenovoLegionToolkit.Tests/SoftwareDisablerTests.cs new file mode 100644 index 000000000..34b541f30 --- /dev/null +++ b/LenovoLegionToolkit.Tests/SoftwareDisablerTests.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using FluentAssertions; +using LenovoLegionToolkit.Lib; +using LenovoLegionToolkit.Lib.SoftwareDisabler; +using Xunit; + +namespace LenovoLegionToolkit.Tests; + +/// +/// Unit tests for the SoftwareDisabler module: AbstractSoftwareDisabler, VantageDisabler, +/// FnKeysDisabler, and LegionZoneDisabler. +/// +public class SoftwareDisablerTests +{ + #region SoftwareStatus Enum Tests + + [Fact] + public void SoftwareStatus_HasThreeValues() + { + var values = Enum.GetValues(); + + values.Should().HaveCount(3); + values.Should().Contain(SoftwareStatus.Enabled); + values.Should().Contain(SoftwareStatus.Disabled); + values.Should().Contain(SoftwareStatus.NotFound); + } + + [Fact] + public void SoftwareStatus_Enabled_HasExpectedValue() + { + ((int)SoftwareStatus.Enabled).Should().Be(0); + } + + [Fact] + public void SoftwareStatus_Disabled_HasExpectedValue() + { + ((int)SoftwareStatus.Disabled).Should().Be(1); + } + + [Fact] + public void SoftwareStatus_NotFound_HasExpectedValue() + { + ((int)SoftwareStatus.NotFound).Should().Be(2); + } + + #endregion + + #region SoftwareDisablerException Tests + + [Fact] + public void SoftwareDisablerException_WithMessageAndInner_SetsProperties() + { + // Arrange + var inner = new InvalidOperationException("inner"); + + // Act + var ex = new SoftwareDisablerException("test message", inner); + + // Assert + ex.Message.Should().Contain("test message"); + ex.InnerException.Should().BeSameAs(inner); + } + + [Fact] + public void SoftwareDisablerException_IsException() + { + // Act + var ex = new SoftwareDisablerException("msg", new Exception()); + + // Assert + ex.Should().BeAssignableTo(); + } + + #endregion + + #region VantageDisabler Protected Properties + + [Fact] + public void VantageDisabler_HasScheduledTasksPaths() + { + // Arrange + var disabler = new VantageDisabler(); + var property = typeof(VantageDisabler).GetProperty("ScheduledTasksPaths", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var paths = (IEnumerable)property.GetValue(disabler)!; + + // Assert + paths.Should().NotBeEmpty(); + paths.Should().Contain("Lenovo\\Vantage"); + paths.Should().Contain("Lenovo\\ImController"); + } + + [Fact] + public void VantageDisabler_HasServiceNames() + { + // Arrange + var disabler = new VantageDisabler(); + var property = typeof(VantageDisabler).GetProperty("ServiceNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().NotBeEmpty(); + names.Should().Contain("ImControllerService"); + names.Should().Contain("LenovoVantageService"); + } + + [Fact] + public void VantageDisabler_HasProcessNames() + { + // Arrange + var disabler = new VantageDisabler(); + var property = typeof(VantageDisabler).GetProperty("ProcessNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().NotBeEmpty(); + names.Should().Contain("LenovoVantage"); + } + + [Fact] + public void VantageDisabler_ScheduledTasksPaths_ContainsExpectedCount() + { + // Arrange + var disabler = new VantageDisabler(); + var property = typeof(VantageDisabler).GetProperty("ScheduledTasksPaths", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var paths = (IEnumerable)property.GetValue(disabler)!; + + // Assert - VantageDisabler has 7 scheduled task paths + paths.Count().Should().Be(7); + } + + #endregion + + #region FnKeysDisabler Protected Properties + + [Fact] + public void FnKeysDisabler_HasEmptyScheduledTasksPaths() + { + // Arrange + var disabler = new FnKeysDisabler(); + var property = typeof(FnKeysDisabler).GetProperty("ScheduledTasksPaths", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var paths = (IEnumerable)property.GetValue(disabler)!; + + // Assert + paths.Should().BeEmpty(); + } + + [Fact] + public void FnKeysDisabler_HasServiceNames() + { + // Arrange + var disabler = new FnKeysDisabler(); + var property = typeof(FnKeysDisabler).GetProperty("ServiceNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().ContainSingle("LenovoFnAndFunctionKeys"); + } + + [Fact] + public void FnKeysDisabler_HasProcessNames() + { + // Arrange + var disabler = new FnKeysDisabler(); + var property = typeof(FnKeysDisabler).GetProperty("ProcessNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().Contain("LenovoUtilityUI"); + names.Should().Contain("LenovoUtilityService"); + names.Should().Contain("LenovoSmartKey"); + names.Should().HaveCount(3); + } + + #endregion + + #region LegionZoneDisabler Protected Properties + + [Fact] + public void LegionZoneDisabler_HasEmptyScheduledTasksPaths() + { + // Arrange + var disabler = new LegionZoneDisabler(); + var property = typeof(LegionZoneDisabler).GetProperty("ScheduledTasksPaths", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var paths = (IEnumerable)property.GetValue(disabler)!; + + // Assert + paths.Should().BeEmpty(); + } + + [Fact] + public void LegionZoneDisabler_HasServiceNames() + { + // Arrange + var disabler = new LegionZoneDisabler(); + var property = typeof(LegionZoneDisabler).GetProperty("ServiceNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().ContainSingle("LZService"); + } + + [Fact] + public void LegionZoneDisabler_HasProcessNames() + { + // Arrange + var disabler = new LegionZoneDisabler(); + var property = typeof(LegionZoneDisabler).GetProperty("ProcessNames", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var names = (IEnumerable)property.GetValue(disabler)!; + + // Assert + names.Should().Contain("LegionZone"); + names.Should().Contain("LZTray"); + names.Should().HaveCount(2); + } + + #endregion + + #region OnRefreshed Event Tests + + [Fact] + public void VantageDisabler_OnRefreshed_Event_CanSubscribe() + { + // Arrange + var disabler = new VantageDisabler(); + var callCount = 0; + + // Act + disabler.OnRefreshed += (_, _) => callCount++; + + // Assert - no exception thrown + callCount.Should().Be(0); + } + + [Fact] + public void FnKeysDisabler_OnRefreshed_Event_CanSubscribe() + { + // Arrange + var disabler = new FnKeysDisabler(); + var callCount = 0; + + // Act + disabler.OnRefreshed += (_, _) => callCount++; + + // Assert + callCount.Should().Be(0); + } + + [Fact] + public void LegionZoneDisabler_OnRefreshed_Event_CanSubscribe() + { + // Arrange + var disabler = new LegionZoneDisabler(); + var callCount = 0; + + // Act + disabler.OnRefreshed += (_, _) => callCount++; + + // Assert + callCount.Should().Be(0); + } + + #endregion + + #region AbstractSoftwareDisablerEventArgs Tests + + [Fact] + public void AbstractSoftwareDisablerEventArgs_SetsStatus() + { + // Arrange & Act + var args = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs { Status = SoftwareStatus.Enabled }; + + // Assert + args.Status.Should().Be(SoftwareStatus.Enabled); + } + + [Fact] + public void AbstractSoftwareDisablerEventArgs_CanSetAllStatusValues() + { + foreach (var status in Enum.GetValues()) + { + // Act + var args = new AbstractSoftwareDisabler.AbstractSoftwareDisablerEventArgs { Status = status }; + + // Assert + args.Status.Should().Be(status); + } + } + + #endregion + + #region GetStatusAsync Tests + + [Fact] + public async Task VantageDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new VantageDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert - any valid enum value is acceptable + Enum.GetValues().Should().Contain(status); + } + + [Fact] + public async Task FnKeysDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new FnKeysDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + Enum.GetValues().Should().Contain(status); + } + + [Fact] + public async Task LegionZoneDisabler_GetStatusAsync_ReturnsValidStatus() + { + // Arrange + var disabler = new LegionZoneDisabler(); + + // Act + var status = await disabler.GetStatusAsync(); + + // Assert + Enum.GetValues().Should().Contain(status); + } + + #endregion +} diff --git a/LenovoLegionToolkit.WPF/App.xaml.cs b/LenovoLegionToolkit.WPF/App.xaml.cs index 3cdf8894c..170a3ec33 100644 --- a/LenovoLegionToolkit.WPF/App.xaml.cs +++ b/LenovoLegionToolkit.WPF/App.xaml.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -92,6 +93,7 @@ private async void Application_Startup(object sender, StartupEventArgs e) Environment.SetEnvironmentVariable("LLT_LOG_PATH", Log.Instance.LogPath); AppDomain.CurrentDomain.UnhandledException += AppDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; if (Log.Instance.IsTraceEnabled) Log.Instance.Trace($"Flags: {flags}"); @@ -202,6 +204,7 @@ private async void Application_Startup(object sender, StartupEventArgs e) IoCContainer.Initialize( new Lib.IoCModule(), + new Lib.Plugins.IoCModule(), new Lib.Automation.IoCModule(), new Lib.Macro.IoCModule(), new IoCModule() @@ -250,6 +253,10 @@ private async void Application_Startup(object sender, StartupEventArgs e) IoCContainer.Resolve().Apply(); + // Check for unsent crash reports from previous session + // This shows a modal dialog before the main window appears + CheckPendingCrashReports(); + if (flags.Minimized) { if (Log.Instance.IsTraceEnabled) @@ -295,6 +302,57 @@ private static async Task InitializePluginsAsync() } } + private static void CheckPendingCrashReports() + { + try + { + // Clean up old crash reports first (older than 30 days) + CrashReportHelper.CleanupOldCrashReports(30); + + var reports = CrashReportHelper.GetUnsentCrashReports().ToList(); + if (reports.Count <= 0) + return; + + // Log that we found pending crash reports + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Found {reports.Count} pending crash report(s)."); + + // Show crash report notification for the most recent report + var mostRecentReport = reports.OrderByDescending(r => + { + var info = new FileInfo(r); + return info.CreationTimeUtc; + }).FirstOrDefault(); + + if (mostRecentReport != null) + { + try + { + var notificationWindow = new CrashReportNotificationWindow(mostRecentReport); + notificationWindow.ShowDialog(); + + // Delete other reports (keep only the most recent one shown) + foreach (var otherReport in reports.Where(r => r != mostRecentReport)) + { + CrashReportHelper.DeleteCrashReport(otherReport); + } + } + catch (Exception ex) + { + if (Log.Instance.IsTraceEnabled) + Log.Instance.Trace($"Failed to show crash report notification: {ex.Message}", ex); + + // Delete all reports if we can't show the notification + foreach (var report in reports) + { + CrashReportHelper.DeleteCrashReport(report); + } + } + } + } + catch { /* Ignore crash report checking errors */ } + } + private void StartBackgroundInitialization() { _backgroundInitializationCancellationTokenSource = new CancellationTokenSource(); @@ -793,6 +851,9 @@ private void AppDomain_UnhandledException(object sender, UnhandledExceptionEvent Log.Instance.ErrorReport("AppDomain_UnhandledException", exception ?? new Exception($"Unknown exception caught: {e.ExceptionObject}")); Log.Instance.Trace($"Unhandled exception occurred.", exception); + // Save crash report BEFORE showing message box + CrashReportHelper.SaveCrashReport(exception, "AppDomain"); + // Try to show message box, but don't let it cause infinite recursion try { @@ -846,6 +907,9 @@ private void Application_DispatcherUnhandledException(object sender, DispatcherU Log.Instance.ErrorReport("Application_DispatcherUnhandledException", e.Exception); Log.Instance.Trace($"Unhandled exception occurred.", e.Exception); + // Save crash report BEFORE showing message box + CrashReportHelper.SaveCrashReport(e.Exception, "Dispatcher"); + // Try to show message box, but don't let it cause infinite recursion try { @@ -881,6 +945,29 @@ private void Application_DispatcherUnhandledException(object sender, DispatcherU } } + private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + try + { + // Log the unobserved task exception + Log.Instance.ErrorReport("TaskScheduler_UnobservedTaskException", e.Exception); + Log.Instance.Trace($"Unobserved task exception occurred.", e.Exception); + + // Save crash report + CrashReportHelper.SaveCrashReport(e.Exception, "TaskScheduler"); + + // Mark as observed to prevent the process from terminating + // Note: In .NET 5+, unobserved task exceptions don't terminate the process by default, + // but we mark as observed for safety + e.SetObserved(); + } + catch + { + // If even this fails, mark as observed to prevent termination + e.SetObserved(); + } + } + private void EnsureSingleInstance() { diff --git a/LenovoLegionToolkit.WPF/LenovoLegionToolkit.WPF.csproj b/LenovoLegionToolkit.WPF/LenovoLegionToolkit.WPF.csproj index ecd5cc826..2d4f1197d 100644 --- a/LenovoLegionToolkit.WPF/LenovoLegionToolkit.WPF.csproj +++ b/LenovoLegionToolkit.WPF/LenovoLegionToolkit.WPF.csproj @@ -31,6 +31,7 @@ + @@ -39,10 +40,10 @@ + - diff --git a/LenovoLegionToolkit.WPF/Pages/KeyboardBacklightPage.xaml.cs b/LenovoLegionToolkit.WPF/Pages/KeyboardBacklightPage.xaml.cs index 8be9da48b..537841468 100644 --- a/LenovoLegionToolkit.WPF/Pages/KeyboardBacklightPage.xaml.cs +++ b/LenovoLegionToolkit.WPF/Pages/KeyboardBacklightPage.xaml.cs @@ -1,15 +1,16 @@ -using System; +using System; using System.Threading.Tasks; using System.Windows; -using LenovoLegionToolkit.Lib; -using LenovoLegionToolkit.Lib.Controllers; using LenovoLegionToolkit.WPF.Controls.KeyboardBacklight.RGB; using LenovoLegionToolkit.WPF.Controls.KeyboardBacklight.Spectrum; +using LenovoLegionToolkit.WPF.ViewModels; namespace LenovoLegionToolkit.WPF.Pages { public partial class KeyboardBacklightPage { + private readonly KeyboardBacklightViewModel _viewModel = new(); + public KeyboardBacklightPage() => InitializeComponent(); private async void KeyboardBacklightPage_Initialized(object? sender, EventArgs e) @@ -20,38 +21,26 @@ private async void KeyboardBacklightPage_Initialized(object? sender, EventArgs e _titleTextBlock.Visibility = Visibility.Visible; - var spectrumController = IoCContainer.Resolve(); - if (await spectrumController.IsSupportedAsync()) + await _viewModel.DetectKeyboardTypeCommand.ExecuteAsync(null); + + if (_viewModel.IsSpectrumSupported) { var control = new SpectrumKeyboardBacklightControl(); _content.Children.Add(control); - _loader.IsLoading = false; - return; } - - var rgbController = IoCContainer.Resolve(); - if (await rgbController.IsSupportedAsync()) + else if (_viewModel.IsRGBSupported) { var control = new RGBKeyboardBacklightControl(); _content.Children.Add(control); - _loader.IsLoading = false; - return; + } + else + { + _noKeyboardsText.Visibility = Visibility.Visible; } - _noKeyboardsText.Visibility = Visibility.Visible; _loader.IsLoading = false; } - public static async Task IsSupportedAsync() - { - var spectrumController = IoCContainer.Resolve(); - if (await spectrumController.IsSupportedAsync()) - return true; - var rgbController = IoCContainer.Resolve(); - if (await rgbController.IsSupportedAsync()) - return true; - - return false; - } + public static async Task IsSupportedAsync() => await KeyboardBacklightViewModel.IsSupportedAsync(); } } diff --git a/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs b/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs index 076721960..69d53e8ce 100644 --- a/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs +++ b/LenovoLegionToolkit.WPF/Pages/MacroPage.xaml.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Linq; using System.Windows; using LenovoLegionToolkit.Lib; using LenovoLegionToolkit.Lib.Extensions; using LenovoLegionToolkit.Lib.Macro; +using LenovoLegionToolkit.WPF.ViewModels; using Wpf.Ui.Common; using Wpf.Ui.Controls; @@ -11,7 +12,7 @@ namespace LenovoLegionToolkit.WPF.Pages { public partial class MacroPage { - private readonly MacroController _controller = IoCContainer.Resolve(); + private readonly MacroViewModel _viewModel = new(IoCContainer.Resolve()); public MacroPage() { @@ -22,7 +23,8 @@ public MacroPage() private void MacroPage_Initialized(object? sender, EventArgs e) { - _enableMacroToggle.IsChecked = _controller.IsEnabled; + _viewModel.LoadState(); + _enableMacroToggle.IsChecked = _viewModel.IsEnabled; var zeroNumberButton = _numberPad.Children.OfType