Compare commits

...

4 Commits

Author SHA1 Message Date
r-zakarya dcb1ac63ce fix(totp): pas de notification 'toujours dans le tray' pendant le scan du QR
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:40:00 +01:00
r-zakarya e3904236a7 fix(totp): scan QR ecran - indices Byte dans ZXing (boucle infinie a 100 % CPU)
ZXing.Delphi declarait rgbIndex/luminanceIndex en Byte dans les conversions
octets bruts -> luminance : au-dela de 255 ils repassaient a 0, la boucle ne
finissait jamais. L'APK n'y passait pas (CreateFromBitmap). Mesure apres
correctif : 60 ms sur un ecran 1920x1080 avec TRY_HARDER, 86 ms sans QR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:35:49 +01:00
r-zakarya 72f9acd733 feat(totp): scanner le QR 2FA affiche a l'ecran (capture + ZXing), seul un otpauth:// part vers la page
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:24:35 +01:00
r-zakarya 508d5345bf feat(fillbar): option fond transparent (cle de couleur), l'opacite reste sur les boutons
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:12:37 +01:00
9 changed files with 256 additions and 16 deletions
+10
View File
@@ -768,6 +768,16 @@ Sidebar Tools expose 2 items MFA :
secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien
n'est sauvegardé. n'est sauvegardé.
**Scanner le QR à l'écran** (bouton QR du champ 2FA de l'éditeur, 2026-09-18) :
`cmd://qr/screen` → la fenêtre passe au tray, `PM.ScreenQr` capture **tout le
bureau virtuel** en mémoire (GDI, octets bruts, jamais sur disque) et le décode
par ZXing.Delphi — le même que l'APK, `android-client\ThirdParty\ZXing` dans le
chemin de recherche du `.dproj`. Pas de sélection de zone : le décodeur trouve
le QR où qu'il soit. **Seul un `otpauth://` repart vers la page**
(`Bridge.onScreenQr`), le reste de l'écran ne quitte pas l'exe. Format `BGR32`
et non `BGRA32` : l'octet alpha d'une capture GDI vaut 0, lu comme transparent
→ image blanche.
Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom, Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom,
**pas `remaining`** (utiliser `t.secondsLeft`). **pas `remaining`** (utiliser `t.secondsLeft`).
@@ -364,7 +364,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGB24(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
r, g, b : Integer; r, g, b : Integer;
begin begin
rgbIndex := 0; rgbIndex := 0;
@@ -389,7 +389,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGR24(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
b, g, r : Byte; b, g, r : Byte;
begin begin
rgbIndex := 0; rgbIndex := 0;
@@ -414,7 +414,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGB32(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
r, g, b : Byte; r, g, b : Byte;
begin begin
rgbIndex := 0; rgbIndex := 0;
@@ -439,7 +439,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGR32(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
b, g, r : Byte; b, g, r : Byte;
begin begin
rgbIndex := 0; rgbIndex := 0;
@@ -464,7 +464,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceBGRA32(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
b, g, r, b, g, r,
alpha : Byte; alpha : Byte;
luminance : Byte; luminance : Byte;
@@ -495,7 +495,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceRGBA32(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
r, g, b, r, g, b,
alpha : Byte; alpha : Byte;
luminance : Byte; luminance : Byte;
@@ -526,7 +526,7 @@ procedure TRGBLuminanceSource.CalculateLuminanceARGB32(
const rgbRawBytes: TArray<Byte>); const rgbRawBytes: TArray<Byte>);
var var
rgbIndex, rgbIndex,
luminanceIndex, luminanceIndex : Integer; // pmserver: etaient Byte -> boucle infinie au-dela de 255 octets
r, g, b, r, g, b,
alpha : Byte; alpha : Byte;
luminance : Byte; luminance : Byte;
+1
View File
@@ -24,6 +24,7 @@ uses
PM.Licence in 'Source\PM.Licence.pas', PM.Licence in 'Source\PM.Licence.pas',
PM.LicenceGate in 'Source\PM.LicenceGate.pas', PM.LicenceGate in 'Source\PM.LicenceGate.pas',
DelphiZXingQRCode in 'Source\DelphiZXingQRCode.pas', DelphiZXingQRCode in 'Source\DelphiZXingQRCode.pas',
PM.ScreenQr in 'Source\PM.ScreenQr.pas',
PM.AutoStart in 'Source\PM.AutoStart.pas', PM.AutoStart in 'Source\PM.AutoStart.pas',
PM.Favicon in 'Source\PM.Favicon.pas', PM.Favicon in 'Source\PM.Favicon.pas',
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas', PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
+2
View File
@@ -91,6 +91,7 @@
<Base>true</Base> <Base>true</Base>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''"> <PropertyGroup Condition="'$(Base)'!=''">
<DCC_UnitSearchPath>..\android-client\ThirdParty\ZXing;..\android-client\ThirdParty\ZXing\1D Barcodes;..\android-client\ThirdParty\ZXing\2D Barcodes;..\android-client\ThirdParty\ZXing\2D Barcodes\Decoder;..\android-client\ThirdParty\ZXing\2D Barcodes\Detector;..\android-client\ThirdParty\ZXing\Common;..\android-client\ThirdParty\ZXing\Common\Detector;..\android-client\ThirdParty\ZXing\Common\ReedSolomon;..\android-client\ThirdParty\ZXing\Filtering;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<DCC_E>false</DCC_E> <DCC_E>false</DCC_E>
<DCC_F>false</DCC_F> <DCC_F>false</DCC_F>
<DCC_K>false</DCC_K> <DCC_K>false</DCC_K>
@@ -250,6 +251,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="Source\PM.Licence.pas"/> <DCCReference Include="Source\PM.Licence.pas"/>
<DCCReference Include="Source\PM.LicenceGate.pas"/> <DCCReference Include="Source\PM.LicenceGate.pas"/>
<DCCReference Include="Source\DelphiZXingQRCode.pas"/> <DCCReference Include="Source\DelphiZXingQRCode.pas"/>
<DCCReference Include="Source\PM.ScreenQr.pas"/>
<DCCReference Include="Source\PM.AutoStart.pas"/> <DCCReference Include="Source\PM.AutoStart.pas"/>
<DCCReference Include="Source\PM.Favicon.pas"/> <DCCReference Include="Source\PM.Favicon.pas"/>
<DCCReference Include="Source\PM.ProcessLockdown.pas"/> <DCCReference Include="Source\PM.ProcessLockdown.pas"/>
+40 -6
View File
@@ -174,6 +174,8 @@ type
// Barrette en icones (police Segoe MDL2 Assets) plutot qu'en mots, et sa // Barrette en icones (police Segoe MDL2 Assets) plutot qu'en mots, et sa
// largeur reelle - elle depend des boutons choisis dans les reglages. // largeur reelle - elle depend des boutons choisis dans les reglages.
FLazyIcons: Boolean; FLazyIcons: Boolean;
// Fond de la barrette retire (cle de couleur) : seuls les boutons restent.
FLazyClear: Boolean;
FLazyW: Integer; FLazyW: Integer;
procedure LazyWindowHandler(var AMsg: TMessage); procedure LazyWindowHandler(var AMsg: TMessage);
// Corps extraits des raccourcis Ctrl+Shift+A et Ctrl+Shift+Q : la barrette // Corps extraits des raccourcis Ctrl+Shift+A et Ctrl+Shift+Q : la barrette
@@ -215,7 +217,7 @@ type
// it before we came out of the tray. False for callers that already decided // it before we came out of the tray. False for callers that already decided
// who should be focused (post-autofill hands it to the filled window). // who should be focused (post-autofill hands it to the filled window).
procedure MinimizeToTray(AClearClipboard: Boolean = True; procedure MinimizeToTray(AClearClipboard: Boolean = True;
AReturnForeground: Boolean = True); AReturnForeground: Boolean = True; ANotify: Boolean = True);
// Record the application currently in the foreground as the one to restore // Record the application currently in the foreground as the one to restore
// focus to when we go back to the tray. Ignores our own and shell windows. // focus to when we go back to the tray. Ignores our own and shell windows.
procedure CaptureForegroundOwner; procedure CaptureForegroundOwner;
@@ -446,6 +448,7 @@ const
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu
LAZY_MENU_SETTINGS = 2; LAZY_MENU_SETTINGS = 2;
LAZY_MENU_CLEAR = 3;
// LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres // LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres
// enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent, // enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent,
// et il faudrait donc suivre la souris sur les cinq fenetres pour recoller // et il faudrait donc suivre la souris sur les cinq fenetres pour recoller
@@ -469,6 +472,12 @@ const
LAZY_BTNS_KEY = 'lazyButtons'; LAZY_BTNS_KEY = 'lazyButtons';
LAZY_BTNS_DEFAULT = 'full,pwd,totp,add,search,open,settings'; LAZY_BTNS_DEFAULT = 'full,pwd,totp,add,search,open,settings';
LAZY_ICONS_KEY = 'lazyIcons'; LAZY_ICONS_KEY = 'lazyIcons';
// FOND TRANSPARENT : peint dans une couleur qu'aucun theme n'emploie, puis
// declaree cle (LWA_COLORKEY) - ces pixels ne sont plus dessines du tout, et
// les clics y TRAVERSENT. On traine donc la barrette par ses boutons
// (DragDetect), le fond n'etant plus la pour etre saisi.
LAZY_CLEAR_KEY = 'lazyClear';
LAZY_KEY_COLOR = $00FF00FF;
LAZY_H = 40; LAZY_H = 40;
// Palette de la barrette, en COLORREF ($00BBGGRR - les octets sont INVERSES // Palette de la barrette, en COLORREF ($00BBGGRR - les octets sont INVERSES
@@ -826,7 +835,7 @@ begin
end; end;
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True; procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True;
AReturnForeground: Boolean = True); AReturnForeground: Boolean = True; ANotify: Boolean = True);
var var
LFormHwnd, LAppHwnd: HWND; LFormHwnd, LAppHwnd: HWND;
begin begin
@@ -884,7 +893,9 @@ begin
// 3. First-time only: pop a balloon notification so the user knows the // 3. First-time only: pop a balloon notification so the user knows the
// app is still running in the tray (and didn't crash). Skipped when // app is still running in the tray (and didn't crash). Skipped when
// the user opted out via Settings. // the user opted out via Settings.
if (not FBalloonShown) and FShowNotifications then // ANotify=False : absence de quelques centaines de ms (scan du QR), la
// fenetre revient seule - l'annonce "toujours dans le tray" serait fausse.
if ANotify and (not FBalloonShown) and FShowNotifications then
begin begin
ShowFirstTimeBalloon; ShowFirstTimeBalloon;
FBalloonShown := True; FBalloonShown := True;
@@ -1267,15 +1278,22 @@ begin
end; end;
procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean); procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean);
var
LAlpha: Byte;
begin begin
if FLazyWnd = 0 then Exit; if FLazyWnd = 0 then Exit;
// TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les // TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les
// clics sur toute sa surface. C'est un allegement du regard, pas un passage. // clics sur toute sa surface. C'est un allegement du regard, pas un passage.
// Le pourcentage s'applique toujours aux boutons, fond retire ou non.
if AHover or (FLazyAlphaPct >= 100) then if AHover or (FLazyAlphaPct >= 100) then
SetLayeredWindowAttributes(FLazyWnd, 0, 255, LWA_ALPHA) LAlpha := 255
else else
SetLayeredWindowAttributes(FLazyWnd, 0, LAlpha := MulDiv(255, FLazyAlphaPct, 100);
MulDiv(255, FLazyAlphaPct, 100), LWA_ALPHA); if FLazyClear then
SetLayeredWindowAttributes(FLazyWnd, LAZY_KEY_COLOR, LAlpha,
LWA_ALPHA or LWA_COLORKEY)
else
SetLayeredWindowAttributes(FLazyWnd, 0, LAlpha, LWA_ALPHA);
end; end;
procedure TPMBridge.ShowLazyMenu; procedure TPMBridge.ShowLazyMenu;
@@ -1298,6 +1316,8 @@ begin
if FLazyIcons then LIconsLabel := 'Show text' else LIconsLabel := 'Show icons'; if FLazyIcons then LIconsLabel := 'Show text' else LIconsLabel := 'Show icons';
AppendMenu(LMenu, MF_SEPARATOR, 0, nil); AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
AppendMenu(LMenu, MF_STRING, LAZY_MENU_ICONS, PChar(LIconsLabel)); AppendMenu(LMenu, MF_STRING, LAZY_MENU_ICONS, PChar(LIconsLabel));
AppendMenu(LMenu, MF_STRING or IfThen(FLazyClear, MF_CHECKED, MF_UNCHECKED),
LAZY_MENU_CLEAR, 'Transparent background');
AppendMenu(LMenu, MF_STRING, LAZY_MENU_SETTINGS, 'Fill bar settings...'); AppendMenu(LMenu, MF_STRING, LAZY_MENU_SETTINGS, 'Fill bar settings...');
// LE BATTEMENT DOIT S'ARRETER PENDANT LE MENU. Il repose la barrette en // LE BATTEMENT DOIT S'ARRETER PENDANT LE MENU. Il repose la barrette en
@@ -1321,6 +1341,11 @@ begin
if Assigned(FOnTrayRestore) then FOnTrayRestore(); if Assigned(FOnTrayRestore) then FOnTrayRestore();
if Assigned(FOnFillBarSettings) then FOnFillBarSettings(); if Assigned(FOnFillBarSettings) then FOnFillBarSettings();
end end
else if LCmd = LAZY_MENU_CLEAR then
begin
if FLazyClear then SetPref(LAZY_CLEAR_KEY, '0') else SetPref(LAZY_CLEAR_KEY, '1');
ReloadLazyBar;
end
else if LCmd = LAZY_MENU_ICONS then else if LCmd = LAZY_MENU_ICONS then
begin begin
if FLazyIcons then SetPref(LAZY_ICONS_KEY, '0') else SetPref(LAZY_ICONS_KEY, '1'); if FLazyIcons then SetPref(LAZY_ICONS_KEY, '0') else SetPref(LAZY_ICONS_KEY, '1');
@@ -1403,6 +1428,14 @@ begin
WM_ERASEBKGND: WM_ERASEBKGND:
begin begin
GetClientRect(FLazyWnd, LRect); GetClientRect(FLazyWnd, LRect);
if FLazyClear then
begin
LBrush := CreateSolidBrush(LAZY_KEY_COLOR);
FillRect(HDC(AMsg.WParam), LRect, LBrush);
DeleteObject(LBrush);
AMsg.Result := 1;
Exit;
end;
LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_BG, LAZY_LITE_BG)); LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_BG, LAZY_LITE_BG));
FillRect(HDC(AMsg.WParam), LRect, LBrush); FillRect(HDC(AMsg.WParam), LRect, LBrush);
DeleteObject(LBrush); DeleteObject(LBrush);
@@ -1580,6 +1613,7 @@ begin
FLazyAlphaPct := LAZY_ALPHA_DEFAULT; FLazyAlphaPct := LAZY_ALPHA_DEFAULT;
FLazyIcons := GetPref(LAZY_ICONS_KEY) = '1'; FLazyIcons := GetPref(LAZY_ICONS_KEY) = '1';
FLazyClear := GetPref(LAZY_CLEAR_KEY) = '1';
LList := GetPref(LAZY_BTNS_KEY); LList := GetPref(LAZY_BTNS_KEY);
if LList = '' then LList := LAZY_BTNS_DEFAULT; if LList = '' then LList := LAZY_BTNS_DEFAULT;
InitCommonControls; InitCommonControls;
+125
View File
@@ -0,0 +1,125 @@
unit PM.ScreenQr;
// Lit un QR affiche a l'ecran - le secret 2FA qu'un site montre a l'activation.
// Capture de TOUS les ecrans (bureau virtuel) en memoire, jamais sur le disque,
// puis decodage par ZXing.Delphi (Apache 2.0, deja vendu pour l'APK dans
// android-client\ThirdParty\ZXing). Aucune selection de zone : le decodeur
// trouve le QR n'importe ou dans l'image.
//
// GDI pur et octets bruts, pas de TBitmap FMX : tout se fait dans un thread
// d'arriere-plan, et le decodage d'un bureau entier prend un moment.
interface
/// Texte du premier QR trouve a l'ecran, '' si aucun.
function ScanScreenForQr: string;
// Exposees pour le banc d'essai (mesure du temps de decodage).
function CaptureVirtualScreen(out AWidth, AHeight: Integer): TArray<Byte>;
function DecodeQrPixels(const APixels: TArray<Byte>; AW, AH: Integer;
ATryHarder: Boolean): string;
implementation
uses
Winapi.Windows, System.SysUtils, System.Generics.Collections,
ZXing.LuminanceSource, ZXing.RGBLuminanceSource, ZXing.HybridBinarizer,
ZXing.BinaryBitmap, ZXing.MultiFormatReader, ZXing.BarcodeFormat,
ZXing.DecodeHintType, ZXing.ReadResult;
const
CAPTURE_BLT = $40000000; // absente de Winapi.Windows ; inclut les fenetres superposees
function CaptureVirtualScreen(out AWidth, AHeight: Integer): TArray<Byte>;
var
LX, LY: Integer;
LScreenDC, LMemDC: HDC;
LBmp: HBITMAP;
LOld: HGDIOBJ;
LInfo: TBitmapInfo;
begin
Result := nil;
LX := GetSystemMetrics(SM_XVIRTUALSCREEN);
LY := GetSystemMetrics(SM_YVIRTUALSCREEN);
AWidth := GetSystemMetrics(SM_CXVIRTUALSCREEN);
AHeight := GetSystemMetrics(SM_CYVIRTUALSCREEN);
if (AWidth <= 0) or (AHeight <= 0) then Exit;
LScreenDC := GetDC(0);
LMemDC := CreateCompatibleDC(LScreenDC);
LBmp := CreateCompatibleBitmap(LScreenDC, AWidth, AHeight);
try
LOld := SelectObject(LMemDC, LBmp);
BitBlt(LMemDC, 0, 0, AWidth, AHeight, LScreenDC, LX, LY, SRCCOPY or CAPTURE_BLT);
SelectObject(LMemDC, LOld);
FillChar(LInfo, SizeOf(LInfo), 0);
LInfo.bmiHeader.biSize := SizeOf(TBitmapInfoHeader);
LInfo.bmiHeader.biWidth := AWidth;
LInfo.bmiHeader.biHeight := -AHeight; // negatif = de haut en bas
LInfo.bmiHeader.biPlanes := 1;
LInfo.bmiHeader.biBitCount := 32;
LInfo.bmiHeader.biCompression := BI_RGB;
SetLength(Result, AWidth * AHeight * 4);
if GetDIBits(LMemDC, LBmp, 0, AHeight, @Result[0], LInfo, DIB_RGB_COLORS) = 0 then
Result := nil;
finally
DeleteObject(LBmp);
DeleteDC(LMemDC);
ReleaseDC(0, LScreenDC);
end;
end;
function DecodeQrPixels(const APixels: TArray<Byte>; AW, AH: Integer;
ATryHarder: Boolean): string;
var
LSource: TLuminanceSource;
LBinarizer: THybridBinarizer;
LImage: TBinaryBitmap;
LReader: TMultiFormatReader;
LHints: TDictionary<TDecodeHintType, TObject>;
LFormats: TList<TBarcodeFormat>;
LRes: TReadResult;
begin
Result := '';
LFormats := TList<TBarcodeFormat>.Create;
LFormats.Add(TBarcodeFormat.QR_CODE);
LHints := TDictionary<TDecodeHintType, TObject>.Create;
LHints.Add(TDecodeHintType.POSSIBLE_FORMATS, LFormats);
if ATryHarder then LHints.Add(TDecodeHintType.TRY_HARDER, nil);
LReader := TMultiFormatReader.Create;
LReader.Hints := LHints;
// BGR32 et non BGRA32 : le 4e octet d'une capture GDI vaut 0, et la
// conversion BGRA le lirait comme une transparence totale - image blanche.
LSource := TRGBLuminanceSource.Create(APixels, AW, AH, TBitmapFormat.BGR32);
LBinarizer := THybridBinarizer.Create(LSource);
LImage := TBinaryBitmap.Create(LBinarizer);
LRes := nil;
try
try
LRes := LReader.decode(LImage, True);
if LRes <> nil then Result := Trim(LRes.Text);
except
Result := '';
end;
finally
LRes.Free;
LImage.Free;
LBinarizer.Free;
LSource.Free;
LReader.Free;
LFormats.Free;
LHints.Free;
end;
end;
function ScanScreenForQr: string;
var
LPixels: TArray<Byte>;
LW, LH: Integer;
begin
Result := '';
LPixels := CaptureVirtualScreen(LW, LH);
if LPixels <> nil then Result := DecodeQrPixels(LPixels, LW, LH, True);
end;
end.
+38 -1
View File
@@ -35,7 +35,7 @@ uses
FMX.TMSFNCEdgeWebBrowser, FMX.TMSFNCEdgeWebBrowser,
{$ENDIF} {$ENDIF}
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.Licence, PM.AutoStart, PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.Licence, PM.AutoStart,
PM.Favicon, DelphiZXingQRCode, PM.Favicon, DelphiZXingQRCode, PM.ScreenQr,
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
const const
// Bump on each release. Surfaced to JS via cmd://app/version, displayed // Bump on each release. Surfaced to JS via cmd://app/version, displayed
@@ -1347,6 +1347,7 @@ begin
LFb.AddPair('buttons', PM.UserPrefs.GetPref('lazyButtons')); LFb.AddPair('buttons', PM.UserPrefs.GetPref('lazyButtons'));
LFb.AddPair('icons', TJSONBool.Create(PM.UserPrefs.GetPref('lazyIcons') = '1')); LFb.AddPair('icons', TJSONBool.Create(PM.UserPrefs.GetPref('lazyIcons') = '1'));
LFb.AddPair('alpha', PM.UserPrefs.GetPref('lazyAlphaPct')); LFb.AddPair('alpha', PM.UserPrefs.GetPref('lazyAlphaPct'));
LFb.AddPair('clear', TJSONBool.Create(PM.UserPrefs.GetPref('lazyClear') = '1'));
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&Bridge.onFillBar)Bridge.onFillBar(' + LFb.ToJSON + ')'); 'if(window.Bridge&&Bridge.onFillBar)Bridge.onFillBar(' + LFb.ToJSON + ')');
finally finally
@@ -1359,6 +1360,8 @@ begin
PM.UserPrefs.SetPref('lazyButtons', GetParam('buttons')); PM.UserPrefs.SetPref('lazyButtons', GetParam('buttons'));
if GetParam('icons') = '1' then PM.UserPrefs.SetPref('lazyIcons', '1') if GetParam('icons') = '1' then PM.UserPrefs.SetPref('lazyIcons', '1')
else PM.UserPrefs.SetPref('lazyIcons', '0'); else PM.UserPrefs.SetPref('lazyIcons', '0');
if GetParam('clear') = '1' then PM.UserPrefs.SetPref('lazyClear', '1')
else PM.UserPrefs.SetPref('lazyClear', '0');
var LAlpha := StrToIntDef(GetParam('alpha'), 60); var LAlpha := StrToIntDef(GetParam('alpha'), 60);
if (LAlpha >= 25) and (LAlpha <= 100) then if (LAlpha >= 25) and (LAlpha <= 100) then
PM.UserPrefs.SetPref('lazyAlphaPct', IntToStr(LAlpha)); PM.UserPrefs.SetPref('lazyAlphaPct', IntToStr(LAlpha));
@@ -1390,6 +1393,40 @@ begin
// envoie tout texte a une recherche web (Google, Chrome) - le jeton serait // envoie tout texte a une recherche web (Google, Chrome) - le jeton serait
// parti chez Google a un appui pres. Un lien a schema propre n'a qu'une // parti chez Google a un appui pres. Un lien a schema propre n'a qu'une
// application pour l'ouvrir : la notre. // application pour l'ouvrir : la notre.
// QR 2FA affiche par un site : la fenetre s'efface le temps de la capture
// (elle recouvre souvent le QR), puis revient. Seul un lien otpauth:// part
// vers la page - le reste de l'ecran ne quitte pas la memoire de l'exe.
else if ACmd = 'qr/screen' then
begin
LogLine('qr/screen: scanning');
FBridge.MinimizeToTray(False, False, False);
TThread.CreateAnonymousThread(
procedure
var
LText: string;
begin
Sleep(400); // laisser Windows retirer la fenetre de l'ecran
LText := ScanScreenForQr;
if Pos('otpauth://', LowerCase(LText)) <> 1 then LText := '';
TThread.Queue(nil,
procedure
var
LJs: TJSONString;
begin
RestoreWindowFromTray;
if LText = '' then LogLine('qr/screen: no otpauth QR found')
else LogLine('qr/screen: otpauth QR found');
LJs := TJSONString.Create(LText);
try
WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&Bridge.onScreenQr)Bridge.onScreenQr(' + LJs.ToJSON + ')');
finally
LJs.Free;
end;
end);
end).Start;
end
else if ACmd = 'sync/qr' then else if ACmd = 'sync/qr' then
begin begin
var LQrUrl := PM.UserPrefs.GetPref('syncUrl'); var LQrUrl := PM.UserPrefs.GetPref('syncUrl');
+11
View File
@@ -47,6 +47,7 @@
<symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol> <symbol id="i-printer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></symbol>
<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol> <symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></symbol>
<symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol> <symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol>
<symbol id="i-qr" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM21 14v.01M14 21h.01M17 21h4v-4"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol> <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none"> <symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/> <circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
@@ -785,6 +786,16 @@
<option value="25">25%</option> <option value="25">25%</option>
</select> </select>
</div> </div>
<div class="setting-row">
<span>
Transparent background
<small class="setting-hint">Only the buttons stay visible. Drag the bar by a button.</small>
</span>
<label class="toggle">
<input type="checkbox" id="fbClear">
<span class="toggle-slider"></span>
</label>
</div>
</div> </div>
<div class="slideover-field"> <div class="slideover-field">
+22 -2
View File
@@ -4833,6 +4833,24 @@ function soTotpField(plainSecret) {
}); });
row.appendChild(input); row.appendChild(input);
row.appendChild(toggle); row.appendChild(toggle);
// Lit le QR qu'affiche le site a l'activation de la 2FA : l'exe s'efface,
// capture l'ecran et ne renvoie qu'un lien otpauth:// (qr/screen).
if (Bridge.active) {
const scan = el('button', { class: 'icon-btn icon-btn-sm', type: 'button', title: 'Scan the 2FA QR code shown on screen' });
scan.appendChild(icon('i-qr'));
scan.addEventListener('click', () => {
Bridge.onScreenQr = (text) => {
const secret = parseOtpAuthUri(text || '');
if (!secret) { toast('No 2FA QR code found on screen', 'warning'); return; }
input.value = secret;
updateTotpDisplay();
soDirtyCheck();
toast('2FA secret read from the QR code');
};
window.location.href = 'cmd://qr/screen';
});
row.appendChild(scan);
}
row.appendChild(clear); row.appendChild(clear);
wrap.appendChild(row); wrap.appendChild(row);
@@ -6710,6 +6728,7 @@ Bridge.onFillBar = function (st) {
$$('.fb-btn').forEach(c => { c.checked = keys.includes(c.dataset.key); }); $$('.fb-btn').forEach(c => { c.checked = keys.includes(c.dataset.key); });
$('#fbStyle').value = st.icons ? 'icons' : 'text'; $('#fbStyle').value = st.icons ? 'icons' : 'text';
$('#fbAlpha').value = String(parseInt(st.alpha, 10) || 60); $('#fbAlpha').value = String(parseInt(st.alpha, 10) || 60);
$('#fbClear').checked = !!st.clear;
}; };
// Clic droit sur la barrette -> "Fill bar settings..." : l'exe a deja ramene la // Clic droit sur la barrette -> "Fill bar settings..." : l'exe a deja ramene la
// fenetre. Les reglages s'ouvrent PAR-DESSUS une entree en cours d'edition, // fenetre. Les reglages s'ouvrent PAR-DESSUS une entree en cours d'edition,
@@ -6728,7 +6747,8 @@ function fillBarPush() {
+ '?on=' + ($('#fbOn').checked ? '1' : '0') + '?on=' + ($('#fbOn').checked ? '1' : '0')
+ '&buttons=' + encodeURIComponent(keys.join(',') || 'none') + '&buttons=' + encodeURIComponent(keys.join(',') || 'none')
+ '&icons=' + ($('#fbStyle').value === 'icons' ? '1' : '0') + '&icons=' + ($('#fbStyle').value === 'icons' ? '1' : '0')
+ '&alpha=' + encodeURIComponent($('#fbAlpha').value); + '&alpha=' + encodeURIComponent($('#fbAlpha').value)
+ '&clear=' + ($('#fbClear').checked ? '1' : '0');
} }
function openSettings() { function openSettings() {
@@ -8242,7 +8262,7 @@ async function init() {
if (syncTest) syncTest.addEventListener('click', syncTestConnection); if (syncTest) syncTest.addEventListener('click', syncTestConnection);
const syncQr = document.getElementById('syncQrBtn'); const syncQr = document.getElementById('syncQrBtn');
if (syncQr) syncQr.addEventListener('click', syncQrToggle); if (syncQr) syncQr.addEventListener('click', syncQrToggle);
['#fbOn', '#fbStyle', '#fbAlpha'].forEach(sel => { ['#fbOn', '#fbStyle', '#fbAlpha', '#fbClear'].forEach(sel => {
const el = $(sel); const el = $(sel);
if (el) el.addEventListener('change', fillBarPush); if (el) el.addEventListener('change', fillBarPush);
}); });