web: save image compression upload setting (#7089)

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-01-02 15:10:58 +05:00
committed by GitHub
parent 2357e3de0b
commit 0be59c71de
11 changed files with 2087 additions and 1857 deletions

View File

@@ -242,4 +242,23 @@ export class EditorModel {
if ((await tabModel.getId()) === id) return tabModel; if ((await tabModel.getId()) === id) return tabModel;
} }
} }
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");
}
} }

View File

@@ -139,4 +139,15 @@ export class SettingsViewModel {
await waitForDialog(this.page, "Restoring backup"); 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);
}
} }

View File

@@ -0,0 +1,79 @@
/*
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, expect } 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 expect(page.getByText("Enable compression")).toBeVisible();
});
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 expect(page.getByText("Enable compression")).toBeHidden();
});
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 expect(page.getByText("Enable compression")).toBeHidden();
});

View File

@@ -33,6 +33,9 @@ import {
hashStream, hashStream,
writeEncryptedFile writeEncryptedFile
} from "../../interfaces/fs"; } from "../../interfaces/fs";
import Config from "../../utils/config";
import { compressImage, FileWithURI } from "../../utils/image-compressor";
import { ImageCompressionOptions } from "../../stores/setting-store";
const FILE_SIZE_LIMIT = 500 * 1024 * 1024; const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024; const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
@@ -58,12 +61,43 @@ export async function attachFiles(files: File[]) {
} }
let images = files.filter((f) => f.type.startsWith("image/")); let images = files.filter((f) => f.type.startsWith("image/"));
const imageCompressionConfig = Config.get<ImageCompressionOptions>(
"imageCompression",
ImageCompressionOptions.ASK_EVERY_TIME
);
switch (imageCompressionConfig) {
case ImageCompressionOptions.ENABLE: {
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 ImageCompressionOptions.DISABLE:
break;
default:
images = images =
images.length > 0 images.length > 0
? (await ImagePickerDialog.show({ ? (await ImagePickerDialog.show({
images images
})) || [] })) || []
: []; : [];
}
const documents = files.filter((f) => !f.type.startsWith("image/")); const documents = files.filter((f) => !f.type.startsWith("image/"));
const attachments: Attachment[] = []; const attachments: Attachment[] = [];
for (const file of [...images, ...documents]) { for (const file of [...images, ...documents]) {

View File

@@ -22,24 +22,13 @@ import Dialog from "../components/dialog";
import { ScrollContainer } from "@notesnook/ui"; import { ScrollContainer } from "@notesnook/ui";
import { Flex, Image, Label, Text } from "@theme-ui/components"; import { Flex, Image, Label, Text } from "@theme-ui/components";
import { formatBytes } from "@notesnook/common"; 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 { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & { export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & {
images: 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( export const ImagePickerDialog = DialogManager.register(
function ImagePickerDialog(props: ImagePickerDialogProps) { function ImagePickerDialog(props: ImagePickerDialogProps) {

View File

@@ -19,7 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { DATE_FORMATS } from "@notesnook/core"; import { DATE_FORMATS } from "@notesnook/core";
import { SettingsGroup } from "./types"; import { SettingsGroup } from "./types";
import { useStore as useSettingStore } from "../../stores/setting-store"; import {
ImageCompressionOptions,
useStore as useSettingStore
} from "../../stores/setting-store";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { isUserPremium } from "../../hooks/use-is-user-premium"; import { isUserPremium } from "../../hooks/use-is-user-premium";
import { TimeFormat } from "@notesnook/core"; import { TimeFormat } from "@notesnook/core";
@@ -55,6 +58,37 @@ 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: ImageCompressionOptions.ASK_EVERY_TIME.toString(),
title: strings.askEveryTime()
},
{
value: ImageCompressionOptions.ENABLE.toString(),
title: strings.enableRecommended()
},
{
value: ImageCompressionOptions.DISABLE.toString(),
title: strings.disable()
}
]
}
]
} }
] ]
}, },

View File

@@ -35,6 +35,13 @@ export const HostIds = [
"MONOGRAPH_HOST" "MONOGRAPH_HOST"
] as const; ] as const;
export type HostId = (typeof HostIds)[number]; export type HostId = (typeof HostIds)[number];
export enum ImageCompressionOptions {
ASK_EVERY_TIME,
ENABLE,
DISABLE
}
class SettingStore extends BaseStore<SettingStore> { class SettingStore extends BaseStore<SettingStore> {
encryptBackups = Config.get("encryptBackups", false); encryptBackups = Config.get("encryptBackups", false);
backupReminderOffset = Config.get("backupReminderOffset", 0); backupReminderOffset = Config.get("backupReminderOffset", 0);
@@ -60,6 +67,10 @@ class SettingStore extends BaseStore<SettingStore> {
trashCleanupInterval: TrashCleanupInterval = 7; trashCleanupInterval: TrashCleanupInterval = 7;
homepage = Config.get("homepage", 0); homepage = Config.get("homepage", 0);
imageCompression = Config.get(
"imageCompression",
ImageCompressionOptions.ASK_EVERY_TIME
);
desktopIntegrationSettings?: DesktopIntegration; desktopIntegrationSettings?: DesktopIntegration;
autoUpdates = true; autoUpdates = true;
isFlatpak = false; isFlatpak = false;
@@ -125,6 +136,11 @@ class SettingStore extends BaseStore<SettingStore> {
Config.set("homepage", homepage); Config.set("homepage", homepage);
}; };
setImageCompression = (imageCompression: ImageCompressionOptions) => {
this.set({ imageCompression });
Config.set("imageCompression", imageCompression);
};
setDesktopIntegration = async (settings: DesktopIntegration) => { setDesktopIntegration = async (settings: DesktopIntegration) => {
const { desktopIntegrationSettings } = this.get(); const { desktopIntegrationSettings } = this.get();

View File

@@ -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/>. 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; type DeriveDimension = (naturalWidth: number, naturalHeight: number) => number;
interface CompressorOptions { interface CompressorOptions {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1111,6 +1111,10 @@ $headline$: Use starting line of the note as title.`,
behaviorDesc: () => t`Change how the app behaves in different situations`, behaviorDesc: () => t`Change how the app behaves in different situations`,
homepage: () => t`Homepage`, homepage: () => t`Homepage`,
homepageDesc: () => t`Default screen to open on app launch`, homepageDesc: () => t`Default screen to open on app launch`,
imageCompression: () => t`Image Compression`,
imageCompressionDesc: () => t`Compress images before uploading`,
askEveryTime: () => t`Ask every time`,
enableRecommended: () => t`Enable (Recommended)`,
dateFormat: () => t`Date format`, dateFormat: () => t`Date format`,
dateFormatDesc: () => t`Choose how dates are displayed in the app`, dateFormatDesc: () => t`Choose how dates are displayed in the app`,
timeFormat: () => t`Time format`, timeFormat: () => t`Time format`,