#ifndef AppVersion #define AppVersion "0.2.4" #endif #ifndef SourceRoot #error SourceRoot must point at the self-contained Companion publish directory. #endif #ifndef OutputRoot #error OutputRoot must point at the installer output directory. #endif [Setup] AppId={{EDEB45E4-41CB-4D7A-A479-4073A1A08B93} AppName=Lumi Companion AppVersion={#AppVersion} AppVerName=Lumi Companion {#AppVersion} AppPublisher=OokamiKunTV AppCopyright=Copyright (c) 2026 OokamiKunTV DefaultDirName={localappdata}\Programs\Lumi Companion DefaultGroupName=Lumi Companion DisableProgramGroupPage=yes PrivilegesRequired=lowest ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible OutputDir={#OutputRoot} OutputBaseFilename=Lumi.Companion-Setup Compression=lzma2/ultra64 SolidCompression=yes WizardStyle=modern SetupLogging=yes CloseApplications=yes CloseApplicationsFilter=Lumi.Companion.App.exe RestartApplications=no UninstallDisplayName=Lumi Companion UninstallDisplayIcon={app}\Lumi.Companion.App.exe VersionInfoCompany=OokamiKunTV VersionInfoCopyright=Copyright (c) 2026 OokamiKunTV VersionInfoDescription=Lumi Companion installer VersionInfoProductName=Lumi Companion VersionInfoProductTextVersion={#AppVersion} LicenseFile={#SourceRoot}\legal\LUMI-COMPANION-LICENCE.txt InfoBeforeFile={#SourceRoot}\legal\PRIVACY-NOTICE.txt InfoAfterFile={#SourceRoot}\legal\THIRD-PARTY-NOTICES.txt [Files] Source: "{#SourceRoot}\Lumi.Companion.App.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceRoot}\components\*"; DestDir: "{app}\components"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#SourceRoot}\legal\*"; DestDir: "{app}\legal"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\Lumi Companion"; Filename: "{app}\Lumi.Companion.App.exe" Name: "{autoprograms}\Lumi Companion - Legal and Privacy"; Filename: "{app}\legal\LEGAL-README.txt" [Run] Filename: "{app}\Lumi.Companion.App.exe"; Description: "Open Lumi Companion"; Flags: nowait postinstall skipifsilent [Code] var PairingPackagePath: String; PairingHost: String; PairingOperatorName: String; PairingOperatorContact: String; PairingPrivacyUrl: String; HostAcknowledgementPage: TInputOptionWizardPage; function LocatePairingPackage: String; var FindRec: TFindRec; begin Result := ''; if FindFirst(ExpandConstant('{src}\*.lumi-pairing.json'), FindRec) then begin try Result := AddBackslash(ExpandConstant('{src}')) + FindRec.Name; finally FindClose(FindRec); end; end; end; function JsonStringValue(const Json: String; const Key: String): String; var Marker: String; Tail: String; MarkerPos: Integer; ColonPos: Integer; EndQuotePos: Integer; begin Result := ''; Marker := '"' + Key + '"'; MarkerPos := Pos(Marker, Json); if MarkerPos = 0 then Exit; Tail := Copy(Json, MarkerPos + Length(Marker), Length(Json)); ColonPos := Pos(':', Tail); if ColonPos = 0 then Exit; Tail := Trim(Copy(Tail, ColonPos + 1, Length(Tail))); if (Length(Tail) < 2) or (Tail[1] <> '"') then Exit; Tail := Copy(Tail, 2, Length(Tail)); EndQuotePos := Pos('"', Tail); if EndQuotePos = 0 then Exit; Result := Copy(Tail, 1, EndQuotePos - 1); end; function LoadPairingJson(const PackagePath: String; var Json: String): Boolean; var Lines: TArrayOfString; Index: Integer; begin Result := False; Json := ''; if (PackagePath = '') or not LoadStringsFromFile(PackagePath, Lines) then Exit; for Index := 0 to GetArrayLength(Lines) - 1 do begin if Index > 0 then Json := Json + #10; Json := Json + Lines[Index]; end; Result := True; end; function ReadPairingHost(const Json: String): String; var Candidate: String; LowerCandidate: String; begin Result := ''; Candidate := JsonStringValue(Json, 'host'); LowerCandidate := Lowercase(Candidate); if ((Pos('https://', LowerCandidate) = 1) or (Pos('http://', LowerCandidate) = 1)) and (Length(Candidate) <= 500) then Result := Candidate; end; procedure InitializeWizard; var HostDisplay: String; Explanation: String; OperatorDetails: String; Json: String; begin PairingPackagePath := LocatePairingPackage; if LoadPairingJson(PairingPackagePath, Json) then begin PairingHost := ReadPairingHost(Json); PairingOperatorName := Copy(JsonStringValue(Json, 'operator_name'), 1, 200); PairingOperatorContact := Copy(JsonStringValue(Json, 'operator_contact'), 1, 300); PairingPrivacyUrl := Copy(JsonStringValue(Json, 'privacy_url'), 1, 500); end; if PairingHost <> '' then HostDisplay := PairingHost else HostDisplay := 'No paired Lumi host was identified beside this installer.'; OperatorDetails := ''; if PairingOperatorName <> '' then OperatorDetails := OperatorDetails + #13#10 + 'Operator name: ' + PairingOperatorName; if PairingOperatorContact <> '' then OperatorDetails := OperatorDetails + #13#10 + 'Operator contact: ' + PairingOperatorContact; if PairingPrivacyUrl <> '' then OperatorDetails := OperatorDetails + #13#10 + 'Privacy information: ' + PairingPrivacyUrl; Explanation := 'This copy of Lumi Companion is intended to connect to:' + #13#10#13#10 + HostDisplay + OperatorDetails + #13#10#13#10 + 'The person or organisation controlling that Lumi installation is the Host Operator. ' + 'The Host Operator is responsible for the Hosted Service it controls, including server configuration, ' + 'access, server-side data processing, retention, integrations, and required user notices. ' + 'OokamiKunTV is the software developer and is not automatically the operator of an independently hosted Lumi installation. ' + 'Responsibility always follows the actual facts and applicable law.'; HostAcknowledgementPage := CreateInputOptionPage( wpInfoBefore, 'Lumi host and data responsibility', 'Review the host that will receive Companion data.', Explanation, False, False); HostAcknowledgementPage.Add( 'I understand which Lumi host this Companion will use and that the Host Operator controls its server-side service and processing.'); end; function NextButtonClick(CurPageID: Integer): Boolean; begin Result := True; { A silent deployment is an administrator/operator action and has no wizard UI in which an interactive acknowledgement can be collected. } if WizardSilent then Exit; if (HostAcknowledgementPage <> nil) and (CurPageID = HostAcknowledgementPage.ID) and not HostAcknowledgementPage.Values[0] then begin MsgBox( 'Confirm that you understand the host and operator responsibility before continuing.', mbInformation, MB_OK); Result := False; end; end; procedure ImportPairingPackage; var SourcePath: String; TargetPath: String; begin { Portable and installed builds share the same DPAPI credential directory. Do not create another authorized device merely because an existing user moves the executable into its durable installed location. } if FileExists(ExpandConstant('{localappdata}\Lumi\Companion\device.credential')) then Exit; SourcePath := PairingPackagePath; if SourcePath = '' then SourcePath := LocatePairingPackage; if SourcePath = '' then Exit; TargetPath := AddBackslash(ExpandConstant('{app}')) + ExtractFileName(SourcePath); if not CopyFile(SourcePath, TargetPath, False) then MsgBox( 'Lumi Companion was installed, but its one-time pairing package could not be imported. ' + 'Open Companion and choose the pairing file manually.', mbError, MB_OK); end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssPostInstall then ImportPairingPackage; end;