fix: Downloads for iOS (#1869)
- Closes #1863 --------- Co-authored-by: Ilya Rakhlin <i.rakhlin@gmail.com>
This commit is contained in:
co-authored by
Ilya Rakhlin
parent
a6ecb76efe
commit
43947b49dc
@@ -1,3 +1,11 @@
|
|||||||
export const isCompanionApp = (userAgent: string): boolean => {
|
export const isCompanionApp = (userAgent: string): boolean => {
|
||||||
return !!userAgent.match(/Home ?Assistant/);
|
return !!userAgent.match(/Home ?Assistant/);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAndroidCompanionApp = (userAgent: string): boolean => {
|
||||||
|
return !!userAgent.match(/(?=.*Home ?Assistant)(?=.*Android)/);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isIOSCompanionApp = (userAgent: string): boolean => {
|
||||||
|
return !!userAgent.match(/(?=.*Home ?Assistant)(?=.*iOS)/);
|
||||||
|
};
|
||||||
|
|||||||
+4
-10
@@ -3,7 +3,6 @@ import { localize } from '../localize/localize';
|
|||||||
import { ExtendedHomeAssistant, FrigateCardError } from '../types';
|
import { ExtendedHomeAssistant, FrigateCardError } from '../types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
import { isCompanionApp } from './companion';
|
|
||||||
import { homeAssistantSignPath } from './ha';
|
import { homeAssistantSignPath } from './ha';
|
||||||
|
|
||||||
export const downloadURL = (url: string, filename = 'download'): void => {
|
export const downloadURL = (url: string, filename = 'download'): void => {
|
||||||
@@ -12,15 +11,11 @@ export const downloadURL = (url: string, filename = 'download'): void => {
|
|||||||
const isSameOrigin = new URL(url).origin === window.location.origin;
|
const isSameOrigin = new URL(url).origin === window.location.origin;
|
||||||
const dataURL = url.startsWith('data:');
|
const dataURL = url.startsWith('data:');
|
||||||
|
|
||||||
if (isCompanionApp(navigator.userAgent) || (!isSameOrigin && !dataURL)) {
|
if (!isSameOrigin && !dataURL) {
|
||||||
// Home Assistant companion apps cannot download files without opening a
|
|
||||||
// new browser window.
|
|
||||||
//
|
|
||||||
// User-agents are specified here:
|
|
||||||
// - Android: https://github.com/home-assistant/android/blob/b285c9525dd4837a82db931c1b2321c0511494e6/common/src/main/java/io/homeassistant/companion/android/common/data/HomeAssistantApis.kt#L23
|
|
||||||
// - iOS: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift#L75
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the HTML5 download attribute to prevent a new window from
|
// Use the HTML5 download attribute to prevent a new window from
|
||||||
// temporarily opening.
|
// temporarily opening.
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -28,7 +23,6 @@ export const downloadURL = (url: string, filename = 'download'): void => {
|
|||||||
link.href = url;
|
link.href = url;
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const downloadMedia = async (
|
export const downloadMedia = async (
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isCompanionApp } from '../../src/utils/companion';
|
import {
|
||||||
|
isCompanionApp,
|
||||||
|
isAndroidCompanionApp,
|
||||||
|
isIOSCompanionApp,
|
||||||
|
} from '../../src/utils/companion';
|
||||||
|
|
||||||
describe('isCompanionApp', () => {
|
describe('isCompanionApp', () => {
|
||||||
it('should return true for userAgent starting with "Home Assistant/"', () => {
|
it('should return true for userAgent starting with "Home Assistant/"', () => {
|
||||||
@@ -18,3 +22,47 @@ describe('isCompanionApp', () => {
|
|||||||
expect(isCompanionApp('')).toBe(false);
|
expect(isCompanionApp('')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isAndroidCompanionApp', () => {
|
||||||
|
it('should return true for userAgent containing "Home Assistant" and "Android"', () => {
|
||||||
|
expect(isAndroidCompanionApp('Home Assistant/1.0 (Android 1.0; 1.0)')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for userAgent containing "HomeAssistant" and "Android"', () => {
|
||||||
|
expect(isAndroidCompanionApp('HomeAssistant/2.0 (Android 2.0; 2.0)')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for userAgent not starting with "Home Assistant/" or "HomeAssistant/"', () => {
|
||||||
|
expect(isAndroidCompanionApp('Mozilla/5.0')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for userAgent containing "Home Assistant/" or "HomeAssistant/" and iOS', () => {
|
||||||
|
expect(
|
||||||
|
isAndroidCompanionApp(
|
||||||
|
'Home Assistant/2025.1.1 (io.robbie.HomeAssistant; build:2025.1077; iOS 18.3.0',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isIOSCompanionApp', () => {
|
||||||
|
it('should return true for userAgent containing "Home Assistant" and "iOS"', () => {
|
||||||
|
expect(isIOSCompanionApp('Home Assistant/1.0 (iOS 1.0; 1.0)')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for userAgent containing "HomeAssistant" and "iOS"', () => {
|
||||||
|
expect(isIOSCompanionApp('HomeAssistant/2.0 (iOS 2.0; 2.0)')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for userAgent not starting with "Home Assistant/" or "HomeAssistant/"', () => {
|
||||||
|
expect(isIOSCompanionApp('Mozilla/5.0')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for userAgent containing "Home Assistant/" or "HomeAssistant/" and Android', () => {
|
||||||
|
expect(
|
||||||
|
isIOSCompanionApp(
|
||||||
|
'Home Assistant/2025.1.1 (io.robbie.HomeAssistant; build:2025.1077; Android 18.3.0',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,20 +46,16 @@ describe('downloadURL', () => {
|
|||||||
expect(link.click).toBeCalled();
|
expect(link.click).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should download in apps via window.open', () => {
|
it('should download different origin via window.open', () => {
|
||||||
// Set the origin to the same.
|
// Set the origin to the same.
|
||||||
const location: Location & { origin: string } = mock<Location>();
|
const location: Location & { origin: string } = mock<Location>();
|
||||||
location.origin = 'http://foo';
|
location.origin = 'http://foo';
|
||||||
global.window.location = location;
|
global.window.location = location;
|
||||||
|
|
||||||
vi.stubGlobal('navigator', {
|
|
||||||
userAgent: 'Home Assistant/2023.3.0-3260 (Android 13; Pixel 7 Pro)',
|
|
||||||
});
|
|
||||||
|
|
||||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
||||||
|
|
||||||
downloadURL('http://foo/url.mp4');
|
downloadURL('http://bar/url.mp4');
|
||||||
expect(windowSpy).toBeCalledWith('http://foo/url.mp4', '_blank');
|
expect(windowSpy).toBeCalledWith('http://bar/url.mp4', '_blank');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user