mirror of
https://github.com/zebrajr/immich.git
synced 2025-12-06 00:20:20 +01:00
feat(web): make google cast opt in (#18514)
* add setting switch this isnt bound to anything yet * make google casting opt-in * doc updates * lint docs * remove unneeded translation items * update mobile openai defs * fix failing test we need to mock user prefs since CastButton uses it
This commit is contained in:
parent
b054e9dc2c
commit
78224961d1
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future.
|
||||
|
||||
## Enable Google Cast Support
|
||||
|
||||
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
|
||||
|
||||
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`
|
||||
|
||||
<img src={require('./img/gcast-enable.webp').default} width="70%" title='Enable Google Cast Support' />
|
||||
|
||||
## Limitations
|
||||
|
||||
To use casting with Immich, there are a few prerequisites:
|
||||
|
|
|
|||
BIN
docs/docs/features/img/gcast-enable.webp
Normal file
BIN
docs/docs/features/img/gcast-enable.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -604,6 +604,7 @@
|
|||
"cannot_undo_this_action": "You cannot undo this action!",
|
||||
"cannot_update_the_description": "Cannot update the description",
|
||||
"cast": "Cast",
|
||||
"cast_description": "Configure available cast destinations",
|
||||
"change_date": "Change date",
|
||||
"change_description": "Change description",
|
||||
"change_display_order": "Change display order",
|
||||
|
|
@ -1027,6 +1028,8 @@
|
|||
"folders": "Folders",
|
||||
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
|
||||
"forward": "Forward",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||
"general": "General",
|
||||
"get_help": "Get Help",
|
||||
"get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
|
||||
|
|
|
|||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/cast_response.dart
generated
Normal file
BIN
mobile/openapi/lib/model/cast_response.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/cast_update.dart
generated
Normal file
BIN
mobile/openapi/lib/model/cast_update.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -9556,6 +9556,26 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CastResponse": {
|
||||
"properties": {
|
||||
"gCastEnabled": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"gCastEnabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CastUpdate": {
|
||||
"properties": {
|
||||
"gCastEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ChangePasswordDto": {
|
||||
"properties": {
|
||||
"newPassword": {
|
||||
|
|
@ -14806,6 +14826,9 @@
|
|||
},
|
||||
"UserPreferencesResponseDto": {
|
||||
"properties": {
|
||||
"cast": {
|
||||
"$ref": "#/components/schemas/CastResponse"
|
||||
},
|
||||
"download": {
|
||||
"$ref": "#/components/schemas/DownloadResponse"
|
||||
},
|
||||
|
|
@ -14835,6 +14858,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"cast",
|
||||
"download",
|
||||
"emailNotifications",
|
||||
"folders",
|
||||
|
|
@ -14852,6 +14876,9 @@
|
|||
"avatar": {
|
||||
"$ref": "#/components/schemas/AvatarUpdate"
|
||||
},
|
||||
"cast": {
|
||||
"$ref": "#/components/schemas/CastUpdate"
|
||||
},
|
||||
"download": {
|
||||
"$ref": "#/components/schemas/DownloadUpdate"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -128,6 +128,9 @@ export type UserAdminUpdateDto = {
|
|||
shouldChangePassword?: boolean;
|
||||
storageLabel?: string | null;
|
||||
};
|
||||
export type CastResponse = {
|
||||
gCastEnabled: boolean;
|
||||
};
|
||||
export type DownloadResponse = {
|
||||
archiveSize: number;
|
||||
includeEmbeddedVideos: boolean;
|
||||
|
|
@ -164,6 +167,7 @@ export type TagsResponse = {
|
|||
sidebarWeb: boolean;
|
||||
};
|
||||
export type UserPreferencesResponseDto = {
|
||||
cast: CastResponse;
|
||||
download: DownloadResponse;
|
||||
emailNotifications: EmailNotificationsResponse;
|
||||
folders: FoldersResponse;
|
||||
|
|
@ -177,6 +181,9 @@ export type UserPreferencesResponseDto = {
|
|||
export type AvatarUpdate = {
|
||||
color?: UserAvatarColor;
|
||||
};
|
||||
export type CastUpdate = {
|
||||
gCastEnabled?: boolean;
|
||||
};
|
||||
export type DownloadUpdate = {
|
||||
archiveSize?: number;
|
||||
includeEmbeddedVideos?: boolean;
|
||||
|
|
@ -214,6 +221,7 @@ export type TagsUpdate = {
|
|||
};
|
||||
export type UserPreferencesUpdateDto = {
|
||||
avatar?: AvatarUpdate;
|
||||
cast?: CastUpdate;
|
||||
download?: DownloadUpdate;
|
||||
emailNotifications?: EmailNotificationsUpdate;
|
||||
folders?: FoldersUpdate;
|
||||
|
|
|
|||
|
|
@ -85,6 +85,11 @@ class PurchaseUpdate {
|
|||
hideBuyButtonUntil?: string;
|
||||
}
|
||||
|
||||
class CastUpdate {
|
||||
@ValidateBoolean({ optional: true })
|
||||
gCastEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class UserPreferencesUpdateDto {
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
|
|
@ -135,6 +140,11 @@ export class UserPreferencesUpdateDto {
|
|||
@ValidateNested()
|
||||
@Type(() => PurchaseUpdate)
|
||||
purchase?: PurchaseUpdate;
|
||||
|
||||
@Optional()
|
||||
@ValidateNested()
|
||||
@Type(() => CastUpdate)
|
||||
cast?: CastUpdate;
|
||||
}
|
||||
|
||||
class RatingsResponse {
|
||||
|
|
@ -183,6 +193,10 @@ class PurchaseResponse {
|
|||
hideBuyButtonUntil!: string;
|
||||
}
|
||||
|
||||
class CastResponse {
|
||||
gCastEnabled: boolean = false;
|
||||
}
|
||||
|
||||
export class UserPreferencesResponseDto implements UserPreferences {
|
||||
folders!: FoldersResponse;
|
||||
memories!: MemoriesResponse;
|
||||
|
|
@ -193,6 +207,7 @@ export class UserPreferencesResponseDto implements UserPreferences {
|
|||
emailNotifications!: EmailNotificationsResponse;
|
||||
download!: DownloadResponse;
|
||||
purchase!: PurchaseResponse;
|
||||
cast!: CastResponse;
|
||||
}
|
||||
|
||||
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {
|
||||
|
|
|
|||
|
|
@ -502,6 +502,9 @@ export interface UserPreferences {
|
|||
showSupportBadge: boolean;
|
||||
hideBuyButtonUntil: string;
|
||||
};
|
||||
cast: {
|
||||
gCastEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => {
|
|||
showSupportBadge: true,
|
||||
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
|
||||
},
|
||||
cast: {
|
||||
gCastEnabled: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { resetSavedUser, user as userStore } from '$lib/stores/user.store';
|
||||
import { preferences as preferencesStore, resetSavedUser, user as userStore } from '$lib/stores/user.store';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import { preferencesFactory } from '@test-data/factories/preferences-factory';
|
||||
import { userAdminFactory } from '@test-data/factories/user-factory';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render } from '@testing-library/svelte';
|
||||
|
|
@ -42,6 +43,9 @@ describe('AssetViewerNavBar component', () => {
|
|||
});
|
||||
|
||||
it('shows back button', () => {
|
||||
const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } });
|
||||
preferencesStore.set(prefs);
|
||||
|
||||
const asset = assetFactory.build({ isTrashed: false });
|
||||
const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });
|
||||
expect(getByTitle('go_back')).toBeInTheDocument();
|
||||
|
|
@ -53,6 +57,10 @@ describe('AssetViewerNavBar component', () => {
|
|||
const user = userAdminFactory.build({ id: ownerId });
|
||||
const asset = assetFactory.build({ ownerId, isTrashed: false });
|
||||
userStore.set(user);
|
||||
|
||||
const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } });
|
||||
preferencesStore.set(prefs);
|
||||
|
||||
const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });
|
||||
expect(getByTitle('delete')).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@
|
|||
let tagsEnabled = $state($preferences?.tags?.enabled ?? false);
|
||||
let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false);
|
||||
|
||||
// Cast
|
||||
let gCastEnabled = $state($preferences?.cast?.gCastEnabled ?? false);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const data = await updateMyPreferences({
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
ratings: { enabled: ratingsEnabled },
|
||||
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
|
||||
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
|
||||
cast: { gCastEnabled },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -138,6 +142,16 @@
|
|||
{/if}
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="cast" title={$t('cast')} subtitle={$t('cast_description')}>
|
||||
<div class="ms-4 mt-6">
|
||||
<SettingSwitch
|
||||
title={$t('gcast_enabled')}
|
||||
subtitle={$t('gcast_enabled_description')}
|
||||
bind:checked={gCastEnabled}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button shape="round" type="submit" size="small" onclick={() => handleSave()}>{$t('save')}</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import 'chromecast-caf-sender';
|
||||
import { Duration } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
||||
|
||||
|
|
@ -24,6 +26,12 @@ export class GCastDestination implements ICastDestination {
|
|||
private currentUrl: string | null = null;
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
const preferencesStore = get(preferences);
|
||||
if (!preferencesStore.cast.gCastEnabled) {
|
||||
this.isAvailable = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// this is a really messy way since google does a pseudo-callbak
|
||||
// in the form of a global window event. We will give Chrome 3 seconds to respond
|
||||
// or we will mark the destination as unavailable
|
||||
|
|
|
|||
43
web/src/test-data/factories/preferences-factory.ts
Normal file
43
web/src/test-data/factories/preferences-factory.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { UserPreferencesResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
|
||||
export const preferencesFactory = Sync.makeFactory<UserPreferencesResponseDto>({
|
||||
cast: {
|
||||
gCastEnabled: false,
|
||||
},
|
||||
download: {
|
||||
archiveSize: 0,
|
||||
includeEmbeddedVideos: false,
|
||||
},
|
||||
emailNotifications: {
|
||||
albumInvite: false,
|
||||
albumUpdate: false,
|
||||
enabled: false,
|
||||
},
|
||||
folders: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
memories: {
|
||||
enabled: false,
|
||||
},
|
||||
people: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
purchase: {
|
||||
hideBuyButtonUntil: '',
|
||||
showSupportBadge: false,
|
||||
},
|
||||
ratings: {
|
||||
enabled: false,
|
||||
},
|
||||
sharedLinks: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user