Compare commits
7 Commits
a0527eb806
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 092eb2f73a | |||
| bf7a8f4d38 | |||
| 9999dd113d | |||
| a3e63ee8f1 | |||
| 6397ed008a | |||
| b1c0c1e11b | |||
| f112de508b |
@@ -316,6 +316,16 @@ Détails :
|
|||||||
Le cadre est peint dans `WM_ERASEBKGND` plutôt que par `WS_BORDER`, qui se
|
Le cadre est peint dans `WM_ERASEBKGND` plutôt que par `WS_BORDER`, qui se
|
||||||
sert dans les couleurs système.
|
sert dans les couleurs système.
|
||||||
|
|
||||||
|
**Bouton Paste (2026-10-14)** : `LAZY_ID_PASTE` / cle `paste` (desactive par
|
||||||
|
defaut) → `PasteClipboardAsKeystrokes` lit le presse-papiers **Windows**
|
||||||
|
(`CF_UNICODETEXT`) et le **tape** dans la fenetre en avant par
|
||||||
|
`ExecuteAutofill(..., AClearFirst=False)` - donc vraies touches et bascule de
|
||||||
|
disposition vers un client VNC (TigerVNC vers un Mac ne transmet pas le
|
||||||
|
presse-papiers). Insere au curseur, saut de ligne final retire (un terminal
|
||||||
|
lancerait la commande), sauts internes tapes en Entree. Marche coffre ferme :
|
||||||
|
rien ne passe par la page. Pas de texte (image, fichier) → bulle « No text in
|
||||||
|
the clipboard ».
|
||||||
|
|
||||||
### Réglages de la barrette (2026-09-18)
|
### Réglages de la barrette (2026-09-18)
|
||||||
|
|
||||||
**Settings → onglet Fill bar**, et le **clic droit** sur la barrette pour les
|
**Settings → onglet Fill bar**, et le **clic droit** sur la barrette pour les
|
||||||
@@ -1443,6 +1453,25 @@ d'accord.
|
|||||||
|
|
||||||
### Contraintes connues autofill
|
### Contraintes connues autofill
|
||||||
|
|
||||||
|
- **VNC (2026-10-14)** : les caracteres partent en `KEYEVENTF_UNICODE`
|
||||||
|
(`VK_PACKET`), que TigerVNC & co ne savent pas retransmettre - un Mac
|
||||||
|
recevait un `V` par caractere. Cible dont l'exe contient « vnc »
|
||||||
|
(`TargetTypesByKeyCodes`) → **vraies touches** (`SendKeyCodeString` :
|
||||||
|
`VkKeyScanEx` dans la disposition de la cible, + Maj/Ctrl/Alt, AltGr =
|
||||||
|
Ctrl+Alt). **AltGr = `VK_LCONTROL` + `VK_RMENU` etendu**, comme le vrai
|
||||||
|
clavier : un Ctrl+Alt generique etait lu par TigerVNC comme deux
|
||||||
|
modificateurs, et `@` (AltGr+0 en AZERTY) arrivait en `a`. Chaque touche porte
|
||||||
|
aussi son scancode (`MapVirtualKeyEx`). **Texte melange (arabe + latin)** :
|
||||||
|
un caractere sans touche dans la disposition courante est cherche dans les
|
||||||
|
autres dispositions installees (`GetKeyboardLayoutList`) ; la fenetre cible
|
||||||
|
y est basculee (`WM_INPUTLANGCHANGEREQUEST`, attente que son fil l'ait
|
||||||
|
prise - TigerVNC traduit chaque touche avec la disposition de SON fil), et
|
||||||
|
la disposition d'origine est rendue a la fin. Seul un caractere qu'aucune
|
||||||
|
disposition installee ne sait taper est **saute et compte** - journal
|
||||||
|
`N character(s) have no key on this keyboard and were NOT typed - copy
|
||||||
|
instead`. Eprouve vers un Mac : `تاسيب... jhbs... @ #` complet, clavier
|
||||||
|
francais comme arabe actif.
|
||||||
|
|
||||||
- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin,
|
- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin,
|
||||||
regedit → autofill silencieusement no-op. Tester avec apps non-élevées.
|
regedit → autofill silencieusement no-op. Tester avec apps non-élevées.
|
||||||
- **Chrome password manager** : si Chrome a des credentials sauvegardés
|
- **Chrome password manager** : si Chrome a des credentials sauvegardés
|
||||||
@@ -1587,6 +1616,16 @@ après chaque `DELETE /attachments/{id}`. Un petit vault sain ne paie rien.
|
|||||||
(`ssh ••••••••••••.11`), une fin de 6 caracteres ou moins est masquee en
|
(`ssh ••••••••••••.11`), une fin de 6 caracteres ou moins est masquee en
|
||||||
entier. Affichage seulement : filtre, copie et remplissage voient le vrai
|
entier. Affichage seulement : filtre, copie et remplissage voient le vrai
|
||||||
texte. L'oeil re-declenche `input` sur le textarea pour redessiner la liste.
|
texte. L'oeil re-declenche `input` sur le textarea pour redessiner la liste.
|
||||||
|
**Lignes continuees** : une ligne finie par `\`, `` ` `` ou `^` se recolle a
|
||||||
|
la suivante (sans le caractere) - une seule commande, sur UNE ligne, car un
|
||||||
|
retour a la ligne tape dans un terminal vaut Entree. Ligne vide ou fin du
|
||||||
|
texte = fin de la commande ; un `#` apres une continuation reste du texte.
|
||||||
|
Limite : un chemin Windows fini par `\` en fin de ligne passe pour une
|
||||||
|
continuation.
|
||||||
|
- **Polices a chasse fixe et arabe** (2026-10-14) : aucune n'a l'arabe, et
|
||||||
|
Windows empruntait une police plus petite. Chaque pile `…, monospace` de
|
||||||
|
`css/style.css` finit par `'Segoe UI', Tahoma, monospace` : le latin reste a
|
||||||
|
chasse fixe, l'arabe prend une taille comparable.
|
||||||
- **Settings search** : `#settingsSearch` filtre les `.setting-row` par
|
- **Settings search** : `#settingsSearch` filtre les `.setting-row` par
|
||||||
texte ; match sur le label de section garde toute la section. Esc dans
|
texte ; match sur le label de section garde toute la section. Esc dans
|
||||||
la search avec query = clear ; Esc vide = ferme Settings.
|
la search avec query = clear ; Esc vide = ferme Settings.
|
||||||
|
|||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
Password Manager - installation
|
||||||
|
================================
|
||||||
|
|
||||||
|
Quatre fichiers dans ce dossier :
|
||||||
|
|
||||||
|
PMServer.exe l'application Windows - c'est elle qui compte
|
||||||
|
pmserverApk.apk l'application Android, FACULTATIVE (voir le point 7)
|
||||||
|
WEBDAV-LOCAL.md le guide pour synchroniser le telephone sans
|
||||||
|
depot en ligne, par ton propre PC (point 7)
|
||||||
|
GITHUB.md le guide pour synchroniser par un depot GitHub
|
||||||
|
prive, de partout (point 7)
|
||||||
|
|
||||||
|
Il n'y a pas d'installeur.
|
||||||
|
|
||||||
|
L'activation se fait en DEUX temps, parce que la licence est liee a
|
||||||
|
ta machine : tu m'envoies un code que l'application t'affiche, je te
|
||||||
|
renvoie ta cle. Compte un aller-retour avant de pouvoir t'en servir.
|
||||||
|
|
||||||
|
|
||||||
|
1. OU LE METTRE
|
||||||
|
|
||||||
|
Mets le dossier ou tu veux, SAUF dans "Program Files" :
|
||||||
|
l'application ecrit son coffre (vault.db) juste a cote de l'exe,
|
||||||
|
et Windows interdit l'ecriture dans Program Files.
|
||||||
|
|
||||||
|
Un bon endroit : C:\Users\<toi>\PasswordManager\
|
||||||
|
|
||||||
|
Choisis bien maintenant : deplacer l'exe plus tard deplace le coffre
|
||||||
|
avec lui, il faut penser a prendre le vault.db aussi.
|
||||||
|
|
||||||
|
|
||||||
|
2. PREMIER LANCEMENT - M'ENVOYER TON "INSTALLATION ID"
|
||||||
|
|
||||||
|
Double-clic sur PMServer.exe. Le premier ecran demande une cle
|
||||||
|
d'activation, que tu n'as pas encore. En BAS de cet ecran il y a un
|
||||||
|
code de seize caracteres, du genre :
|
||||||
|
|
||||||
|
Installation ID
|
||||||
|
CD6F-F1B3-25C4-0111
|
||||||
|
|
||||||
|
Envoie-moi ce code (photo de l'ecran, copier-coller, peu importe).
|
||||||
|
Il ne dit rien sur toi : c'est une empreinte de ta carte mere et de
|
||||||
|
ton disque, melangees et hachees. Il ne permet ni de t'identifier,
|
||||||
|
ni de remonter a ton materiel.
|
||||||
|
|
||||||
|
Je te renvoie ta cle. Tu la colles dans le champ, "Activate", et
|
||||||
|
c'est fini : elle n'est demandee qu'une fois.
|
||||||
|
|
||||||
|
- Un AUTRE compte Windows sur le meme PC la redemandera.
|
||||||
|
- Elle ne marchera QUE sur cette machine. Si tu changes de carte
|
||||||
|
mere ou de disque systeme, redemande-m'en une, c'est gratuit
|
||||||
|
et immediat.
|
||||||
|
|
||||||
|
|
||||||
|
3. CREER TON COFFRE
|
||||||
|
|
||||||
|
Ensuite, "Register" : tu choisis ton nom d'utilisateur et ton mot
|
||||||
|
de passe maitre.
|
||||||
|
|
||||||
|
LE MOT DE PASSE MAITRE N'EST STOCKE NULLE PART. Il n'y a aucun
|
||||||
|
moyen de le recuperer s'il est perdu - le coffre devient illisible,
|
||||||
|
y compris pour moi. Va dans Settings > Security des le premier jour
|
||||||
|
et genere un "recovery code", garde-le hors de l'ordinateur.
|
||||||
|
|
||||||
|
|
||||||
|
4. SI L'APPLICATION NE S'OUVRE PAS (fenetre blanche ou rien)
|
||||||
|
|
||||||
|
Il manque le moteur d'affichage de Microsoft, deja present sur
|
||||||
|
Windows 11 mais pas toujours sur Windows 10. Il est gratuit :
|
||||||
|
|
||||||
|
https://developer.microsoft.com/microsoft-edge/webview2/
|
||||||
|
|
||||||
|
-> "Evergreen Bootstrapper", telecharge, installe, relance l'app.
|
||||||
|
|
||||||
|
|
||||||
|
5. SAUVEGARDE
|
||||||
|
|
||||||
|
Tout le coffre tient dans vault.db, a cote de l'exe. Le copier
|
||||||
|
suffit a le sauvegarder. Settings > Backup fait une copie chiffree
|
||||||
|
automatique dans le dossier de ton choix - mets-le en route.
|
||||||
|
|
||||||
|
Si tu ranges ce dossier dans Google Drive, OneDrive ou Dropbox, la
|
||||||
|
sauvegarde part avec, et elle est chiffree : le service ne voit
|
||||||
|
qu'un fichier illisible.
|
||||||
|
|
||||||
|
|
||||||
|
6. AU QUOTIDIEN
|
||||||
|
|
||||||
|
- L'application vit dans la zone de notification (a cote de l'heure),
|
||||||
|
elle ne se ferme pas avec la croix. Clic droit sur l'icone > Quit.
|
||||||
|
- Ctrl+Shift+L remplit identifiant + mot de passe dans le navigateur :
|
||||||
|
clique d'abord le champ identifiant, puis la combinaison.
|
||||||
|
- Clic droit sur l'icone > "Fill bar" pour faire la meme chose
|
||||||
|
a la souris, sans raccourci. Settings > Fill bar (ou clic droit
|
||||||
|
sur la barrette) choisit ses boutons, icones ou texte, et sa
|
||||||
|
transparence (fond retire possible : seuls les boutons restent).
|
||||||
|
- Ctrl+Shift+Q ouvre une recherche rapide. Ouverte par ce raccourci
|
||||||
|
(ou le bouton Search de la barrette), elle remplit le champ ou tu
|
||||||
|
etais ; l'interrupteur Copy / Fill en haut permet de copier a la
|
||||||
|
place. Depuis l'icone du tray, elle copie seulement.
|
||||||
|
- Activer une 2FA sur un site : laisse son QR code a l'ecran, ouvre
|
||||||
|
l'entree, et clique le bouton QR du champ "Two-factor (TOTP)".
|
||||||
|
Le secret se remplit tout seul.
|
||||||
|
- Ouvert une entree par erreur ? Alt+fleche gauche (ou la fleche < en
|
||||||
|
haut du panneau) revient a la precedente, sans refaire la recherche.
|
||||||
|
Alt+fleche droite repart en avant.
|
||||||
|
- Un raccourci clavier gene une autre application ? Settings >
|
||||||
|
Autofill, clique le raccourci puis appuie sur Suppr : il passe a
|
||||||
|
"Off" et la combinaison redevient libre. La barrette continue de
|
||||||
|
remplir meme si les raccourcis sont coupes.
|
||||||
|
- NOTES DE COMMANDES (ssh, scripts...) : + > "Commands (note)", ou
|
||||||
|
coche "Command list" dans une note existante. Une commande par
|
||||||
|
ligne, et une ligne "# Titre" pour faire des groupes :
|
||||||
|
# Prod
|
||||||
|
ssh admin@10.0.0.11
|
||||||
|
ssh admin@10.0.0.12
|
||||||
|
Une commande trop longue peut continuer sur la ligne suivante si la
|
||||||
|
ligne finit par \ (ou ` en PowerShell, ^ en cmd).
|
||||||
|
Ctrl+Shift+Q, choisis la note, tape un bout de la commande, puis
|
||||||
|
Entree : elle est tapee dans ton terminal SANS etre lancee (c'est
|
||||||
|
toi qui valides). Ctrl+Entree la copie. Esc ou < revient aux
|
||||||
|
entrees. L'oeil de la note masque les commandes (partage d'ecran).
|
||||||
|
|
||||||
|
Rien ne sort de ton ordinateur : pas de compte en ligne, pas de
|
||||||
|
serveur. Tout est chiffre sur place.
|
||||||
|
|
||||||
|
|
||||||
|
7. LE TELEPHONE (pmserverApk.apk) - FACULTATIF
|
||||||
|
|
||||||
|
A LIRE AVANT D'INSTALLER : l'application Android ne sait pas creer
|
||||||
|
de coffre. Elle ne fait que reprendre celui du PC, et pour cela il
|
||||||
|
faut un endroit ou le PC publie une copie chiffree. Deux choix :
|
||||||
|
|
||||||
|
- UN WEBDAV LOCAL, sur ton propre PC : rien en ligne, pas de compte.
|
||||||
|
Le telephone se synchronise quand il est sur le meme Wi-Fi que le
|
||||||
|
PC, et que le PC est allume. Tout est explique pas a pas dans
|
||||||
|
WEBDAV-LOCAL.md - suis ce guide A LA PLACE des points a. et d.
|
||||||
|
ci-dessous.
|
||||||
|
- UN DEPOT PRIVE Gitea ou GitHub : la synchro marche de partout.
|
||||||
|
Un depot public est refuse : n'importe qui pourrait telecharger
|
||||||
|
ton coffre chiffre et tout son historique.
|
||||||
|
|
||||||
|
Sans l'un ou l'autre, l'application s'installe mais reste vide : ne
|
||||||
|
perds pas de temps avec elle, dis-le moi et on regarde ensemble.
|
||||||
|
|
||||||
|
Ce qui suit resume le depot Gitea / GitHub ; GITHUB.md le detaille
|
||||||
|
ecran par ecran (creer le depot, le jeton, regler PC et telephone).
|
||||||
|
|
||||||
|
Ce qu'il faut, une fois :
|
||||||
|
|
||||||
|
a. Sur le PC : Settings > Sync, renseigner l'adresse du depot et
|
||||||
|
le jeton, choisir un MOT DE PASSE DE SYNCHRO (different du mot
|
||||||
|
de passe maitre), puis "Sync now".
|
||||||
|
Adresse : celle du depot, telle que GitHub ou Gitea l'affiche,
|
||||||
|
par exemple https://github.com/<toi>/<depot>.git
|
||||||
|
Pour GitHub, un jeton "fine-grained" limite a ce depot, droit
|
||||||
|
Contents : Read and write. (GitLab n'est pas pris en charge.)
|
||||||
|
LE MOT DE PASSE DE SYNCHRO DOIT ETRE FORT (5 mots au hasard, ou
|
||||||
|
16 caracteres tires au hasard) : c'est lui seul qui protege la
|
||||||
|
copie qui part sur le depot.
|
||||||
|
|
||||||
|
b. Sur le telephone : autoriser l'installation d'applications
|
||||||
|
depuis cette source quand Android le demande, puis installer
|
||||||
|
pmserverApk.apk.
|
||||||
|
|
||||||
|
c. Sur le PC : Settings > Sync > "QR for phone". Un QR apparait
|
||||||
|
pour 60 secondes.
|
||||||
|
|
||||||
|
d. Sur le telephone : Settings > "Scan the QR shown on the PC",
|
||||||
|
viser le QR. L'adresse et le jeton se remplissent seuls.
|
||||||
|
Le QR ne contient PAS le mot de passe de synchro : tape-le a
|
||||||
|
la main, puis "Save", puis "Download vault".
|
||||||
|
|
||||||
|
Ensuite, "Unlock with" choisit comment le coffre s'ouvre sur le
|
||||||
|
telephone : le verrouillage d'ecran (empreinte, schema), un code
|
||||||
|
propre a l'application, ou le mot de passe de synchro.
|
||||||
|
|
||||||
|
- Si tu RETIRES le verrouillage d'ecran, Android detruit la cle
|
||||||
|
qui protegeait la copie locale. L'application demande alors le
|
||||||
|
mot de passe et rouvre le coffre : rien n'est perdu.
|
||||||
|
- Le telephone lit et modifie le coffre, et "Sync" fait l'aller-
|
||||||
|
retour avec le depot (ou le WebDAV). Le PC reste la reference.
|
||||||
|
- A l'ouverture, le telephone verifie en arriere-plan : si le PC a
|
||||||
|
publie du nouveau, le bouton Sync s'allume.
|
||||||
|
|
||||||
|
Pour la 2FA sur le telephone : dans une entree, le crayon, puis le
|
||||||
|
bouton QR du champ "2FA secret" ; vise le QR du site avec la camera.
|
||||||
|
|
||||||
|
Une note de commandes s'affiche sur le telephone commande par
|
||||||
|
commande, chacune avec son bouton copier.
|
||||||
|
|
||||||
|
La licence ne concerne pas le telephone : c'est l'application
|
||||||
|
Windows qui est licenciee.
|
||||||
@@ -37,6 +37,19 @@ function VaultToJson(AVault: TVault): string;
|
|||||||
/// Les lignes d'une entree, dans l'ordre d'affichage.
|
/// Les lignes d'une entree, dans l'ordre d'affichage.
|
||||||
function DetailToJson(const AEntry: TVaultEntry): string;
|
function DetailToJson(const AEntry: TVaultEntry): string;
|
||||||
|
|
||||||
|
type
|
||||||
|
TCommandItem = record
|
||||||
|
Group, Cmd: string;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// NOTE DE COMMANDES (modele "commands"), meme regle que le poste
|
||||||
|
/// (js/app.js parseCommandNote) : "# titre" ouvre un groupe, chaque autre ligne
|
||||||
|
/// non vide est une commande ; une ligne finie par \, ` ou ^ se recolle a la
|
||||||
|
/// suivante, sur UNE ligne. Une ligne vide ou la fin du texte termine la
|
||||||
|
/// commande en cours.
|
||||||
|
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
|
||||||
|
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
|
||||||
|
|
||||||
/// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la
|
/// Une ligne du detail par son rang, dans le MEME ordre que DetailToJson : la
|
||||||
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
|
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
|
||||||
/// Faux si le rang ne designe rien.
|
/// Faux si le rang ne designe rien.
|
||||||
@@ -245,6 +258,49 @@ begin
|
|||||||
Result := LRows;
|
Result := LRows;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function IsCommandNote(const AEntry: TVaultEntry): Boolean;
|
||||||
|
begin
|
||||||
|
Result := AEntry.IsNote and SameText(AEntry.Template, 'commands');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ParseCommandNote(const AText: string): TArray<TCommandItem>;
|
||||||
|
var
|
||||||
|
LGroup, LPending, LLine, LPart: string;
|
||||||
|
LCont: Boolean;
|
||||||
|
|
||||||
|
procedure Flush;
|
||||||
|
begin
|
||||||
|
if LPending <> '' then
|
||||||
|
begin
|
||||||
|
SetLength(Result, Length(Result) + 1);
|
||||||
|
Result[High(Result)].Group := LGroup;
|
||||||
|
Result[High(Result)].Cmd := LPending;
|
||||||
|
end;
|
||||||
|
LPending := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Result := nil;
|
||||||
|
LGroup := '';
|
||||||
|
LPending := '';
|
||||||
|
for LLine in AText.Replace(#13, '').Split([#10]) do
|
||||||
|
begin
|
||||||
|
var LTrim := LLine.Trim;
|
||||||
|
if LTrim = '' then begin Flush; Continue; end;
|
||||||
|
if (LPending = '') and LTrim.StartsWith('#') then
|
||||||
|
begin
|
||||||
|
LGroup := LTrim.TrimLeft(['#']).Trim;
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
LCont := CharInSet(LTrim[High(LTrim)], ['\', '`', '^']);
|
||||||
|
if LCont then LPart := Copy(LTrim, 1, Length(LTrim) - 1).TrimRight
|
||||||
|
else LPart := LTrim;
|
||||||
|
if LPending = '' then LPending := LPart else LPending := LPending + ' ' + LPart;
|
||||||
|
if not LCont then Flush;
|
||||||
|
end;
|
||||||
|
Flush;
|
||||||
|
end;
|
||||||
|
|
||||||
function DetailToJson(const AEntry: TVaultEntry): string;
|
function DetailToJson(const AEntry: TVaultEntry): string;
|
||||||
var
|
var
|
||||||
LRoot: TJSONObject;
|
LRoot: TJSONObject;
|
||||||
@@ -278,6 +334,21 @@ begin
|
|||||||
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
|
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
|
||||||
LArr.AddElement(LObj);
|
LArr.AddElement(LObj);
|
||||||
end;
|
end;
|
||||||
|
// La liste des commandes, pour l'affichage. La copie passe par un rang
|
||||||
|
// (copy?f=cmd&k=), comme les lignes : Delphi relit le clair chez lui.
|
||||||
|
if IsCommandNote(AEntry) then
|
||||||
|
begin
|
||||||
|
var LCmds := ParseCommandNote(AEntry.Password);
|
||||||
|
var LCArr := TJSONArray.Create;
|
||||||
|
LRoot.AddPair('cmds', LCArr);
|
||||||
|
for I := 0 to High(LCmds) do
|
||||||
|
begin
|
||||||
|
LObj := TJSONObject.Create;
|
||||||
|
LObj.AddPair('g', LCmds[I].Group);
|
||||||
|
LObj.AddPair('c', LCmds[I].Cmd);
|
||||||
|
LCArr.AddElement(LObj);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
Result := LRoot.ToJSON;
|
Result := LRoot.ToJSON;
|
||||||
finally
|
finally
|
||||||
LRoot.Free;
|
LRoot.Free;
|
||||||
|
|||||||
@@ -447,6 +447,22 @@ l'ouverture, écriture 204, puis synchro du poste qui reprend la modification.
|
|||||||
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
|
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
|
||||||
fermé malgré la règle.
|
fermé malgré la règle.
|
||||||
|
|
||||||
|
## Notes de commandes — 2026-10-10
|
||||||
|
|
||||||
|
Une note marquée **Commands** sur le poste (modèle `commands`) affiche en tête
|
||||||
|
un bloc *Commands* : une commande par ligne sous le titre de son groupe, avec
|
||||||
|
un bouton copier. Le texte entier reste dessous, dans *Content*.
|
||||||
|
|
||||||
|
- **Même découpage que le poste** : `ParseCommandNote` (PMA.WebUI) est le
|
||||||
|
portage de `parseCommandNote` (js/app.js) — `# titre` ouvre un groupe, une
|
||||||
|
ligne finie par `\`, `` ` `` ou `^` se recolle à la suivante. Les deux
|
||||||
|
doivent dire la même chose ; éprouvé sur les mêmes cas.
|
||||||
|
- **La copie passe par un rang** (`copy?f=cmd&k=`), comme les autres lignes :
|
||||||
|
Delphi relit la note et copie la commande.
|
||||||
|
- **Ni remplissage ni masquage** ici : pas de fenêtre cible, et le corps des
|
||||||
|
notes est déjà affiché en clair. Une note devient « Commands » depuis le
|
||||||
|
poste (case *Command list*) ; la synchro porte le modèle.
|
||||||
|
|
||||||
## Saisie : ce qui a résisté
|
## Saisie : ce qui a résisté
|
||||||
|
|
||||||
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
|
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
|
||||||
|
|||||||
@@ -279,6 +279,11 @@
|
|||||||
.field .val.multi { white-space: pre-wrap; word-break: normal;
|
.field .val.multi { white-space: pre-wrap; word-break: normal;
|
||||||
overflow-wrap: anywhere; }
|
overflow-wrap: anywhere; }
|
||||||
.field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; }
|
.field.totp .val { font-size: 30px; font-weight: 300; letter-spacing: 2px; }
|
||||||
|
/* Note de commandes : une commande par ligne, sous le titre de son groupe. */
|
||||||
|
.field.cmds .cmd-group { font-size: 12px; font-weight: 600; opacity: .7;
|
||||||
|
text-transform: uppercase; margin: 10px 0 2px; }
|
||||||
|
.field.cmds .cmd-line { padding: 4px 0; }
|
||||||
|
.field.cmds .val.cmd { font-family: monospace; font-size: 14px; }
|
||||||
.bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18);
|
.bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.18);
|
||||||
margin-top: 10px; overflow: hidden; }
|
margin-top: 10px; overflow: hidden; }
|
||||||
.bar i { display: block; height: 100%; background: var(--text);
|
.bar i { display: block; height: 100%; background: var(--text);
|
||||||
@@ -1089,6 +1094,23 @@ function renderDetail(d) {
|
|||||||
+ '<button class="btn ico-btn" data-copy="totp">'
|
+ '<button class="btn ico-btn" data-copy="totp">'
|
||||||
+ ico('copy', 'Copy code') + '</button></div>'
|
+ ico('copy', 'Copy code') + '</button></div>'
|
||||||
+ '<div class="bar"><i id="totpb"></i></div></div>';
|
+ '<div class="bar"><i id="totpb"></i></div></div>';
|
||||||
|
// NOTE DE COMMANDES : une ligne par commande, groupees sous leur titre, avec
|
||||||
|
// un bouton copier. Le texte entier reste plus bas, dans "Content".
|
||||||
|
if (d.cmds && d.cmds.length) {
|
||||||
|
var lastG = null;
|
||||||
|
html += '<div class="field cmds"><div class="cap">Commands</div>';
|
||||||
|
for (i = 0; i < d.cmds.length; i++) {
|
||||||
|
var cm = d.cmds[i];
|
||||||
|
if (cm.g !== lastG) {
|
||||||
|
lastG = cm.g;
|
||||||
|
if (cm.g) html += '<div class="cmd-group">' + esc(cm.g) + '</div>';
|
||||||
|
}
|
||||||
|
html += '<div class="line cmd-line"><div class="val cmd">' + esc(cm.c) + '</div>'
|
||||||
|
+ '<button class="btn ico-btn" data-copy="cmd" data-k="' + i + '">'
|
||||||
|
+ ico('copy', 'Copy command') + '</button></div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
for (i = 0; i < detailRows.length; i++) {
|
for (i = 0; i < detailRows.length; i++) {
|
||||||
var r = detailRows[i];
|
var r = detailRows[i];
|
||||||
html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>'
|
html += '<div class="field"><div class="cap">' + esc(r.c) + '</div>'
|
||||||
@@ -1170,6 +1192,7 @@ document.getElementById('fields').addEventListener('click', function (ev) {
|
|||||||
var copy = t.getAttribute('data-copy');
|
var copy = t.getAttribute('data-copy');
|
||||||
if (copy === 'totp') { send('copy?f=totp'); return; }
|
if (copy === 'totp') { send('copy?f=totp'); return; }
|
||||||
if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; }
|
if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; }
|
||||||
|
if (copy === 'cmd') { send('copy?f=cmd&k=' + t.getAttribute('data-k')); return; }
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+21
-17
@@ -17,10 +17,14 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Les polices a chasse fixe n'ont pas l'arabe : Windows empruntait une police
|
||||||
|
au hasard, plus petite. 'Segoe UI' / Tahoma, places avant 'monospace' dans
|
||||||
|
chaque pile, prennent ces caracteres a une taille comparable ; le latin reste
|
||||||
|
dans la police a chasse fixe. */
|
||||||
button { font-family: inherit; cursor: pointer; }
|
button { font-family: inherit; cursor: pointer; }
|
||||||
input, select, textarea { font-family: inherit; font-size: inherit; }
|
input, select, textarea { font-family: inherit; font-size: inherit; }
|
||||||
kbd {
|
kbd {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -248,7 +252,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.licence-machine code {
|
.licence-machine code {
|
||||||
display: inline-block; padding: 6px 10px;
|
display: inline-block; padding: 6px 10px;
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
font-family: ui-monospace, "Cascadia Mono", Consolas, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 14px; letter-spacing: 1px; color: var(--text);
|
font-size: 14px; letter-spacing: 1px; color: var(--text);
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
@@ -1216,7 +1220,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
}
|
}
|
||||||
.entry-pw {
|
.entry-pw {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1268,7 +1272,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
}
|
}
|
||||||
.totp-code {
|
.totp-code {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -1330,7 +1334,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
}
|
}
|
||||||
.auth-card-code {
|
.auth-card-code {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -1386,7 +1390,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.health-score.is-danger { border-color: #ef4444; color: #ef4444; }
|
.health-score.is-danger { border-color: #ef4444; color: #ef4444; }
|
||||||
.health-score-num {
|
.health-score-num {
|
||||||
font-size: 36px; font-weight: 700;
|
font-size: 36px; font-weight: 700;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
}
|
}
|
||||||
.health-score-lbl {
|
.health-score-lbl {
|
||||||
font-size: 11px; font-weight: 600;
|
font-size: 11px; font-weight: 600;
|
||||||
@@ -1417,7 +1421,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.health-bd-n { text-align: right; font-variant-numeric: tabular-nums; }
|
.health-bd-n { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
.health-bd-pts {
|
.health-bd-pts {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
/* Nothing wrong in this category — dim it so the eye goes to what needs work. */
|
/* Nothing wrong in this category — dim it so the eye goes to what needs work. */
|
||||||
@@ -1572,7 +1576,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.history-date {
|
.history-date {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
}
|
}
|
||||||
.history-preview {
|
.history-preview {
|
||||||
display: flex; align-items: flex-start; gap: 6px;
|
display: flex; align-items: flex-start; gap: 6px;
|
||||||
@@ -1581,7 +1585,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.history-value {
|
.history-value {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
/* Long revealed passwords wrap to multiple lines (no ellipsis) so
|
/* Long revealed passwords wrap to multiple lines (no ellipsis) so
|
||||||
@@ -1634,7 +1638,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cheatsheet-keys kbd {
|
.cheatsheet-keys kbd {
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -1814,7 +1818,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.audit-row:hover { background: var(--bg); }
|
.audit-row:hover { background: var(--bg); }
|
||||||
.audit-date {
|
.audit-date {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.audit-action {
|
.audit-action {
|
||||||
@@ -1823,7 +1827,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
}
|
}
|
||||||
.audit-ip {
|
.audit-ip {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1969,7 +1973,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.so-note-body {
|
.so-note-body {
|
||||||
min-height: 220px;
|
min-height: 220px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
font-family: 'JetBrains Mono', ui-monospace, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -1987,7 +1991,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.so-cmds-cmd { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
.so-cmds-cmd { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||||
.so-cmds-empty { font-size: 12px; color: var(--text-dim); }
|
.so-cmds-empty { font-size: 12px; color: var(--text-dim); }
|
||||||
#qsBack { flex-shrink: 0; }
|
#qsBack { flex-shrink: 0; }
|
||||||
.qs-cmd { font-family: ui-monospace, Consolas, monospace; font-size: 13px; }
|
.qs-cmd { font-family: ui-monospace, Consolas, 'Segoe UI', Tahoma, monospace; font-size: 13px; }
|
||||||
|
|
||||||
/* ---- + New dropdown ---------------------------------- */
|
/* ---- + New dropdown ---------------------------------- */
|
||||||
.new-entry-wrap {
|
.new-entry-wrap {
|
||||||
@@ -2543,7 +2547,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -2780,7 +2784,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
|||||||
|
|
||||||
/* ---- Hotkey capture buttons (Settings → Autofill) ---- */
|
/* ---- Hotkey capture buttons (Settings → Autofill) ---- */
|
||||||
.hotkey-capture-btn {
|
.hotkey-capture-btn {
|
||||||
font-family: ui-monospace, "Cascadia Code", Menlo, monospace;
|
font-family: ui-monospace, "Cascadia Code", Menlo, 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -3240,7 +3244,7 @@ input[type="password"]::-ms-clear {
|
|||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
.prc-code {
|
.prc-code {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', 'Segoe UI', Tahoma, monospace;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
letter-spacing: 6px;
|
letter-spacing: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ type
|
|||||||
// les declenche aussi, meme capture de la fenetre cible.
|
// les declenche aussi, meme capture de la fenetre cible.
|
||||||
procedure FireNewEntryRequest;
|
procedure FireNewEntryRequest;
|
||||||
procedure FireQuickSearchRequest;
|
procedure FireQuickSearchRequest;
|
||||||
|
procedure PasteClipboardAsKeystrokes;
|
||||||
/// Pose l'opacite qui convient a l'etat de survol donne.
|
/// Pose l'opacite qui convient a l'etat de survol donne.
|
||||||
procedure ApplyLazyAlpha(AHover: Boolean);
|
procedure ApplyLazyAlpha(AHover: Boolean);
|
||||||
/// Menu du clic droit sur la barrette : le choix de l'opacite au repos.
|
/// Menu du clic droit sur la barrette : le choix de l'opacite au repos.
|
||||||
@@ -450,6 +451,7 @@ const
|
|||||||
LAZY_ID_ADD = 106; // nouvelle entree depuis la fenetre en avant (Ctrl+Shift+A)
|
LAZY_ID_ADD = 106; // nouvelle entree depuis la fenetre en avant (Ctrl+Shift+A)
|
||||||
LAZY_ID_SEARCH = 107; // recherche rapide (Ctrl+Shift+Q)
|
LAZY_ID_SEARCH = 107; // recherche rapide (Ctrl+Shift+Q)
|
||||||
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
|
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
|
||||||
|
LAZY_ID_PASTE = 109; // tape le presse-papiers Windows dans la fenetre en avant
|
||||||
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;
|
LAZY_MENU_CLEAR = 3;
|
||||||
@@ -1116,6 +1118,59 @@ end;
|
|||||||
// The target of a fill is whatever window owns the foreground at the moment
|
// The target of a fill is whatever window owns the foreground at the moment
|
||||||
// the user asks for it - a shortcut, or a click on the lazy strip, which takes
|
// the user asks for it - a shortcut, or a click on the lazy strip, which takes
|
||||||
// no foreground precisely so this stays true.
|
// no foreground precisely so this stays true.
|
||||||
|
/// COLLER EN TAPANT. Certains clients a distance (TigerVNC vers un Mac) ne
|
||||||
|
/// transmettent pas le presse-papiers Windows : Ctrl+V n'y colle rien. Le texte
|
||||||
|
/// est donc TAPE dans la fenetre en avant, par le meme chemin que l'autofill -
|
||||||
|
/// vraies touches pour un client VNC, dispositions basculees au besoin. Le
|
||||||
|
/// presse-papiers est celui de Windows : coffre ouvert ou non, rien ne passe
|
||||||
|
/// par la page. Pas de Ctrl+A + Suppr avant : on insere au curseur, comme un
|
||||||
|
/// collage.
|
||||||
|
procedure TPMBridge.PasteClipboardAsKeystrokes;
|
||||||
|
var
|
||||||
|
LTarget: HWND;
|
||||||
|
LData: THandle;
|
||||||
|
LPtr: PWideChar;
|
||||||
|
LText: string;
|
||||||
|
begin
|
||||||
|
LTarget := GetForegroundWindow;
|
||||||
|
if IsOwnProcessWindow(LTarget) then
|
||||||
|
begin
|
||||||
|
ShowBalloon('Nothing to paste into',
|
||||||
|
'Click where the text should go first, then press Paste.', True);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
LText := '';
|
||||||
|
if OpenClipboard(0) then
|
||||||
|
try
|
||||||
|
LData := GetClipboardData(CF_UNICODETEXT);
|
||||||
|
if LData <> 0 then
|
||||||
|
begin
|
||||||
|
LPtr := GlobalLock(LData);
|
||||||
|
if LPtr <> nil then
|
||||||
|
try
|
||||||
|
LText := LPtr;
|
||||||
|
finally
|
||||||
|
GlobalUnlock(LData);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
CloseClipboard;
|
||||||
|
end;
|
||||||
|
// Un retour a la ligne final, tape dans un terminal, vaudrait Entree.
|
||||||
|
// Les sauts de ligne internes partent en #13 : Entree, comme un collage.
|
||||||
|
LText := LText.Replace(#13#10, #10).Replace(#13, #10).TrimRight([#10])
|
||||||
|
.Replace(#10, #13);
|
||||||
|
if LText = '' then
|
||||||
|
begin
|
||||||
|
// Une image ou un fichier copies ne sont pas du texte : le dire tel quel.
|
||||||
|
ShowBalloon('No text in the clipboard', 'Copy some text first, then press Paste.', True);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if not ExecuteAutofill(LTarget, '', LText, False, False, False) then
|
||||||
|
ShowBalloon('Paste blocked',
|
||||||
|
'The window did not accept the keystrokes (it may run as administrator).', True);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TPMBridge.FireAutofillRequest(AKind: TAutofillKind;
|
procedure TPMBridge.FireAutofillRequest(AKind: TAutofillKind;
|
||||||
AFromBar: Boolean = False);
|
AFromBar: Boolean = False);
|
||||||
var
|
var
|
||||||
@@ -1302,6 +1357,7 @@ begin
|
|||||||
LAZY_ID_SEARCH: LGlyph := #$E721; // Search
|
LAZY_ID_SEARCH: LGlyph := #$E721; // Search
|
||||||
LAZY_ID_OPEN: LGlyph := #$E8A7; // OpenInNewWindow
|
LAZY_ID_OPEN: LGlyph := #$E8A7; // OpenInNewWindow
|
||||||
LAZY_ID_SETTINGS: LGlyph := #$E713; // Settings (roue)
|
LAZY_ID_SETTINGS: LGlyph := #$E713; // Settings (roue)
|
||||||
|
LAZY_ID_PASTE: LGlyph := #$E77F; // Paste
|
||||||
else
|
else
|
||||||
LGlyph := #$E711; // Cancel (X)
|
LGlyph := #$E711; // Cancel (X)
|
||||||
end;
|
end;
|
||||||
@@ -1505,6 +1561,7 @@ begin
|
|||||||
LAZY_ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
LAZY_ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||||||
LAZY_ID_ADD: FireNewEntryRequest;
|
LAZY_ID_ADD: FireNewEntryRequest;
|
||||||
LAZY_ID_SEARCH: FireQuickSearchRequest;
|
LAZY_ID_SEARCH: FireQuickSearchRequest;
|
||||||
|
LAZY_ID_PASTE: PasteClipboardAsKeystrokes;
|
||||||
LAZY_ID_SETTINGS:
|
LAZY_ID_SETTINGS:
|
||||||
begin
|
begin
|
||||||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||||||
@@ -1669,6 +1726,7 @@ begin
|
|||||||
AddButton('full', LAZY_ID_FULL, 'Full', 'Fill username and password', 56);
|
AddButton('full', LAZY_ID_FULL, 'Full', 'Fill username and password', 56);
|
||||||
AddButton('pwd', LAZY_ID_PWD, 'Password', 'Fill the password only', 76);
|
AddButton('pwd', LAZY_ID_PWD, 'Password', 'Fill the password only', 76);
|
||||||
AddButton('totp', LAZY_ID_TOTP, '2FA', 'Fill the 2FA code', 52);
|
AddButton('totp', LAZY_ID_TOTP, '2FA', 'Fill the 2FA code', 52);
|
||||||
|
AddButton('paste', LAZY_ID_PASTE, 'Paste', 'Type the Windows clipboard into the current window', 52);
|
||||||
AddButton('add', LAZY_ID_ADD, 'Add', 'New entry from the current window', 48);
|
AddButton('add', LAZY_ID_ADD, 'Add', 'New entry from the current window', 48);
|
||||||
AddButton('search', LAZY_ID_SEARCH, 'Search', 'Quick search', 60);
|
AddButton('search', LAZY_ID_SEARCH, 'Search', 'Quick search', 60);
|
||||||
// Meme raison que le X : quand la barrette est allumee, elle est souvent la
|
// Meme raison que le X : quand la barrette est allumee, elle est souvent la
|
||||||
@@ -2076,6 +2134,157 @@ begin
|
|||||||
SendInput(LCount, @LInputs[0], SizeOf(TInput));
|
SendInput(LCount, @LInputs[0], SizeOf(TInput));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function QueryFullProcessImageNameW(hProcess: THandle; dwFlags: DWORD;
|
||||||
|
lpExeName: PWideChar; var lpdwSize: DWORD): BOOL; stdcall;
|
||||||
|
external kernel32 name 'QueryFullProcessImageNameW';
|
||||||
|
|
||||||
|
/// UN VISUALISEUR VNC NE SAIT PAS TAPER DE L'UNICODE. SendUnicodeString envoie
|
||||||
|
/// chaque caractere en VK_PACKET ; une fenetre Windows le comprend, mais VNC
|
||||||
|
/// (TigerVNC, RealVNC, TightVNC, UltraVNC) traduit les VRAIES touches vers la
|
||||||
|
/// machine distante et ne sait que faire de VK_PACKET : un Mac recevait
|
||||||
|
/// "VVVVVV", un V par caractere. Pour lui on tape les touches du clavier.
|
||||||
|
/// ponytail: reconnu au nom de l'exe ("vnc") ; un autre client a distance qui
|
||||||
|
/// aurait le meme defaut s'ajouterait ici, ou deviendrait un reglage.
|
||||||
|
function TargetTypesByKeyCodes(AHwnd: HWND): Boolean;
|
||||||
|
const
|
||||||
|
PROCESS_QUERY_LIMITED_INFORMATION = $1000;
|
||||||
|
var
|
||||||
|
LPid, LLen: DWORD;
|
||||||
|
LProc: THandle;
|
||||||
|
LName: array[0..MAX_PATH] of WideChar;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
LPid := 0;
|
||||||
|
GetWindowThreadProcessId(AHwnd, LPid);
|
||||||
|
if LPid = 0 then Exit;
|
||||||
|
LProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, LPid);
|
||||||
|
if LProc = 0 then Exit;
|
||||||
|
try
|
||||||
|
LLen := Length(LName);
|
||||||
|
if QueryFullProcessImageNameW(LProc, 0, @LName[0], LLen) then
|
||||||
|
Result := ExtractFileName(string(LName)).ToLower.Contains('vnc');
|
||||||
|
finally
|
||||||
|
CloseHandle(LProc);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Le texte en VRAIES touches, dans la disposition clavier de la cible : chaque
|
||||||
|
/// caractere devient sa touche plus Maj / Ctrl / Alt selon VkKeyScanEx, avec
|
||||||
|
/// son scancode (certains clients VNC lisent la position physique).
|
||||||
|
/// ALTGR = Ctrl GAUCHE + Alt DROIT (touche etendue), comme le clavier le
|
||||||
|
/// produit : un Ctrl+Alt generique etait lu par TigerVNC comme deux
|
||||||
|
/// modificateurs ordinaires, et "@" (AltGr+0 en AZERTY) arrivait en "a".
|
||||||
|
/// TEXTE MELANGE (arabe + latin) : un caractere sans touche dans la
|
||||||
|
/// disposition courante est cherche dans les AUTRES dispositions installees
|
||||||
|
/// sur le poste ; on y bascule la fenetre cible (TigerVNC traduit chaque touche
|
||||||
|
/// avec la disposition de SON fil), on tape, et la disposition d'origine est
|
||||||
|
/// rendue a la fin. Seul un caractere qu'aucune disposition installee ne sait
|
||||||
|
/// taper est saute et compte - la copie, que TigerVNC synchronise, reste alors
|
||||||
|
/// le recours. Rend le nombre de sautes.
|
||||||
|
function SwitchTargetLayout(ATarget: HWND; ALayout: HKL): Boolean;
|
||||||
|
var
|
||||||
|
LThread: DWORD;
|
||||||
|
begin
|
||||||
|
LThread := GetWindowThreadProcessId(ATarget, nil);
|
||||||
|
PostMessage(ATarget, WM_INPUTLANGCHANGEREQUEST, 0, LPARAM(ALayout));
|
||||||
|
// La bascule est asynchrone : on attend qu'elle soit faite avant de taper,
|
||||||
|
// sinon les premieres touches partiraient encore dans l'ancienne.
|
||||||
|
for var I := 1 to 30 do
|
||||||
|
begin
|
||||||
|
if GetKeyboardLayout(LThread) = ALayout then Exit(True);
|
||||||
|
Sleep(10);
|
||||||
|
end;
|
||||||
|
Result := GetKeyboardLayout(LThread) = ALayout;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function LayoutForChar(ACh: WideChar; ACurrent: HKL): HKL;
|
||||||
|
var
|
||||||
|
LList: array[0..31] of HKL;
|
||||||
|
N, I: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
N := GetKeyboardLayoutList(Length(LList), LList);
|
||||||
|
for I := 0 to N - 1 do
|
||||||
|
if (LList[I] <> ACurrent) and (VkKeyScanEx(ACh, LList[I]) <> -1) then
|
||||||
|
Exit(LList[I]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function SendKeyCodeString(const AText: string; ATarget: HWND; ALayout: HKL): Integer;
|
||||||
|
var
|
||||||
|
LInputs: TArray<TInput>;
|
||||||
|
LCount: Integer;
|
||||||
|
procedure Key(AVk: Word; AUp: Boolean; AExtended: Boolean = False);
|
||||||
|
begin
|
||||||
|
if LCount >= Length(LInputs) then SetLength(LInputs, LCount + 16);
|
||||||
|
FillChar(LInputs[LCount], SizeOf(TInput), 0);
|
||||||
|
LInputs[LCount].Itype := INPUT_KEYBOARD;
|
||||||
|
LInputs[LCount].ki.wVk := AVk;
|
||||||
|
LInputs[LCount].ki.wScan := MapVirtualKeyEx(AVk, 0 {MAPVK_VK_TO_VSC}, ALayout);
|
||||||
|
if AUp then LInputs[LCount].ki.dwFlags := KEYEVENTF_KEYUP;
|
||||||
|
if AExtended then
|
||||||
|
LInputs[LCount].ki.dwFlags := LInputs[LCount].ki.dwFlags or KEYEVENTF_EXTENDEDKEY;
|
||||||
|
Inc(LCount);
|
||||||
|
end;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
LScan: SHORT;
|
||||||
|
LVk, LMods: Byte;
|
||||||
|
LShift, LCtrl, LAlt: Boolean;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
var LOriginal := ALayout;
|
||||||
|
try
|
||||||
|
for I := 1 to Length(AText) do
|
||||||
|
begin
|
||||||
|
LScan := VkKeyScanEx(AText[I], ALayout);
|
||||||
|
if LScan = -1 then
|
||||||
|
begin
|
||||||
|
var LOther := LayoutForChar(AText[I], ALayout);
|
||||||
|
if (LOther <> 0) and SwitchTargetLayout(ATarget, LOther) then
|
||||||
|
begin
|
||||||
|
ALayout := LOther;
|
||||||
|
LScan := VkKeyScanEx(AText[I], ALayout);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if LScan = -1 then
|
||||||
|
begin
|
||||||
|
Inc(Result);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
LVk := Lo(Word(LScan));
|
||||||
|
LMods := Hi(Word(LScan));
|
||||||
|
LShift := LMods and 1 <> 0;
|
||||||
|
LCtrl := LMods and 2 <> 0;
|
||||||
|
LAlt := LMods and 4 <> 0;
|
||||||
|
LCount := 0;
|
||||||
|
if LShift then Key(VK_LSHIFT, False);
|
||||||
|
if LCtrl then Key(VK_LCONTROL, False);
|
||||||
|
// Alt seul = Alt gauche ; avec Ctrl, c'est AltGr = Alt droit, etendu.
|
||||||
|
if LAlt and LCtrl then Key(VK_RMENU, False, True)
|
||||||
|
else if LAlt then Key(VK_LMENU, False);
|
||||||
|
Key(LVk, False);
|
||||||
|
Key(LVk, True);
|
||||||
|
if LAlt and LCtrl then Key(VK_RMENU, True, True)
|
||||||
|
else if LAlt then Key(VK_LMENU, True);
|
||||||
|
if LCtrl then Key(VK_LCONTROL, True);
|
||||||
|
if LShift then Key(VK_LSHIFT, True);
|
||||||
|
SendInput(LCount, @LInputs[0], SizeOf(TInput));
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
// Rendre a l'utilisateur la disposition qu'il avait choisie.
|
||||||
|
if ALayout <> LOriginal then SwitchTargetLayout(ATarget, LOriginal);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Rend le nombre de caracteres qui n'ont pas pu etre tapes (VNC seulement).
|
||||||
|
function SendTextTo(const AText: string; AByKeys: Boolean; ATarget: HWND;
|
||||||
|
ALayout: HKL): Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if AByKeys then Result := SendKeyCodeString(AText, ATarget, ALayout)
|
||||||
|
else SendUnicodeString(AText);
|
||||||
|
end;
|
||||||
|
|
||||||
// Send one virtual-key press (down+up).
|
// Send one virtual-key press (down+up).
|
||||||
procedure SendVKey(AVk: Word);
|
procedure SendVKey(AVk: Word);
|
||||||
var
|
var
|
||||||
@@ -2290,8 +2499,12 @@ begin
|
|||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
WaitForModifierRelease(1000);
|
WaitForModifierRelease(1000);
|
||||||
ForceReleaseModifiers; // timeout hit with keys still down → clean state
|
ForceReleaseModifiers; // timeout hit with keys still down → clean state
|
||||||
|
var LByKeys := (ATargetHWND <> 0) and TargetTypesByKeyCodes(ATargetHWND);
|
||||||
|
var LLayout := GetKeyboardLayout(GetWindowThreadProcessId(ATargetHWND, nil));
|
||||||
FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms',
|
FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms',
|
||||||
[LFgMs, LSw.ElapsedMilliseconds]);
|
[LFgMs, LSw.ElapsedMilliseconds]);
|
||||||
|
if LByKeys then FLastFillWaits := FLastFillWaits + ', typed as key codes (VNC)';
|
||||||
|
var LMissing := 0;
|
||||||
Sleep(FocusSettleDelayMs);
|
Sleep(FocusSettleDelayMs);
|
||||||
|
|
||||||
// Never type into our own window: if the target refused the foreground,
|
// Never type into our own window: if the target refused the foreground,
|
||||||
@@ -2315,19 +2528,19 @@ begin
|
|||||||
if AUsernameOnly then
|
if AUsernameOnly then
|
||||||
begin
|
begin
|
||||||
ClearFieldIfWanted;
|
ClearFieldIfWanted;
|
||||||
SendUnicodeString(AUsername);
|
Inc(LMissing, SendTextTo(AUsername, LByKeys, ATargetHWND, LLayout));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if AUsername = '' then
|
if AUsername = '' then
|
||||||
begin
|
begin
|
||||||
ClearFieldIfWanted;
|
ClearFieldIfWanted;
|
||||||
SendUnicodeString(APassword);
|
Inc(LMissing, SendTextTo(APassword, LByKeys, ATargetHWND, LLayout));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
ClearFieldIfWanted;
|
ClearFieldIfWanted;
|
||||||
SendUnicodeString(AUsername);
|
Inc(LMissing, SendTextTo(AUsername, LByKeys, ATargetHWND, LLayout));
|
||||||
Sleep(200);
|
Sleep(200);
|
||||||
// Real-VK send: a physically held Shift here turns Tab into Shift+Tab —
|
// Real-VK send: a physically held Shift here turns Tab into Shift+Tab —
|
||||||
// focus moves BACKWARD and the password lands in the username field.
|
// focus moves BACKWARD and the password lands in the username field.
|
||||||
@@ -2339,8 +2552,12 @@ begin
|
|||||||
// Tab lands somewhere that is not an editable field (a link, a reveal
|
// Tab lands somewhere that is not an editable field (a link, a reveal
|
||||||
// button, the document itself, as on the Reddit login form) it selects the
|
// button, the document itself, as on the Reddit login form) it selects the
|
||||||
// ENTIRE PAGE, and the password is then typed into nothing.
|
// ENTIRE PAGE, and the password is then typed into nothing.
|
||||||
SendUnicodeString(APassword);
|
Inc(LMissing, SendTextTo(APassword, LByKeys, ATargetHWND, LLayout));
|
||||||
finally
|
finally
|
||||||
|
if LMissing > 0 then
|
||||||
|
FLastFillWaits := FLastFillWaits + Format(
|
||||||
|
', %d character(s) have no key on this keyboard and were NOT typed - copy instead',
|
||||||
|
[LMissing]);
|
||||||
// Re-register with the LIVE combo (user may have rebound it), not the
|
// Re-register with the LIVE combo (user may have rebound it), not the
|
||||||
// hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A.
|
// hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A.
|
||||||
if FNewEntryHotkeyRegistered then
|
if FNewEntryHotkeyRegistered then
|
||||||
|
|||||||
@@ -768,6 +768,7 @@
|
|||||||
<label><input type="checkbox" class="fb-btn" data-key="full"> Full</label>
|
<label><input type="checkbox" class="fb-btn" data-key="full"> Full</label>
|
||||||
<label><input type="checkbox" class="fb-btn" data-key="pwd"> Password</label>
|
<label><input type="checkbox" class="fb-btn" data-key="pwd"> Password</label>
|
||||||
<label><input type="checkbox" class="fb-btn" data-key="totp"> 2FA</label>
|
<label><input type="checkbox" class="fb-btn" data-key="totp"> 2FA</label>
|
||||||
|
<label><input type="checkbox" class="fb-btn" data-key="paste"> Paste (types the clipboard)</label>
|
||||||
<label><input type="checkbox" class="fb-btn" data-key="add"> Add current window</label>
|
<label><input type="checkbox" class="fb-btn" data-key="add"> Add current window</label>
|
||||||
<label><input type="checkbox" class="fb-btn" data-key="search"> Search</label>
|
<label><input type="checkbox" class="fb-btn" data-key="search"> Search</label>
|
||||||
<label><input type="checkbox" class="fb-btn" data-key="open"> Open</label>
|
<label><input type="checkbox" class="fb-btn" data-key="open"> Open</label>
|
||||||
|
|||||||
@@ -4038,15 +4038,26 @@ const ENTRY_TEMPLATES = {
|
|||||||
// DECOUPE D'UNE NOTE DE COMMANDES. "# titre" ouvre un groupe, toute autre ligne
|
// DECOUPE D'UNE NOTE DE COMMANDES. "# titre" ouvre un groupe, toute autre ligne
|
||||||
// non vide est une commande, les lignes vides sont ignorees. Rend
|
// non vide est une commande, les lignes vides sont ignorees. Rend
|
||||||
// [{group, cmd}] dans l'ordre du texte. Pur : tests/commands.test.js.
|
// [{group, cmd}] dans l'ordre du texte. Pur : tests/commands.test.js.
|
||||||
|
// LIGNE CONTINUEE : une ligne finie par \ (bash), ` (PowerShell) ou ^ (cmd)
|
||||||
|
// se recolle a la suivante, sans le caractere ni le retour a la ligne. Le
|
||||||
|
// resultat tient sur UNE ligne : tapee dans un terminal, une fin de ligne vaut
|
||||||
|
// Entree et lancerait une commande a moitie ecrite. Une ligne vide ou la fin du
|
||||||
|
// texte termine la commande en cours.
|
||||||
function parseCommandNote(text) {
|
function parseCommandNote(text) {
|
||||||
const out = [];
|
const out = [];
|
||||||
let group = '';
|
let group = '';
|
||||||
|
let pending = '';
|
||||||
|
const flush = () => { if (pending) out.push({ group, cmd: pending }); pending = ''; };
|
||||||
for (const raw of String(text || '').split(/\r?\n/)) {
|
for (const raw of String(text || '').split(/\r?\n/)) {
|
||||||
const line = raw.trim();
|
const line = raw.trim();
|
||||||
if (!line) continue;
|
if (!line) { flush(); continue; }
|
||||||
if (line.startsWith('#')) { group = line.replace(/^#+\s*/, ''); continue; }
|
if (!pending && line.startsWith('#')) { group = line.replace(/^#+\s*/, ''); continue; }
|
||||||
out.push({ group, cmd: line });
|
const cont = /[\\`^]$/.test(line);
|
||||||
|
const part = cont ? line.slice(0, -1).trimEnd() : line;
|
||||||
|
pending = pending ? pending + ' ' + part : part;
|
||||||
|
if (!cont) flush();
|
||||||
}
|
}
|
||||||
|
flush();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,35 @@ test('masque : fin courte masquee en entier, pas d\'espace', () => {
|
|||||||
assert.equal(T.maskCommand(''), '');
|
assert.equal(T.maskCommand(''), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('lignes continuees (\\, `, ^) : une seule commande, sur une ligne', () => {
|
||||||
|
const note = [
|
||||||
|
'# D',
|
||||||
|
'docker run -d \\',
|
||||||
|
' -p 8080:80 \\',
|
||||||
|
' nginx',
|
||||||
|
'Get-Item a `',
|
||||||
|
' -Force',
|
||||||
|
'dir ^',
|
||||||
|
' /s',
|
||||||
|
].join('\n');
|
||||||
|
const r = T.parseCommandNote(note);
|
||||||
|
assert.deepEqual(plain(r).map(x => x.cmd), [
|
||||||
|
'docker run -d -p 8080:80 nginx',
|
||||||
|
'Get-Item a -Force',
|
||||||
|
'dir /s',
|
||||||
|
]);
|
||||||
|
assert.equal(r[0].group, 'D');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('continuation coupee par une ligne vide ou la fin du texte', () => {
|
||||||
|
assert.deepEqual(plain(T.parseCommandNote('a \\\n\nb')).map(x => x.cmd), ['a', 'b']);
|
||||||
|
assert.deepEqual(plain(T.parseCommandNote('a \\')).map(x => x.cmd), ['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un "#" apres une continuation fait partie de la commande', () => {
|
||||||
|
assert.equal(T.parseCommandNote('echo \\\n#x')[0].cmd, 'echo #x');
|
||||||
|
});
|
||||||
|
|
||||||
test('texte vide ou absent', () => {
|
test('texte vide ou absent', () => {
|
||||||
assert.equal(T.parseCommandNote('').length, 0);
|
assert.equal(T.parseCommandNote('').length, 0);
|
||||||
assert.equal(T.parseCommandNote(null).length, 0);
|
assert.equal(T.parseCommandNote(null).length, 0);
|
||||||
|
|||||||
@@ -2124,6 +2124,14 @@ begin
|
|||||||
if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then
|
if DetailRowAt(FCurrent, StrToIntDef(Param('k'), -1), LCaption, LValue) then
|
||||||
CopyToClipboard(LValue, LCaption);
|
CopyToClipboard(LValue, LCaption);
|
||||||
end
|
end
|
||||||
|
// Une commande d'une note de commandes, par son rang dans la liste.
|
||||||
|
else if Param('f') = 'cmd' then
|
||||||
|
begin
|
||||||
|
var LCmds := ParseCommandNote(FCurrent.Password);
|
||||||
|
var LK := StrToIntDef(Param('k'), -1);
|
||||||
|
if (LK >= 0) and (LK <= High(LCmds)) then
|
||||||
|
CopyToClipboard(LCmds[LK].Cmd, 'Command');
|
||||||
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
LIdx := StrToIntDef(Param('i'), -1);
|
LIdx := StrToIntDef(Param('i'), -1);
|
||||||
|
|||||||
Reference in New Issue
Block a user