feat(apk): edition de l'entree entiere en un seul enregistrement, et icone choisie dans la galerie
Un crayon ouvre un formulaire pre-rempli : tous les champs modifiables a la
fois, un seul Save, donc UN commit au lieu d'un par champ ('setfields',
plusieurs PatchEntryField dans le meme CommitChange).
L'icone quitte le menu de l'entree pour ce formulaire : From site (recherche),
Image... (galerie, reduite a 128 px, provenance custom) et Remove.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,13 @@
|
|||||||
<!-- Scanner du QR des reglages de synchro, demandee au premier appui. -->
|
<!-- Scanner du QR des reglages de synchro, demandee au premier appui. -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
<!-- Choisir une icone dans la galerie. READ_MEDIA_IMAGES est le nom
|
||||||
|
d'Android 13 et plus, READ_EXTERNAL_STORAGE celui d'avant ; la
|
||||||
|
seconde est donc bornee aux anciennes versions pour ne pas demander
|
||||||
|
un acces large la ou le selecteur recent n'exige rien. -->
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
|
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
|
||||||
<queries>
|
<queries>
|
||||||
<%queries-child-elements%>
|
<%queries-child-elements%>
|
||||||
|
|||||||
@@ -585,6 +585,7 @@
|
|||||||
<div class="head">
|
<div class="head">
|
||||||
<button class="btn ico-btn back-btn" id="back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-label="Back" role="img"><path d="M20 12H5M11 5l-7 7 7 7"/></svg></button>
|
<button class="btn ico-btn back-btn" id="back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-label="Back" role="img"><path d="M20 12H5M11 5l-7 7 7 7"/></svg></button>
|
||||||
<h1 id="dtitle"></h1>
|
<h1 id="dtitle"></h1>
|
||||||
|
<button class="btn ico-btn" id="pencil" aria-label="Edit entry"></button>
|
||||||
<button class="btn ico-btn" id="kebab" aria-label="Entry menu"></button>
|
<button class="btn ico-btn" id="kebab" aria-label="Entry menu"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="scroll"><div id="fields"></div></div>
|
<div class="scroll"><div id="fields"></div></div>
|
||||||
@@ -1067,6 +1068,7 @@ function renderDetail(d) {
|
|||||||
detailRows = d.rows || [];
|
detailRows = d.rows || [];
|
||||||
document.getElementById('dtitle').textContent = d.title || '';
|
document.getElementById('dtitle').textContent = d.title || '';
|
||||||
document.getElementById('kebab').hidden = false;
|
document.getElementById('kebab').hidden = false;
|
||||||
|
document.getElementById('pencil').hidden = false;
|
||||||
var html = '', i;
|
var html = '', i;
|
||||||
if (d.totp)
|
if (d.totp)
|
||||||
html += '<div class="field totp" id="totp"><div class="cap">2FA code</div>'
|
html += '<div class="field totp" id="totp"><div class="cap">2FA code</div>'
|
||||||
@@ -1126,6 +1128,8 @@ document.getElementById('fields').addEventListener('click', function (ev) {
|
|||||||
send('touch');
|
send('touch');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var ia = t.getAttribute('data-iconact');
|
||||||
|
if (ia !== null) { send('icon?a=' + ia); return; }
|
||||||
var save = t.getAttribute('data-save');
|
var save = t.getAttribute('data-save');
|
||||||
if (save !== null) { send('savefile?k=' + save); return; }
|
if (save !== null) { send('savefile?k=' + save); return; }
|
||||||
var copy = t.getAttribute('data-copy');
|
var copy = t.getAttribute('data-copy');
|
||||||
@@ -1458,6 +1462,7 @@ function openNewEntry() {
|
|||||||
// qui, cote Delphi, visent la derniere entree ouverte.
|
// qui, cote Delphi, visent la derniere entree ouverte.
|
||||||
closeMenu();
|
closeMenu();
|
||||||
document.getElementById('kebab').hidden = true;
|
document.getElementById('kebab').hidden = true;
|
||||||
|
document.getElementById('pencil').hidden = true;
|
||||||
document.getElementById('fields').innerHTML =
|
document.getElementById('fields').innerHTML =
|
||||||
'<div class="form">'
|
'<div class="form">'
|
||||||
+ '<div class="field"><div class="cap">Kind</div><div class="line">'
|
+ '<div class="field"><div class="cap">Kind</div><div class="line">'
|
||||||
@@ -1565,13 +1570,83 @@ function closeMenu() {
|
|||||||
function openMenu() {
|
function openMenu() {
|
||||||
var m = document.getElementById('menu');
|
var m = document.getElementById('menu');
|
||||||
m.innerHTML = '<button data-hist>Previous passwords</button>'
|
m.innerHTML = '<button data-hist>Previous passwords</button>'
|
||||||
+ '<button data-icon="fetch">Fetch icon</button>'
|
|
||||||
+ '<button data-icon="clear">Remove icon</button>'
|
|
||||||
+ '<button data-dup>Duplicate entry</button>'
|
+ '<button data-dup>Duplicate entry</button>'
|
||||||
+ '<button class="danger" data-del>Delete entry</button>';
|
+ '<button class="danger" data-del>Delete entry</button>';
|
||||||
m.hidden = false;
|
m.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MODIFIER L'ENTREE ENTIERE, PAS UN CHAMP A LA FOIS.
|
||||||
|
//
|
||||||
|
// Chaque ligne avait son crayon : ouvrir, taper, valider, recommencer - et
|
||||||
|
// CHAQUE validation etait un enregistrement complet, donc un commit de plus
|
||||||
|
// dans le depot. Ici un seul formulaire, un seul Save, un seul commit.
|
||||||
|
//
|
||||||
|
// La page ne connait toujours pas le schema : elle rend une ligne par champ
|
||||||
|
// que Delphi a marque modifiable ('e' porte son nom), dans l'ordre qu'il a
|
||||||
|
// choisi. Ajouter un champ modifiable cote Delphi suffit a le voir ici.
|
||||||
|
function openEditEntry() {
|
||||||
|
if (!lastDetail) return;
|
||||||
|
closeMenu();
|
||||||
|
var html = '<div class="form">', i, r;
|
||||||
|
for (i = 0; i < detailRows.length; i++) {
|
||||||
|
r = detailRows[i];
|
||||||
|
if (!r.e) continue;
|
||||||
|
html += '<div class="field"><div class="cap">' + esc(r.c) + '</div><div class="line'
|
||||||
|
+ (r.m ? ' wrap' : '') + '">'
|
||||||
|
+ (r.m
|
||||||
|
? '<textarea class="field-in" data-ef="' + esc(r.e) + '" rows="5"'
|
||||||
|
+ ' autocorrect="off" spellcheck="false" style="' + FIELD_STYLE
|
||||||
|
+ ';height:auto;resize:vertical">' + esc(r.v) + '</textarea>'
|
||||||
|
: '<input class="field-in"' + (r.e === 'password' ? ' id="editpwd"' : '')
|
||||||
|
+ ' data-ef="' + esc(r.e) + '" type="text"'
|
||||||
|
+ ' autocomplete="off" autocorrect="off" autocapitalize="none"'
|
||||||
|
+ ' spellcheck="false" style="' + FIELD_STYLE + '" value="'
|
||||||
|
+ esc(r.v) + '">')
|
||||||
|
+ (r.e === 'password' ? genButton('editpwd') : '')
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
// L'ICONE SE CHANGE OU SE CHANGENT LES CHAMPS. Elle vivait dans le menu de
|
||||||
|
// l'entree, a cote de Supprimer : deux actions de nature opposee au meme
|
||||||
|
// endroit, et rien qui montre l'icone du moment.
|
||||||
|
html += '<div class="field"><div class="cap">Icon</div><div class="line">'
|
||||||
|
+ '<button class="btn" data-iconact="fetch">From site</button>'
|
||||||
|
+ '<button class="btn" data-iconact="pick">Image...</button>'
|
||||||
|
+ '<button class="btn" data-iconact="clear">Remove</button>'
|
||||||
|
+ '</div></div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="line" style="display:flex;gap:8px;margin-top:12px">'
|
||||||
|
+ '<button class="btn wide" id="eCancel" style="flex:1">Cancel</button>'
|
||||||
|
+ '<button class="btn main wide" id="eSave" style="flex:1">Save</button>'
|
||||||
|
+ '</div>';
|
||||||
|
document.getElementById('fields').innerHTML = html;
|
||||||
|
document.getElementById('eCancel').addEventListener('click', function () {
|
||||||
|
renderDetail(lastDetail);
|
||||||
|
});
|
||||||
|
document.getElementById('eSave').addEventListener('click', saveEditEntry);
|
||||||
|
send('touch');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UN SEUL ENVOI, ET SEULEMENT CE QUI A CHANGE : le reste du coffre n'a pas a
|
||||||
|
/// etre reecrit parce qu'on a corrige une faute de frappe dans un nom.
|
||||||
|
function saveEditEntry() {
|
||||||
|
var ins = document.querySelectorAll('#fields [data-ef]'), q = [], n = 0, i, el, f, v, k;
|
||||||
|
for (i = 0; i < ins.length; i++) {
|
||||||
|
el = ins[i];
|
||||||
|
f = el.getAttribute('data-ef');
|
||||||
|
v = el.value;
|
||||||
|
for (k = 0; k < detailRows.length; k++)
|
||||||
|
if (detailRows[k].e === f && detailRows[k].v === v) { f = ''; break; }
|
||||||
|
if (!f) continue;
|
||||||
|
q.push('f' + n + '=' + encodeURIComponent(f) + '&v' + n + '=' + encodeURIComponent(v));
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (!n) { renderDetail(lastDetail); return; }
|
||||||
|
document.getElementById('eSave').disabled = true;
|
||||||
|
send('setfields?' + q.join('&'));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pencil').innerHTML = ico('edit', 'Edit entry');
|
||||||
|
document.getElementById('pencil').addEventListener('click', openEditEntry);
|
||||||
document.getElementById('kebab').innerHTML = ico('dots', 'Entry menu');
|
document.getElementById('kebab').innerHTML = ico('dots', 'Entry menu');
|
||||||
document.getElementById('kebab').addEventListener('click', function (ev) {
|
document.getElementById('kebab').addEventListener('click', function (ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|||||||
+166
-1
@@ -48,6 +48,7 @@ uses
|
|||||||
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
||||||
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
|
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
|
||||||
FMX.WebBrowser, System.Messaging, FMX.Media,
|
FMX.WebBrowser, System.Messaging, FMX.Media,
|
||||||
|
FMX.MediaLibrary, FMX.MediaLibrary.Actions,
|
||||||
PMA.Vault;
|
PMA.Vault;
|
||||||
|
|
||||||
const
|
const
|
||||||
@@ -340,6 +341,9 @@ type
|
|||||||
/// Change UN champ de l'entree ouverte. Le nom du champ vient de la page,
|
/// Change UN champ de l'entree ouverte. Le nom du champ vient de la page,
|
||||||
/// et n'est donc pas cru sur parole.
|
/// et n'est donc pas cru sur parole.
|
||||||
procedure SaveEntryField(const AField, ANewValue: string);
|
procedure SaveEntryField(const AField, ANewValue: string);
|
||||||
|
/// Plusieurs champs d'un coup, en UN SEUL enregistrement - donc un seul
|
||||||
|
/// commit dans le depot. Les deux tableaux vont par paires.
|
||||||
|
procedure SaveEntryFields(const AFields, AValues: TArray<string>);
|
||||||
/// Ajoute une entree au coffre. Le mot de passe voyage par le pont,
|
/// Ajoute une entree au coffre. Le mot de passe voyage par le pont,
|
||||||
/// comme 'setfield' - meme regle, meme journalisation muette.
|
/// comme 'setfield' - meme regle, meme journalisation muette.
|
||||||
procedure CreateEntry(const ATitle, ASite, AUser, APwd, AKind,
|
procedure CreateEntry(const ATitle, ASite, AUser, APwd, AKind,
|
||||||
@@ -382,6 +386,9 @@ type
|
|||||||
// Scanner du QR affiche par le poste (ZXing.Delphi, Apache 2.0).
|
// Scanner du QR affiche par le poste (ZXing.Delphi, Apache 2.0).
|
||||||
var
|
var
|
||||||
FScanning, FScanBusy: Boolean;
|
FScanning, FScanBusy: Boolean;
|
||||||
|
/// L'action de la galerie survit a l'appel : le selecteur d'Android rend
|
||||||
|
/// la main tout de suite et rappelle plus tard.
|
||||||
|
FPickIcon: TTakePhotoFromLibraryAction;
|
||||||
FScanFrame: Integer;
|
FScanFrame: Integer;
|
||||||
FScanLay: TLayout;
|
FScanLay: TLayout;
|
||||||
FScanImg: TImage;
|
FScanImg: TImage;
|
||||||
@@ -401,6 +408,12 @@ type
|
|||||||
function PwdName: string;
|
function PwdName: string;
|
||||||
/// Va chercher l'icone du site de l'entree ouverte, ou l'efface.
|
/// Va chercher l'icone du site de l'entree ouverte, ou l'efface.
|
||||||
procedure SetEntryIcon(AFetch: Boolean);
|
procedure SetEntryIcon(AFetch: Boolean);
|
||||||
|
/// CHOISIR UNE IMAGE DE LA GALERIE comme icone. Elle est reduite avant
|
||||||
|
/// d'entrer dans le coffre - une photo de telephone pese quelques Mo, et
|
||||||
|
/// une icone se regarde a 32 px dans une liste.
|
||||||
|
procedure PickEntryIcon;
|
||||||
|
procedure BeginPickIcon;
|
||||||
|
procedure IconPicked(AImage: TBitmap);
|
||||||
/// Le depot a bouge, ou ne l'a plus : LE DRAPEAU ET SON TEMOIN ENSEMBLE.
|
/// Le depot a bouge, ou ne l'a plus : LE DRAPEAU ET SON TEMOIN ENSEMBLE.
|
||||||
///
|
///
|
||||||
/// Ils ne l'etaient pas. FRemoteAhead etait remis a faux a trois endroits -
|
/// Ils ne l'etaient pas. FRemoteAhead etait remis a faux a trois endroits -
|
||||||
@@ -1254,6 +1267,46 @@ begin
|
|||||||
end);
|
end);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
/// L'EDITION DE L'ENTREE ENTIERE. Un champ a la fois, c'etait un commit par
|
||||||
|
/// champ : corriger un nom et son adresse laissait deux instantanes dans le
|
||||||
|
/// depot. Ici les corrections s'appliquent l'une apres l'autre au meme clair,
|
||||||
|
/// et c'est le resultat qui part.
|
||||||
|
///
|
||||||
|
/// Le filtre des noms est celui de SaveEntryField, et pour la meme raison : ce
|
||||||
|
/// qui traverse le pont se traite comme une entree exterieure.
|
||||||
|
procedure TForm8.SaveEntryFields(const AFields, AValues: TArray<string>);
|
||||||
|
var
|
||||||
|
LUuid, LTitle: string;
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
if Length(AFields) = 0 then Exit;
|
||||||
|
if FCurrent.Uuid = '' then
|
||||||
|
begin
|
||||||
|
RefuseChange('This entry has no uuid - cannot be updated');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
for I := 0 to High(AFields) do
|
||||||
|
if (AFields[I] <> 'title') and (AFields[I] <> 'site')
|
||||||
|
and (AFields[I] <> 'username') and (AFields[I] <> 'password')
|
||||||
|
and (AFields[I] <> 'folder') and (AFields[I] <> 'totp_secret') then
|
||||||
|
begin
|
||||||
|
RefuseChange('That field cannot be changed from here');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
LUuid := FCurrent.Uuid;
|
||||||
|
LTitle := FCurrent.Display;
|
||||||
|
CommitChange('Saved: ' + LTitle,
|
||||||
|
Format('%d field(s) changed from phone', [Length(AFields)]), LUuid,
|
||||||
|
function (APlain: string): string
|
||||||
|
var
|
||||||
|
J: Integer;
|
||||||
|
begin
|
||||||
|
Result := APlain;
|
||||||
|
for J := 0 to High(AFields) do
|
||||||
|
Result := PatchEntryField(Result, LUuid, AFields[J], AValues[J]);
|
||||||
|
end);
|
||||||
|
end;
|
||||||
|
|
||||||
/// SUPPRIMER - la seule action sans retour depuis le telephone.
|
/// SUPPRIMER - la seule action sans retour depuis le telephone.
|
||||||
///
|
///
|
||||||
/// Elle ne met pas a la corbeille : celle du poste ne voyage pas dans la
|
/// Elle ne met pas a la corbeille : celle du poste ne voyage pas dans la
|
||||||
@@ -1950,7 +2003,10 @@ begin
|
|||||||
end
|
end
|
||||||
|
|
||||||
else if LAction = 'icon' then
|
else if LAction = 'icon' then
|
||||||
SetEntryIcon(Param('a') = 'fetch')
|
begin
|
||||||
|
if Param('a') = 'pick' then PickEntryIcon
|
||||||
|
else SetEntryIcon(Param('a') = 'fetch');
|
||||||
|
end
|
||||||
|
|
||||||
{$IFDEF DEBUG}
|
{$IFDEF DEBUG}
|
||||||
// JETER LE COFFRE LOCAL - debogage seulement, et absent a la compilation
|
// JETER LE COFFRE LOCAL - debogage seulement, et absent a la compilation
|
||||||
@@ -2002,6 +2058,21 @@ begin
|
|||||||
SaveEntryField(TNetEncoding.URL.Decode(Param('f')),
|
SaveEntryField(TNetEncoding.URL.Decode(Param('f')),
|
||||||
TNetEncoding.URL.Decode(Param('v')))
|
TNetEncoding.URL.Decode(Param('v')))
|
||||||
|
|
||||||
|
// f0=&v0=&f1=&v1=... : autant de paires que de champs modifies.
|
||||||
|
else if LAction = 'setfields' then
|
||||||
|
begin
|
||||||
|
var LF: TArray<string> := [];
|
||||||
|
var LV: TArray<string> := [];
|
||||||
|
var LN := 0;
|
||||||
|
while Param('f' + IntToStr(LN)) <> '' do
|
||||||
|
begin
|
||||||
|
LF := LF + [TNetEncoding.URL.Decode(Param('f' + IntToStr(LN)))];
|
||||||
|
LV := LV + [TNetEncoding.URL.Decode(Param('v' + IntToStr(LN)))];
|
||||||
|
Inc(LN);
|
||||||
|
end;
|
||||||
|
SaveEntryFields(LF, LV);
|
||||||
|
end
|
||||||
|
|
||||||
// La page vient de poser le focus sur la recherche : on deplie le clavier.
|
// La page vient de poser le focus sur la recherche : on deplie le clavier.
|
||||||
else if LAction = 'kbd' then
|
else if LAction = 'kbd' then
|
||||||
ShowSoftKeyboard
|
ShowSoftKeyboard
|
||||||
@@ -3392,6 +3463,100 @@ begin
|
|||||||
end).Start;
|
end).Start;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
/// L'ICONE VENUE DE LA GALERIE. Provenance a VRAI, comme un televersement
|
||||||
|
/// depuis le poste : une icone choisie ne doit jamais etre remplacee par une
|
||||||
|
/// recherche automatique, et ce drapeau est ce qui le dit au poste.
|
||||||
|
const
|
||||||
|
ICON_MAX_PX = 128;
|
||||||
|
|
||||||
|
procedure TForm8.IconPicked(AImage: TBitmap);
|
||||||
|
var
|
||||||
|
LSmall: TBitmap;
|
||||||
|
LStream: TMemoryStream;
|
||||||
|
LB64: TBase64Encoding;
|
||||||
|
LUri, LTitle, LUuid: string;
|
||||||
|
LScale: Single;
|
||||||
|
begin
|
||||||
|
if (AImage = nil) or (AImage.Width = 0) or (AImage.Height = 0) then Exit;
|
||||||
|
if FCurrent.Uuid = '' then Exit;
|
||||||
|
LUuid := FCurrent.Uuid;
|
||||||
|
LTitle := FCurrent.Display;
|
||||||
|
if AImage.Width > AImage.Height then
|
||||||
|
LScale := ICON_MAX_PX / AImage.Width
|
||||||
|
else
|
||||||
|
LScale := ICON_MAX_PX / AImage.Height;
|
||||||
|
if LScale > 1 then LScale := 1;
|
||||||
|
LSmall := TBitmap.Create(Round(AImage.Width * LScale),
|
||||||
|
Round(AImage.Height * LScale));
|
||||||
|
try
|
||||||
|
LSmall.Canvas.BeginScene;
|
||||||
|
try
|
||||||
|
LSmall.Canvas.DrawBitmap(AImage, RectF(0, 0, AImage.Width, AImage.Height),
|
||||||
|
RectF(0, 0, LSmall.Width, LSmall.Height), 1, True);
|
||||||
|
finally
|
||||||
|
LSmall.Canvas.EndScene;
|
||||||
|
end;
|
||||||
|
LStream := TMemoryStream.Create;
|
||||||
|
try
|
||||||
|
LSmall.SaveToStream(LStream); // PNG
|
||||||
|
// Sans coupure de ligne : une URI data: coupee ne s'affiche pas.
|
||||||
|
LB64 := TBase64Encoding.Create(0);
|
||||||
|
try
|
||||||
|
LUri := 'data:image/png;base64,'
|
||||||
|
+ LB64.EncodeBytesToString(LStream.Memory, LStream.Size);
|
||||||
|
finally
|
||||||
|
LB64.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
LStream.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
LSmall.Free;
|
||||||
|
end;
|
||||||
|
Log(Format('icon: picked image, %d chars', [Length(LUri)]));
|
||||||
|
CommitChange('Icon set: ' + LTitle, 'icon picked on phone', LUuid,
|
||||||
|
function (APlain: string): string
|
||||||
|
begin
|
||||||
|
Result := PatchEntryIcon(APlain, LUuid, LUri, True);
|
||||||
|
end);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm8.BeginPickIcon;
|
||||||
|
begin
|
||||||
|
if FPickIcon = nil then
|
||||||
|
begin
|
||||||
|
FPickIcon := TTakePhotoFromLibraryAction.Create(Self);
|
||||||
|
FPickIcon.OnDidFinishTaking := IconPicked;
|
||||||
|
end;
|
||||||
|
FPickIcon.Execute;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm8.PickEntryIcon;
|
||||||
|
begin
|
||||||
|
if FBusy then Exit;
|
||||||
|
if FCurrent.Uuid = '' then
|
||||||
|
begin
|
||||||
|
RefuseChange('This entry has no uuid');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
{$IFDEF ANDROID}
|
||||||
|
// Les deux noms : le premier est celui d'Android 13 et plus, le second celui
|
||||||
|
// d'avant. Un nom inconnu du systeme est simplement refuse, et le selecteur
|
||||||
|
// recent n'exige de toute facon rien - on ouvre donc dans tous les cas, et
|
||||||
|
// c'est Android qui refuse s'il doit refuser.
|
||||||
|
PermissionsService.RequestPermissions(
|
||||||
|
['android.permission.READ_MEDIA_IMAGES',
|
||||||
|
'android.permission.READ_EXTERNAL_STORAGE'],
|
||||||
|
procedure(const APermissions: TClassicStringDynArray;
|
||||||
|
const AGrantResults: TClassicPermissionStatusDynArray)
|
||||||
|
begin
|
||||||
|
BeginPickIcon;
|
||||||
|
end);
|
||||||
|
{$ELSE}
|
||||||
|
BeginPickIcon;
|
||||||
|
{$ENDIF}
|
||||||
|
end;
|
||||||
|
|
||||||
/// POSER OU CHANGER LE CODE, IMMEDIATEMENT ET SUR DEMANDE.
|
/// POSER OU CHANGER LE CODE, IMMEDIATEMENT ET SUR DEMANDE.
|
||||||
///
|
///
|
||||||
/// Le coffre doit etre ouvert : c'est la seule preuve que le code courant est
|
/// Le coffre doit etre ouvert : c'est la seule preuve que le code courant est
|
||||||
|
|||||||
Reference in New Issue
Block a user