mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-12 13:39:14 +01:00
Enable Basic Desktop Modal Support (#11484)
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
This commit is contained in:
parent
3b9be21fd7
commit
7e6f2fa798
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -119,6 +119,7 @@ apps/browser/src/autofill @bitwarden/team-autofill-dev
|
|||||||
apps/desktop/src/autofill @bitwarden/team-autofill-dev
|
apps/desktop/src/autofill @bitwarden/team-autofill-dev
|
||||||
libs/common/src/autofill @bitwarden/team-autofill-dev
|
libs/common/src/autofill @bitwarden/team-autofill-dev
|
||||||
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||||
|
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev
|
||||||
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
|
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
|
||||||
# DuckDuckGo integration
|
# DuckDuckGo integration
|
||||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||||
|
@ -57,6 +57,7 @@ import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
|||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||||
|
|
||||||
|
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||||
import { SendComponent } from "./tools/send/send.component";
|
import { SendComponent } from "./tools/send/send.component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,6 +178,10 @@ const routes: Routes = [
|
|||||||
component: RemovePasswordComponent,
|
component: RemovePasswordComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "passkeys",
|
||||||
|
component: Fido2PlaceholderComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: AnonLayoutWrapperComponent,
|
component: AnonLayoutWrapperComponent,
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
style="background:white; display:flex; justify-content: center; align-items: center; flex-direction: column"
|
||||||
|
>
|
||||||
|
<h1 style="color: black">Select your passkey</h1>
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
style="color:black; padding: 10px 20px; border: 1px solid black; margin: 10px"
|
||||||
|
bitButton
|
||||||
|
type="button"
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="closeModal()"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class Fido2PlaceholderComponent {
|
||||||
|
constructor(
|
||||||
|
private readonly desktopSettingsService: DesktopSettingsService,
|
||||||
|
private readonly router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async closeModal() {
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
|
await this.desktopSettingsService.setInModalMode(false);
|
||||||
|
}
|
||||||
|
}
|
@ -284,6 +284,8 @@ export class Main {
|
|||||||
this.migrationRunner.run().then(
|
this.migrationRunner.run().then(
|
||||||
async () => {
|
async () => {
|
||||||
await this.toggleHardwareAcceleration();
|
await this.toggleHardwareAcceleration();
|
||||||
|
// Reset modal mode to make sure main window is displayed correctly
|
||||||
|
await this.desktopSettingsService.resetInModalMode();
|
||||||
await this.windowMain.init();
|
await this.windowMain.init();
|
||||||
await this.i18nService.init();
|
await this.i18nService.init();
|
||||||
await this.messagingMain.init();
|
await this.messagingMain.init();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import * as url from "url";
|
||||||
|
|
||||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
@ -9,6 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
|
import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||||
|
import { cleanUserAgent, isDev } from "../utils";
|
||||||
|
|
||||||
import { WindowMain } from "./window.main";
|
import { WindowMain } from "./window.main";
|
||||||
|
|
||||||
@ -49,6 +51,11 @@ export class TrayMain {
|
|||||||
label: this.i18nService.t("showHide"),
|
label: this.i18nService.t("showHide"),
|
||||||
click: () => this.toggleWindow(),
|
click: () => this.toggleWindow(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
visible: isDev(),
|
||||||
|
label: "Fake Popup",
|
||||||
|
click: () => this.fakePopup(),
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: this.i18nService.t("exit"),
|
label: this.i18nService.t("exit"),
|
||||||
@ -190,7 +197,7 @@ export class TrayMain {
|
|||||||
this.hideDock();
|
this.hideDock();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.windowMain.win.show();
|
this.windowMain.show();
|
||||||
if (this.isDarwin()) {
|
if (this.isDarwin()) {
|
||||||
this.showDock();
|
this.showDock();
|
||||||
}
|
}
|
||||||
@ -203,4 +210,38 @@ export class TrayMain {
|
|||||||
this.windowMain.win.close();
|
this.windowMain.win.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to test modal behavior during development and could be removed in the future.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async fakePopup() {
|
||||||
|
if (this.windowMain.win == null || this.windowMain.win.isDestroyed()) {
|
||||||
|
await this.windowMain.createWindow("modal-app");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restyle existing
|
||||||
|
const existingWin = this.windowMain.win;
|
||||||
|
|
||||||
|
await this.desktopSettingsService.setInModalMode(true);
|
||||||
|
await existingWin.loadURL(
|
||||||
|
url.format({
|
||||||
|
protocol: "file:",
|
||||||
|
//pathname: `${__dirname}/index.html`,
|
||||||
|
pathname: path.join(__dirname, "/index.html"),
|
||||||
|
slashes: true,
|
||||||
|
hash: "/passkeys",
|
||||||
|
query: {
|
||||||
|
redirectUrl: "/passkeys",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
userAgent: cleanUserAgent(existingWin.webContents.userAgent),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
existingWin.once("ready-to-show", () => {
|
||||||
|
existingWin.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import * as path from "path";
|
|||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
|
|
||||||
import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron";
|
import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { concatMap, firstValueFrom, pairwise } from "rxjs";
|
||||||
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@ -14,6 +14,7 @@ import { processisolations } from "@bitwarden/desktop-napi";
|
|||||||
import { BiometricStateService } from "@bitwarden/key-management";
|
import { BiometricStateService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { WindowState } from "../platform/models/domain/window-state";
|
import { WindowState } from "../platform/models/domain/window-state";
|
||||||
|
import { applyMainWindowStyles, applyPopupModalStyles } from "../platform/popup-modal-styles";
|
||||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||||
import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils";
|
import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils";
|
||||||
|
|
||||||
@ -77,6 +78,24 @@ export class WindowMain {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.desktopSettingsService.inModalMode$
|
||||||
|
.pipe(
|
||||||
|
pairwise(),
|
||||||
|
concatMap(async ([lastValue, newValue]) => {
|
||||||
|
if (lastValue && !newValue) {
|
||||||
|
// Reset the window state to the main window state
|
||||||
|
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||||
|
// Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode.
|
||||||
|
this.win.hide();
|
||||||
|
} else if (!lastValue && newValue) {
|
||||||
|
// Apply the popup modal styles
|
||||||
|
applyPopupModalStyles(this.win);
|
||||||
|
this.win.show();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => {
|
this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => {
|
||||||
if (this.win == null) {
|
if (this.win == null) {
|
||||||
return;
|
return;
|
||||||
@ -182,7 +201,20 @@ export class WindowMain {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createWindow(): Promise<void> {
|
/// Show the window with main window styles
|
||||||
|
show() {
|
||||||
|
if (this.win != null) {
|
||||||
|
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||||
|
this.win.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main window. The template argument is used to determine the styling of the window and what url will be loaded.
|
||||||
|
* When the template is "modal-app", the window will be styled as a modal and the passkeys page will be loaded.
|
||||||
|
* TODO: We might want to refactor the template argument to accomodate more target pages, e.g. ssh-agent.
|
||||||
|
*/
|
||||||
|
async createWindow(template: "full-app" | "modal-app" = "full-app"): Promise<void> {
|
||||||
this.windowStates[mainWindowSizeKey] = await this.getWindowState(
|
this.windowStates[mainWindowSizeKey] = await this.getWindowState(
|
||||||
this.defaultWidth,
|
this.defaultWidth,
|
||||||
this.defaultHeight,
|
this.defaultHeight,
|
||||||
@ -216,6 +248,12 @@ export class WindowMain {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (template === "modal-app") {
|
||||||
|
applyPopupModalStyles(this.win);
|
||||||
|
} else {
|
||||||
|
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||||
|
}
|
||||||
|
|
||||||
this.win.webContents.on("dom-ready", () => {
|
this.win.webContents.on("dom-ready", () => {
|
||||||
this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0;
|
this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0;
|
||||||
});
|
});
|
||||||
@ -225,21 +263,41 @@ export class WindowMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show it later since it might need to be maximized.
|
// Show it later since it might need to be maximized.
|
||||||
this.win.show();
|
// use once event to avoid flash on unstyled content.
|
||||||
|
this.win.once("ready-to-show", () => {
|
||||||
|
this.win.show();
|
||||||
|
});
|
||||||
|
|
||||||
// and load the index.html of the app.
|
if (template === "full-app") {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// and load the index.html of the app.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
this.win.loadURL(
|
void this.win.loadURL(
|
||||||
url.format({
|
url.format({
|
||||||
protocol: "file:",
|
protocol: "file:",
|
||||||
pathname: path.join(__dirname, "/index.html"),
|
pathname: path.join(__dirname, "/index.html"),
|
||||||
slashes: true,
|
slashes: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// we're in modal mode - load the passkeys page
|
||||||
|
await this.win.loadURL(
|
||||||
|
url.format({
|
||||||
|
protocol: "file:",
|
||||||
|
pathname: path.join(__dirname, "/index.html"),
|
||||||
|
slashes: true,
|
||||||
|
hash: "/passkeys",
|
||||||
|
query: {
|
||||||
|
redirectUrl: "/passkeys",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
@ -336,6 +394,12 @@ export class WindowMain {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inModalMode = await firstValueFrom(this.desktopSettingsService.inModalMode$);
|
||||||
|
|
||||||
|
if (inModalMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bounds = win.getBounds();
|
const bounds = win.getBounds();
|
||||||
|
|
||||||
@ -346,9 +410,14 @@ export class WindowMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.windowStates[configKey].isMaximized = win.isMaximized();
|
// We treat fullscreen as maximized (would be even better to store isFullscreen as its own flag).
|
||||||
|
this.windowStates[configKey].isMaximized = win.isMaximized() || win.isFullScreen();
|
||||||
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
|
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
|
||||||
|
|
||||||
|
// Maybe store these as well?
|
||||||
|
// win.isFocused();
|
||||||
|
// win.isVisible();
|
||||||
|
|
||||||
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
|
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
|
||||||
this.windowStates[configKey].x = bounds.x;
|
this.windowStates[configKey].x = bounds.x;
|
||||||
this.windowStates[configKey].y = bounds.y;
|
this.windowStates[configKey].y = bounds.y;
|
||||||
|
52
apps/desktop/src/platform/popup-modal-styles.ts
Normal file
52
apps/desktop/src/platform/popup-modal-styles.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
import { WindowState } from "./models/domain/window-state";
|
||||||
|
|
||||||
|
// change as needed, however limited by mainwindow minimum size
|
||||||
|
const popupWidth = 680;
|
||||||
|
const popupHeight = 500;
|
||||||
|
|
||||||
|
export function applyPopupModalStyles(window: BrowserWindow) {
|
||||||
|
window.unmaximize();
|
||||||
|
window.setSize(popupWidth, popupHeight);
|
||||||
|
window.center();
|
||||||
|
window.setWindowButtonVisibility?.(false);
|
||||||
|
window.setMenuBarVisibility?.(false);
|
||||||
|
window.setResizable(false);
|
||||||
|
window.setAlwaysOnTop(true);
|
||||||
|
|
||||||
|
// Adjusting from full screen is a bit more hassle
|
||||||
|
if (window.isFullScreen()) {
|
||||||
|
window.setFullScreen(false);
|
||||||
|
window.once("leave-full-screen", () => {
|
||||||
|
window.setSize(popupWidth, popupHeight);
|
||||||
|
window.center();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) {
|
||||||
|
window.setMinimumSize(680, 500);
|
||||||
|
|
||||||
|
// need to guard against null/undefined values
|
||||||
|
|
||||||
|
if (existingWindowState?.width && existingWindowState?.height) {
|
||||||
|
window.setSize(existingWindowState.width, existingWindowState.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingWindowState?.x && existingWindowState?.y) {
|
||||||
|
window.setPosition(existingWindowState.x, existingWindowState.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setWindowButtonVisibility?.(true);
|
||||||
|
window.setMenuBarVisibility?.(true);
|
||||||
|
window.setResizable(true);
|
||||||
|
window.setAlwaysOnTop(false);
|
||||||
|
|
||||||
|
// We're currently not recovering the maximized state, mostly due to conflicts with hiding the window.
|
||||||
|
// window.setFullScreen(existingWindowState.isMaximized);
|
||||||
|
|
||||||
|
// if (existingWindowState.isMaximized) {
|
||||||
|
// window.maximize();
|
||||||
|
// }
|
||||||
|
}
|
@ -75,6 +75,10 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
|
|||||||
clearOn: [], // User setting, no need to clear
|
clearOn: [], // User setting, no need to clear
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const IN_MODAL_MODE = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "inModalMode", {
|
||||||
|
deserializer: (b) => b,
|
||||||
|
});
|
||||||
|
|
||||||
const PREVENT_SCREENSHOTS = new KeyDefinition<boolean>(
|
const PREVENT_SCREENSHOTS = new KeyDefinition<boolean>(
|
||||||
DESKTOP_SETTINGS_DISK,
|
DESKTOP_SETTINGS_DISK,
|
||||||
"preventScreenshots",
|
"preventScreenshots",
|
||||||
@ -170,6 +174,10 @@ export class DesktopSettingsService {
|
|||||||
*/
|
*/
|
||||||
minimizeOnCopy$ = this.minimizeOnCopyState.state$.pipe(map(Boolean));
|
minimizeOnCopy$ = this.minimizeOnCopyState.state$.pipe(map(Boolean));
|
||||||
|
|
||||||
|
private readonly inModalModeState = this.stateProvider.getGlobal(IN_MODAL_MODE);
|
||||||
|
|
||||||
|
inModalMode$ = this.inModalModeState.state$.pipe(map(Boolean));
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
constructor(private stateProvider: StateProvider) {
|
||||||
this.window$ = this.windowState.state$.pipe(
|
this.window$ = this.windowState.state$.pipe(
|
||||||
map((window) =>
|
map((window) =>
|
||||||
@ -178,6 +186,14 @@ export class DesktopSettingsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to clear the setting on application start to make sure we don't end up
|
||||||
|
* stuck in modal mode if the application is force-closed in modal mode.
|
||||||
|
*/
|
||||||
|
async resetInModalMode() {
|
||||||
|
await this.inModalModeState.update(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
async setHardwareAcceleration(enabled: boolean) {
|
async setHardwareAcceleration(enabled: boolean) {
|
||||||
await this.hwState.update(() => enabled);
|
await this.hwState.update(() => enabled);
|
||||||
}
|
}
|
||||||
@ -286,6 +302,14 @@ export class DesktopSettingsService {
|
|||||||
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
|
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the modal mode of the application. Setting this changes the windows-size and other properties.
|
||||||
|
* @param value `true` if the application is in modal mode, `false` if it is not.
|
||||||
|
*/
|
||||||
|
async setInModalMode(value: boolean) {
|
||||||
|
await this.inModalModeState.update(() => value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the setting for whether or not the screenshot protection is enabled.
|
* Sets the setting for whether or not the screenshot protection is enabled.
|
||||||
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
|
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
|
||||||
|
Loading…
Reference in New Issue
Block a user