From 78224961d16e0900790ccb962819b177ead54ca9 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Wed, 28 May 2025 15:57:36 -0500 Subject: [PATCH] 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 --- docs/docs/features/casting.md | 8 ++ docs/docs/features/img/gcast-enable.webp | Bin 0 -> 19770 bytes i18n/en.json | 3 + mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + mobile/openapi/lib/model/cast_response.dart | 99 ++++++++++++++++ mobile/openapi/lib/model/cast_update.dart | 108 ++++++++++++++++++ .../model/user_preferences_response_dto.dart | 10 +- .../model/user_preferences_update_dto.dart | 19 ++- open-api/immich-openapi-specs.json | 27 +++++ open-api/typescript-sdk/src/fetch-client.ts | 8 ++ server/src/dtos/user-preferences.dto.ts | 15 +++ server/src/types.ts | 3 + server/src/utils/preferences.ts | 3 + .../asset-viewer/asset-viewer-nav-bar.spec.ts | 10 +- .../feature-settings.svelte | 14 +++ .../utils/cast/gcast-destination.svelte.ts | 8 ++ .../factories/preferences-factory.ts | 43 +++++++ 19 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 docs/docs/features/img/gcast-enable.webp create mode 100644 mobile/openapi/lib/model/cast_response.dart create mode 100644 mobile/openapi/lib/model/cast_update.dart create mode 100644 web/src/test-data/factories/preferences-factory.ts diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md index cc25e24da76..bca85cb28ce 100644 --- a/docs/docs/features/casting.md +++ b/docs/docs/features/casting.md @@ -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` + + + ## Limitations To use casting with Immich, there are a few prerequisites: diff --git a/docs/docs/features/img/gcast-enable.webp b/docs/docs/features/img/gcast-enable.webp new file mode 100644 index 0000000000000000000000000000000000000000..f128b82e25c827eef25d99476c8f178c36e36c57 GIT binary patch literal 19770 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 literal 0 HcmV?d00001 diff --git a/i18n/en.json b/i18n/en.json index 56e38cf8166..603eb58b3db 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 24faee28ade..b8bcbcebcd7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -321,6 +321,8 @@ Class | Method | HTTP request | Description - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) - [CQMode](doc//CQMode.md) + - [CastResponse](doc//CastResponse.md) + - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d3a342db6c1..ca33a5dea1b 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -114,6 +114,8 @@ part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; part 'model/cq_mode.dart'; +part 'model/cast_response.dart'; +part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index cc01fd2c064..1059655323a 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -284,6 +284,10 @@ class ApiClient { return CLIPConfig.fromJson(value); case 'CQMode': return CQModeTypeTransformer().decode(value); + case 'CastResponse': + return CastResponse.fromJson(value); + case 'CastUpdate': + return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); case 'CheckExistingAssetsDto': diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart new file mode 100644 index 00000000000..d49f1ad3d76 --- /dev/null +++ b/mobile/openapi/lib/model/cast_response.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class CastResponse { + /// Returns a new [CastResponse] instance. + CastResponse({ + this.gCastEnabled = false, + }); + + bool gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastResponse && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled.hashCode); + + @override + String toString() => 'CastResponse[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + json[r'gCastEnabled'] = this.gCastEnabled; + return json; + } + + /// Returns a new [CastResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastResponse? fromJson(dynamic value) { + upgradeDto(value, "CastResponse"); + if (value is Map) { + final json = value.cast(); + + return CastResponse( + gCastEnabled: mapValueOfType(json, r'gCastEnabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = CastResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = CastResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = CastResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'gCastEnabled', + }; +} + diff --git a/mobile/openapi/lib/model/cast_update.dart b/mobile/openapi/lib/model/cast_update.dart new file mode 100644 index 00000000000..87076391325 --- /dev/null +++ b/mobile/openapi/lib/model/cast_update.dart @@ -0,0 +1,108 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class CastUpdate { + /// Returns a new [CastUpdate] instance. + CastUpdate({ + this.gCastEnabled, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastUpdate && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled == null ? 0 : gCastEnabled!.hashCode); + + @override + String toString() => 'CastUpdate[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + if (this.gCastEnabled != null) { + json[r'gCastEnabled'] = this.gCastEnabled; + } else { + // json[r'gCastEnabled'] = null; + } + return json; + } + + /// Returns a new [CastUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastUpdate? fromJson(dynamic value) { + upgradeDto(value, "CastUpdate"); + if (value is Map) { + final json = value.cast(); + + return CastUpdate( + gCastEnabled: mapValueOfType(json, r'gCastEnabled'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = CastUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = CastUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = CastUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 215e691cb1e..c729e0d80fd 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserPreferencesResponseDto { /// Returns a new [UserPreferencesResponseDto] instance. UserPreferencesResponseDto({ + required this.cast, required this.download, required this.emailNotifications, required this.folders, @@ -24,6 +25,8 @@ class UserPreferencesResponseDto { required this.tags, }); + CastResponse cast; + DownloadResponse download; EmailNotificationsResponse emailNotifications; @@ -44,6 +47,7 @@ class UserPreferencesResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -57,6 +61,7 @@ class UserPreferencesResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (cast.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + (folders.hashCode) + @@ -68,10 +73,11 @@ class UserPreferencesResponseDto { (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; + json[r'cast'] = this.cast; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'folders'] = this.folders; @@ -93,6 +99,7 @@ class UserPreferencesResponseDto { final json = value.cast(); return UserPreferencesResponseDto( + cast: CastResponse.fromJson(json[r'cast'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, folders: FoldersResponse.fromJson(json[r'folders'])!, @@ -149,6 +156,7 @@ class UserPreferencesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'cast', 'download', 'emailNotifications', 'folders', diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 3e420df119a..73e3cac9ffd 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -14,6 +14,7 @@ class UserPreferencesUpdateDto { /// Returns a new [UserPreferencesUpdateDto] instance. UserPreferencesUpdateDto({ this.avatar, + this.cast, this.download, this.emailNotifications, this.folders, @@ -33,6 +34,14 @@ class UserPreferencesUpdateDto { /// AvatarUpdate? avatar; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + CastUpdate? cast; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -108,6 +117,7 @@ class UserPreferencesUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -122,6 +132,7 @@ class UserPreferencesUpdateDto { int get hashCode => // ignore: unnecessary_parenthesis (avatar == null ? 0 : avatar!.hashCode) + + (cast == null ? 0 : cast!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + (folders == null ? 0 : folders!.hashCode) + @@ -133,7 +144,7 @@ class UserPreferencesUpdateDto { (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -142,6 +153,11 @@ class UserPreferencesUpdateDto { } else { // json[r'avatar'] = null; } + if (this.cast != null) { + json[r'cast'] = this.cast; + } else { + // json[r'cast'] = null; + } if (this.download != null) { json[r'download'] = this.download; } else { @@ -200,6 +216,7 @@ class UserPreferencesUpdateDto { return UserPreferencesUpdateDto( avatar: AvatarUpdate.fromJson(json[r'avatar']), + cast: CastUpdate.fromJson(json[r'cast']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), folders: FoldersUpdate.fromJson(json[r'folders']), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 98382a382ca..6aca3c349ad 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -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" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0ce6f417b15..050fabca927 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -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; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index a9d32523aeb..43e15689b93 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -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 => { diff --git a/server/src/types.ts b/server/src/types.ts index d166a94e8b6..9d5ba46e120 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -502,6 +502,9 @@ export interface UserPreferences { showSupportBadge: boolean; hideBuyButtonUntil: string; }; + cast: { + gCastEnabled: boolean; + }; } export interface UserMetadata extends Record> { diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index a013c0b74ed..009dabce58c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => { showSupportBadge: true, hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), }, + cast: { + gCastEnabled: false, + }, }; }; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index f77fbc7f200..f6a46143bc6 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -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(); }); diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte index d331e404320..b7db2c92a29 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -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} + +
+ +
+
+
diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts index fcfb8c382a6..f101c504f04 100644 --- a/web/src/lib/utils/cast/gcast-destination.svelte.ts +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -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 { + 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 diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts new file mode 100644 index 00000000000..d531bc1a99f --- /dev/null +++ b/web/src/test-data/factories/preferences-factory.ts @@ -0,0 +1,43 @@ +import type { UserPreferencesResponseDto } from '@immich/sdk'; +import { Sync } from 'factory.ts'; + +export const preferencesFactory = Sync.makeFactory({ + 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, + }, +});