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_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",

View File

@ -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)

View File

@ -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';

View File

@ -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':

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 {
/// 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',

View File

@ -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"
},

View File

@ -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;

View File

@ -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 => {

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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