mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
make google casting opt-in
This commit is contained in:
parent
38b3663d8b
commit
c237e0f3a1
@ -605,6 +605,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",
|
||||
@ -1028,6 +1029,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",
|
||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -319,6 +319,7 @@ Class | Method | HTTP request | Description
|
||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||
- [CLIPConfig](doc//CLIPConfig.md)
|
||||
- [CQMode](doc//CQMode.md)
|
||||
- [CastResponse](doc//CastResponse.md)
|
||||
- [ChangePasswordDto](doc//ChangePasswordDto.md)
|
||||
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
|
||||
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -114,6 +114,7 @@ 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/change_password_dto.dart';
|
||||
part 'model/check_existing_assets_dto.dart';
|
||||
part 'model/check_existing_assets_response_dto.dart';
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -284,6 +284,8 @@ class ApiClient {
|
||||
return CLIPConfig.fromJson(value);
|
||||
case 'CQMode':
|
||||
return CQModeTypeTransformer().decode(value);
|
||||
case 'CastResponse':
|
||||
return CastResponse.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',
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -9484,6 +9484,26 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CastResponse": {
|
||||
"properties": {
|
||||
"gCastEnabled": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"gCastEnabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CastUpdate": {
|
||||
"properties": {
|
||||
"gCastEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ChangePasswordDto": {
|
||||
"properties": {
|
||||
"newPassword": {
|
||||
@ -14734,6 +14754,9 @@
|
||||
},
|
||||
"UserPreferencesResponseDto": {
|
||||
"properties": {
|
||||
"cast": {
|
||||
"$ref": "#/components/schemas/CastResponse"
|
||||
},
|
||||
"download": {
|
||||
"$ref": "#/components/schemas/DownloadResponse"
|
||||
},
|
||||
@ -14763,6 +14786,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cast",
|
||||
"download",
|
||||
"emailNotifications",
|
||||
"folders",
|
||||
@ -14780,6 +14804,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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -168,13 +168,6 @@
|
||||
bind:checked={$showDeleteModal}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('enable_gcast')}
|
||||
subtitle={$t('enable_gcast_description')}
|
||||
bind:checked={$gcastEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user