Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/app/info/info.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ <h5 class="card-title">Homebrew Channel</h5>
<ng-template #unrooted>Unrooted</ng-template>
</ng-container>
</p>
<div class="mb-3" *ngIf="homebrewAppInfo">
<label for="homebrewCustomRepoUrl" class="form-label">Custom Repository URL</label>
<div class="input-group">
<input
id="homebrewCustomRepoUrl"
class="form-control"
placeholder="https://example.com/repo.json"
type="url"
[value]="homebrewCustomRepoUrl"
(input)="homebrewCustomRepoUrl = $any($event.target).value"
(keydown.enter)="addCustomRepoToHomebrew()">
<button class="btn btn-outline-primary" type="button"
[disabled]="!homebrewCustomRepoUrl.trim()"
(click)="addCustomRepoToHomebrew()">
Add Repo
</button>
</div>
<small class="text-muted">This opens Homebrew Channel on your TV with this URL prefilled.</small>
</div>
<div>
<button class="btn btn-primary me-2" (click)="installHbChannel()"
*ngIf="!homebrewAppInfo && homebrewRepoManifest">Install
Expand Down
76 changes: 76 additions & 0 deletions src/app/info/info.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {InfoComponent} from "./info.component";
import {APP_ID_HBCHANNEL} from "../shared/constants";
import {MessageDialogComponent} from "../shared/components/message-dialog/message-dialog.component";

describe('InfoComponent', () => {
function setup() {
const appManager = {
launch: jasmine.createSpy('launch').and.resolveTo(undefined),
};
const component = new InfoComponent(
{} as any,
{selected$: {subscribe: () => ({unsubscribe: () => undefined})}} as any,
{} as any,
appManager as any,
{} as any,
{} as any
);
component.device = {name: 'test-device'} as any;
return {component, appManager};
}

it('launches Homebrew Channel addRepository mode for valid custom repo URL', async () => {
const {component, appManager} = setup();
component.homebrewCustomRepoUrl = 'https://example.com/repo.json';

await component.addCustomRepoToHomebrew();

expect(appManager.launch).toHaveBeenCalledWith(
component.device,
APP_ID_HBCHANNEL,
{
launchMode: 'addRepository',
url: 'https://example.com/repo.json',
}
);
expect(component.homebrewCustomRepoUrl).toBe('');
});

it('shows error and does not launch when URL is invalid', async () => {
const {component, appManager} = setup();
const dialogSpy = spyOn(MessageDialogComponent, 'open');
component.homebrewCustomRepoUrl = 'not-a-url';

await component.addCustomRepoToHomebrew();

expect(appManager.launch).not.toHaveBeenCalled();
expect(dialogSpy).toHaveBeenCalled();
});

it('shows error and does not launch for non-http protocols', async () => {
const {component, appManager} = setup();
const dialogSpy = spyOn(MessageDialogComponent, 'open');
component.homebrewCustomRepoUrl = 'ftp://example.com/repo.json';

await component.addCustomRepoToHomebrew();

expect(appManager.launch).not.toHaveBeenCalled();
expect(dialogSpy).toHaveBeenCalled();
});

it('trims whitespace before launching addRepository mode', async () => {
const {component, appManager} = setup();
component.homebrewCustomRepoUrl = ' https://example.com/repo.json ';

await component.addCustomRepoToHomebrew();

expect(appManager.launch).toHaveBeenCalledWith(
component.device,
APP_ID_HBCHANNEL,
{
launchMode: 'addRepository',
url: 'https://example.com/repo.json',
}
);
});
});
30 changes: 30 additions & 0 deletions src/app/info/info.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class InfoComponent implements OnInit, OnDestroy {
homebrewAppConfig: Partial<HomebrewChannelConfiguration> | null = null;
homebrewRepoManifest?: RepositoryItem;
homebrewRepoHasUpdate: boolean = false;
homebrewCustomRepoUrl: string = '';
infoError: any;
deviceSubscription!: Subscription;
infoSubscription?: Subscription;
Expand Down Expand Up @@ -160,4 +161,33 @@ export class InfoComponent implements OnInit, OnDestroy {
await this.loadHomebrewInfo(device);
}

async addCustomRepoToHomebrew(): Promise<void> {
const device = this.device;
if (!device) {
MessageDialogComponent.open(this.modalService, {
message: 'No device selected',
positive: 'OK'
});
return;
}
try {
const parsed = new URL(this.homebrewCustomRepoUrl.trim());
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Repository URL must use http:// or https://');
}
await this.appManager.launch(device, APP_ID_HBCHANNEL, {
launchMode: 'addRepository',
url: parsed.toString(),
});
this.homebrewCustomRepoUrl = '';
} catch (e) {
const reason = e instanceof Error ? e.message : 'An unexpected error occurred while adding the repository';
MessageDialogComponent.open(this.modalService, {
message: `Failed to add custom repository: ${reason}`,
error: e as Error,
positive: 'OK'
});
}
}

}
Loading