Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: save image compression upload setting #7089

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/__e2e__/models/editor.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,23 @@ export class EditorModel {
.replace(" words", "")
);
}

async attachImage() {
await this.page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"]);
await this.page.evaluate(async () => {
const resp = await fetch("https://dummyjson.com/image/150");
const blob = await resp.blob();
window.navigator.clipboard.write([
new ClipboardItem({
"image/png": new Blob([blob], { type: "image/png" })
})
]);
});

await this.page.keyboard.down("Control");
await this.page.keyboard.press("KeyV");
await this.page.keyboard.up("Control");
}
}
11 changes: 11 additions & 0 deletions apps/web/__e2e__/models/settings-view.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,15 @@ export class SettingsViewModel {

await waitForDialog(this.page, "Restoring backup");
}

async selectImageCompression(option: { value: string; label: string }) {
const item = await this.navigation.findItem("Behaviour");
await item?.click();

const imageCompressionDropdown = this.page
.locator(getTestId("setting-image-compression"))
.locator("select");

await imageCompressionDropdown.selectOption(option);
}
}
76 changes: 76 additions & 0 deletions apps/web/__e2e__/settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { test } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE } from "./utils";

test("ask for image compression during image upload when 'Image Compression' setting is 'Ask every time'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "0",
label: "Ask every time"
});
await settings.close();

const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await page.getByText("Enable compression").waitFor({ state: "visible" });
});

test("do not ask for image compression during image upload when 'Image Compression' setting is 'Enable (Recommended)'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "1",
label: "Enable (Recommended)"
});
await settings.close();

const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await page.getByText("Enable compression").waitFor({ state: "hidden" });
});

test("do not ask for image compression during image upload when 'Image Compression' setting is 'Disable'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "2",
label: "Disable"
});
await settings.close();

const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await page.getByText("Enable compression").waitFor({ state: "hidden" });
});
44 changes: 38 additions & 6 deletions apps/web/src/components/editor/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@ import {
hashStream,
writeEncryptedFile
} from "../../interfaces/fs";
import Config from "../../utils/config";
import { compressImage, FileWithURI } from "../../utils/image-compressor";

const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;

function getImageCompressionConfig() {
return Config.get("imageCompression", 0);
}

export async function insertAttachments(type = "*/*") {
if (!isUserPremium()) {
await BuyDialog.show({});
Expand All @@ -58,12 +64,38 @@ export async function attachFiles(files: File[]) {
}

let images = files.filter((f) => f.type.startsWith("image/"));
images =
images.length > 0
? (await ImagePickerDialog.show({
images
})) || []
: [];

switch (getImageCompressionConfig()) {
case 1:
let compressedImages: FileWithURI[] = [];
for (const image of images) {
const compressed = await compressImage(image, {
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
width: (naturalWidth) => naturalWidth,
height: (_, naturalHeight) => naturalHeight,
resize: "contain",
quality: 0.7
});
compressedImages.push(
new FileWithURI([compressed], image.name, {
lastModified: image.lastModified,
type: image.type
})
);
}
images = compressedImages;
break;
case 2:
break;
default:
images =
images.length > 0
? (await ImagePickerDialog.show({
images
})) || []
: [];
}

const documents = files.filter((f) => !f.type.startsWith("image/"));
const attachments: Attachment[] = [];
for (const file of [...images, ...documents]) {
Expand Down
13 changes: 1 addition & 12 deletions apps/web/src/dialogs/image-picker-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,13 @@ import Dialog from "../components/dialog";
import { ScrollContainer } from "@notesnook/ui";
import { Flex, Image, Label, Text } from "@theme-ui/components";
import { formatBytes } from "@notesnook/common";
import { compressImage } from "../utils/image-compressor";
import { compressImage, FileWithURI } from "../utils/image-compressor";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";

export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & {
images: File[];
};
class FileWithURI extends File {
uri: string;
constructor(
fileBits: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(fileBits, fileName, options);
this.uri = URL.createObjectURL(this);
}
}

export const ImagePickerDialog = DialogManager.register(
function ImagePickerDialog(props: ImagePickerDialogProps) {
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/dialogs/settings/behaviour-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ export const BehaviourSettings: SettingsGroup[] = [
]
}
]
},
{
key: "image-compression",
title: strings.imageCompression(),
description: strings.imageCompressionDesc(),
keywords: ["compress images", "image quality"],
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.imageCompression, listener),
components: [
{
type: "dropdown",
onSelectionChanged: (value) =>
useSettingStore.getState().setImageCompression(parseInt(value)),
selectedOption: () =>
useSettingStore.getState().imageCompression.toString(),
options: [
{ value: "0", title: strings.askEveryTime() },
{
value: "1",
title: `${strings.enable()} (${strings.recommended()})`
},
{ value: "2", title: strings.disable() }
]
}
]
}
]
},
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/stores/setting-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class SettingStore extends BaseStore<SettingStore> {

trashCleanupInterval: TrashCleanupInterval = 7;
homepage = Config.get("homepage", 0);
imageCompression = Config.get("imageCompression", 0);
desktopIntegrationSettings?: DesktopIntegration;
autoUpdates = true;
isFlatpak = false;
Expand Down Expand Up @@ -125,6 +126,11 @@ class SettingStore extends BaseStore<SettingStore> {
Config.set("homepage", homepage);
};

setImageCompression = (imageCompression: number) => {
this.set({ imageCompression });
Config.set("imageCompression", imageCompression);
};

setDesktopIntegration = async (settings: DesktopIntegration) => {
const { desktopIntegrationSettings } = this.get();

Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/utils/image-compressor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export class FileWithURI extends File {
uri: string;
constructor(
fileBits: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(fileBits, fileName, options);
this.uri = URL.createObjectURL(this);
}
}

type DeriveDimension = (naturalWidth: number, naturalHeight: number) => number;

interface CompressorOptions {
Expand Down
Loading
Loading