make google casting opt-in

This commit is contained in:
bwees 2025-05-22 22:29:46 -05:00
parent 38b3663d8b
commit c237e0f3a1
No known key found for this signature in database
14 changed files with 193 additions and 8 deletions

View File

@ -605,6 +605,7 @@
"cannot_undo_this_action": "You cannot undo this action!", "cannot_undo_this_action": "You cannot undo this action!",
"cannot_update_the_description": "Cannot update the description", "cannot_update_the_description": "Cannot update the description",
"cast": "Cast", "cast": "Cast",
"cast_description": "Configure available cast destinations",
"change_date": "Change date", "change_date": "Change date",
"change_description": "Change description", "change_description": "Change description",
"change_display_order": "Change display order", "change_display_order": "Change display order",
@ -1028,6 +1029,8 @@
"folders": "Folders", "folders": "Folders",
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
"forward": "Forward", "forward": "Forward",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General", "general": "General",
"get_help": "Get Help", "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", "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",

View File

@ -319,6 +319,7 @@ Class | Method | HTTP request | Description
- [BulkIdsDto](doc//BulkIdsDto.md) - [BulkIdsDto](doc//BulkIdsDto.md)
- [CLIPConfig](doc//CLIPConfig.md) - [CLIPConfig](doc//CLIPConfig.md)
- [CQMode](doc//CQMode.md) - [CQMode](doc//CQMode.md)
- [CastResponse](doc//CastResponse.md)
- [ChangePasswordDto](doc//ChangePasswordDto.md) - [ChangePasswordDto](doc//ChangePasswordDto.md)
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)

View File

@ -114,6 +114,7 @@ part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart'; part 'model/bulk_ids_dto.dart';
part 'model/clip_config.dart'; part 'model/clip_config.dart';
part 'model/cq_mode.dart'; part 'model/cq_mode.dart';
part 'model/cast_response.dart';
part 'model/change_password_dto.dart'; part 'model/change_password_dto.dart';
part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart'; part 'model/check_existing_assets_response_dto.dart';

View File

@ -284,6 +284,8 @@ class ApiClient {
return CLIPConfig.fromJson(value); return CLIPConfig.fromJson(value);
case 'CQMode': case 'CQMode':
return CQModeTypeTransformer().decode(value); return CQModeTypeTransformer().decode(value);
case 'CastResponse':
return CastResponse.fromJson(value);
case 'ChangePasswordDto': case 'ChangePasswordDto':
return ChangePasswordDto.fromJson(value); return ChangePasswordDto.fromJson(value);
case 'CheckExistingAssetsDto': case 'CheckExistingAssetsDto':

View 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',
};
}

View File

@ -13,6 +13,7 @@ part of openapi.api;
class UserPreferencesResponseDto { class UserPreferencesResponseDto {
/// Returns a new [UserPreferencesResponseDto] instance. /// Returns a new [UserPreferencesResponseDto] instance.
UserPreferencesResponseDto({ UserPreferencesResponseDto({
required this.cast,
required this.download, required this.download,
required this.emailNotifications, required this.emailNotifications,
required this.folders, required this.folders,
@ -24,6 +25,8 @@ class UserPreferencesResponseDto {
required this.tags, required this.tags,
}); });
CastResponse cast;
DownloadResponse download; DownloadResponse download;
EmailNotificationsResponse emailNotifications; EmailNotificationsResponse emailNotifications;
@ -44,6 +47,7 @@ class UserPreferencesResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
other.cast == cast &&
other.download == download && other.download == download &&
other.emailNotifications == emailNotifications && other.emailNotifications == emailNotifications &&
other.folders == folders && other.folders == folders &&
@ -57,6 +61,7 @@ class UserPreferencesResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(cast.hashCode) +
(download.hashCode) + (download.hashCode) +
(emailNotifications.hashCode) + (emailNotifications.hashCode) +
(folders.hashCode) + (folders.hashCode) +
@ -68,10 +73,11 @@ class UserPreferencesResponseDto {
(tags.hashCode); (tags.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'cast'] = this.cast;
json[r'download'] = this.download; json[r'download'] = this.download;
json[r'emailNotifications'] = this.emailNotifications; json[r'emailNotifications'] = this.emailNotifications;
json[r'folders'] = this.folders; json[r'folders'] = this.folders;
@ -93,6 +99,7 @@ class UserPreferencesResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserPreferencesResponseDto( return UserPreferencesResponseDto(
cast: CastResponse.fromJson(json[r'cast'])!,
download: DownloadResponse.fromJson(json[r'download'])!, download: DownloadResponse.fromJson(json[r'download'])!,
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
folders: FoldersResponse.fromJson(json[r'folders'])!, folders: FoldersResponse.fromJson(json[r'folders'])!,
@ -149,6 +156,7 @@ class UserPreferencesResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'cast',
'download', 'download',
'emailNotifications', 'emailNotifications',
'folders', 'folders',

View File

@ -9484,6 +9484,26 @@
], ],
"type": "string" "type": "string"
}, },
"CastResponse": {
"properties": {
"gCastEnabled": {
"default": false,
"type": "boolean"
}
},
"required": [
"gCastEnabled"
],
"type": "object"
},
"CastUpdate": {
"properties": {
"gCastEnabled": {
"type": "boolean"
}
},
"type": "object"
},
"ChangePasswordDto": { "ChangePasswordDto": {
"properties": { "properties": {
"newPassword": { "newPassword": {
@ -14734,6 +14754,9 @@
}, },
"UserPreferencesResponseDto": { "UserPreferencesResponseDto": {
"properties": { "properties": {
"cast": {
"$ref": "#/components/schemas/CastResponse"
},
"download": { "download": {
"$ref": "#/components/schemas/DownloadResponse" "$ref": "#/components/schemas/DownloadResponse"
}, },
@ -14763,6 +14786,7 @@
} }
}, },
"required": [ "required": [
"cast",
"download", "download",
"emailNotifications", "emailNotifications",
"folders", "folders",
@ -14780,6 +14804,9 @@
"avatar": { "avatar": {
"$ref": "#/components/schemas/AvatarUpdate" "$ref": "#/components/schemas/AvatarUpdate"
}, },
"cast": {
"$ref": "#/components/schemas/CastUpdate"
},
"download": { "download": {
"$ref": "#/components/schemas/DownloadUpdate" "$ref": "#/components/schemas/DownloadUpdate"
}, },

View File

@ -128,6 +128,9 @@ export type UserAdminUpdateDto = {
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
storageLabel?: string | null; storageLabel?: string | null;
}; };
export type CastResponse = {
gCastEnabled: boolean;
};
export type DownloadResponse = { export type DownloadResponse = {
archiveSize: number; archiveSize: number;
includeEmbeddedVideos: boolean; includeEmbeddedVideos: boolean;
@ -164,6 +167,7 @@ export type TagsResponse = {
sidebarWeb: boolean; sidebarWeb: boolean;
}; };
export type UserPreferencesResponseDto = { export type UserPreferencesResponseDto = {
cast: CastResponse;
download: DownloadResponse; download: DownloadResponse;
emailNotifications: EmailNotificationsResponse; emailNotifications: EmailNotificationsResponse;
folders: FoldersResponse; folders: FoldersResponse;
@ -177,6 +181,9 @@ export type UserPreferencesResponseDto = {
export type AvatarUpdate = { export type AvatarUpdate = {
color?: UserAvatarColor; color?: UserAvatarColor;
}; };
export type CastUpdate = {
gCastEnabled?: boolean;
};
export type DownloadUpdate = { export type DownloadUpdate = {
archiveSize?: number; archiveSize?: number;
includeEmbeddedVideos?: boolean; includeEmbeddedVideos?: boolean;
@ -214,6 +221,7 @@ export type TagsUpdate = {
}; };
export type UserPreferencesUpdateDto = { export type UserPreferencesUpdateDto = {
avatar?: AvatarUpdate; avatar?: AvatarUpdate;
cast?: CastUpdate;
download?: DownloadUpdate; download?: DownloadUpdate;
emailNotifications?: EmailNotificationsUpdate; emailNotifications?: EmailNotificationsUpdate;
folders?: FoldersUpdate; folders?: FoldersUpdate;

View File

@ -85,6 +85,11 @@ class PurchaseUpdate {
hideBuyButtonUntil?: string; hideBuyButtonUntil?: string;
} }
class CastUpdate {
@ValidateBoolean({ optional: true })
gCastEnabled?: boolean;
}
export class UserPreferencesUpdateDto { export class UserPreferencesUpdateDto {
@Optional() @Optional()
@ValidateNested() @ValidateNested()
@ -135,6 +140,11 @@ export class UserPreferencesUpdateDto {
@ValidateNested() @ValidateNested()
@Type(() => PurchaseUpdate) @Type(() => PurchaseUpdate)
purchase?: PurchaseUpdate; purchase?: PurchaseUpdate;
@Optional()
@ValidateNested()
@Type(() => CastUpdate)
cast?: CastUpdate;
} }
class RatingsResponse { class RatingsResponse {
@ -183,6 +193,10 @@ class PurchaseResponse {
hideBuyButtonUntil!: string; hideBuyButtonUntil!: string;
} }
class CastResponse {
gCastEnabled: boolean = false;
}
export class UserPreferencesResponseDto implements UserPreferences { export class UserPreferencesResponseDto implements UserPreferences {
folders!: FoldersResponse; folders!: FoldersResponse;
memories!: MemoriesResponse; memories!: MemoriesResponse;
@ -193,6 +207,7 @@ export class UserPreferencesResponseDto implements UserPreferences {
emailNotifications!: EmailNotificationsResponse; emailNotifications!: EmailNotificationsResponse;
download!: DownloadResponse; download!: DownloadResponse;
purchase!: PurchaseResponse; purchase!: PurchaseResponse;
cast!: CastResponse;
} }
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {

View File

@ -502,6 +502,9 @@ export interface UserPreferences {
showSupportBadge: boolean; showSupportBadge: boolean;
hideBuyButtonUntil: string; hideBuyButtonUntil: string;
}; };
cast: {
gCastEnabled: boolean;
};
} }
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> { export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {

View File

@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => {
showSupportBadge: true, showSupportBadge: true,
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
}, },
cast: {
gCastEnabled: false,
},
}; };
}; };

View File

@ -168,13 +168,6 @@
bind:checked={$showDeleteModal} bind:checked={$showDeleteModal}
/> />
</div> </div>
<div class="ms-4">
<SettingSwitch
title={$t('enable_gcast')}
subtitle={$t('enable_gcast_description')}
bind:checked={$gcastEnabled}
/>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -34,6 +34,9 @@
let tagsEnabled = $state($preferences?.tags?.enabled ?? false); let tagsEnabled = $state($preferences?.tags?.enabled ?? false);
let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false); let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false);
// Cast
let gCastEnabled = $state($preferences?.cast?.gCastEnabled ?? false);
const handleSave = async () => { const handleSave = async () => {
try { try {
const data = await updateMyPreferences({ const data = await updateMyPreferences({
@ -44,6 +47,7 @@
ratings: { enabled: ratingsEnabled }, ratings: { enabled: ratingsEnabled },
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar }, sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar }, tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
cast: { gCastEnabled },
}, },
}); });
@ -138,6 +142,16 @@
{/if} {/if}
</SettingAccordion> </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"> <div class="flex justify-end">
<Button shape="round" type="submit" size="small" onclick={() => handleSave()}>{$t('save')}</Button> <Button shape="round" type="submit" size="small" onclick={() => handleSave()}>{$t('save')}</Button>
</div> </div>

View File

@ -1,6 +1,8 @@
import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte'; import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte';
import { preferences } from '$lib/stores/user.store';
import 'chromecast-caf-sender'; import 'chromecast-caf-sender';
import { Duration } from 'luxon'; 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'; 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; private currentUrl: string | null = null;
async initialize(): Promise<boolean> { 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 // 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 // in the form of a global window event. We will give Chrome 3 seconds to respond
// or we will mark the destination as unavailable // or we will mark the destination as unavailable