feat(android): an entry can get its icon, through the same relay as the desktop

Fetching https://<site>/favicon.ico directly would resolve a DNS name - and
open a connection - to EVERY domain kept in the vault. The list of sites where
someone holds an account is precisely what this vault protects; handing it to
the network for a sixteen-pixel drawing would be a poor trade. The relay sees
only that someone is interested in one domain, which is why the call is asked
for entry by entry and never happens on its own.

Two tries: the full host, then the second-level domain - chat.qwen.ai has no
icon at the relay, qwen.ai does. The type comes from the first bytes, never
from the extension: the relay answers PNG under a .ico address.

A dedicated patch rather than PatchEntryField, because icon_custom is a NUMBER
in the snapshot and writing it as a string would give "0" - which the desktop
reads with GetValue<Boolean> and would choke on. It is set to zero: the icon
comes from an automatic lookup, not from a choice, and that flag is what stops
the desktop from ever replacing a chosen icon during a refresh.

Base64 without line breaks. Delphi's encoder wraps at 76 characters by
default, a wrapped data: URI does not display, and that trap has already been
paid once on the containers.
This commit is contained in:
r-zakarya
2026-08-22 15:39:19 +01:00
parent 73e3faa24c
commit 2010dec22a
6 changed files with 332 additions and 6 deletions
+54
View File
@@ -87,6 +87,18 @@ function DuplicateEntry(const APlainJson, AUuid: string): string;
function AttachmentContent(const APlainJson, AUuid: string;
AIndex: Integer): string;
/// Pose l'icone d'une entree, et le drapeau de provenance avec elle.
///
/// PAS PatchEntryField : `icon_custom` est un NOMBRE dans l'instantane, et
/// l'ecrire comme une chaine donnerait `"icon_custom": "0"` - que le poste lit
/// avec GetValue<Boolean>, qui echouerait sur une chaine. Deux champs de types
/// differents, donc une fonction qui les connait tous les deux.
///
/// AUri vide efface l'icone. Le drapeau retombe alors a zero comme sur le
/// poste : une icone absente n'est pas une icone choisie.
function PatchEntryIcon(const APlainJson, AUuid, AUri: string;
ACustom: Boolean): string;
/// Fond l'instantane LOCAL dans l'instantane DISTANT et rend le resultat.
///
/// LE DISTANT SERT DE BASE, jamais le local. Il porte ce que le telephone ne
@@ -297,6 +309,48 @@ begin
end;
end;
function PatchEntryIcon(const APlainJson, AUuid, AUri: string;
ACustom: Boolean): string;
var
LRoot, LEntry: TJSONObject;
LEntries: TJSONArray;
I: Integer;
LFound: Boolean;
begin
if AUuid = '' then
raise EPMAPatch.Create('Entree sans uuid : rien a retrouver');
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
if LRoot = nil then
raise EPMAPatch.Create('Instantane illisible');
try
if not LRoot.TryGetValue<TJSONArray>('entries', LEntries) then
raise EPMAPatch.Create('Instantane sans tableau "entries"');
LFound := False;
for I := 0 to LEntries.Count - 1 do
begin
if not (LEntries.Items[I] is TJSONObject) then Continue;
LEntry := LEntries.Items[I] as TJSONObject;
if LEntry.GetValue<string>('uuid', '') <> AUuid then Continue;
LEntry.RemovePair('icon_b64').Free;
LEntry.AddPair('icon_b64', AUri);
LEntry.RemovePair('icon_custom').Free;
LEntry.AddPair('icon_custom',
TJSONNumber.Create(Ord(ACustom and (AUri <> ''))));
LEntry.RemovePair('updated_at').Free;
LEntry.AddPair('updated_at', NowUtcIso);
LFound := True;
Break;
end;
if not LFound then
raise EPMAPatch.CreateFmt('Entree %s introuvable', [AUuid]);
Result := LRoot.ToJSON;
finally
LRoot.Free;
end;
end;
function AttachmentContent(const APlainJson, AUuid: string;
AIndex: Integer): string;
var