mirror of
https://github.com/immich-app/immich
synced 2025-06-02 17:47:00 +00: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",
|
||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -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)
|
||||
|
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@ -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';
|
||||
|
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@ -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':
|
||||
|
99
mobile/openapi/lib/model/cast_response.dart
generated
Normal file
99
mobile/openapi/lib/model/cast_response.dart
generated
Normal file
@ -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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
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<String, dynamic>();
|
||||
|
||||
return CastResponse(
|
||||
gCastEnabled: mapValueOfType<bool>(json, r'gCastEnabled')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CastResponse> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CastResponse>[];
|
||||
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<String, CastResponse> mapFromJson(dynamic json) {
|
||||
final map = <String, CastResponse>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // 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<String, List<CastResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CastResponse>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
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 = <String>{
|
||||
'gCastEnabled',
|
||||
};
|
||||
}
|
||||
|
108
mobile/openapi/lib/model/cast_update.dart
generated
Normal file
108
mobile/openapi/lib/model/cast_update.dart
generated
Normal file
@ -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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
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<String, dynamic>();
|
||||
|
||||
return CastUpdate(
|
||||
gCastEnabled: mapValueOfType<bool>(json, r'gCastEnabled'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CastUpdate> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CastUpdate>[];
|
||||
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<String, CastUpdate> mapFromJson(dynamic json) {
|
||||
final map = <String, CastUpdate>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // 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<String, List<CastUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CastUpdate>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
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 = <String>{
|
||||
};
|
||||
}
|
||||
|
@ -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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
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<String, dynamic>();
|
||||
|
||||
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 = <String>{
|
||||
'cast',
|
||||
'download',
|
||||
'emailNotifications',
|
||||
'folders',
|
||||
|
@ -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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -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']),
|
||||
|
@ -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…
x
Reference in New Issue
Block a user