mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
web: save image compression upload setting (#7089)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -242,4 +242,23 @@ export class EditorModel {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
79
apps/web/__e2e__/settings.test.ts
Normal file
79
apps/web/__e2e__/settings.test.ts
Normal 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();
|
||||
});
|
||||
@@ -33,6 +33,9 @@ import {
|
||||
hashStream,
|
||||
writeEncryptedFile
|
||||
} 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 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/"));
|
||||
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.length > 0
|
||||
? (await ImagePickerDialog.show({
|
||||
images
|
||||
})) || []
|
||||
: [];
|
||||
}
|
||||
|
||||
const documents = files.filter((f) => !f.type.startsWith("image/"));
|
||||
const attachments: Attachment[] = [];
|
||||
for (const file of [...images, ...documents]) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -19,7 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { DATE_FORMATS } from "@notesnook/core";
|
||||
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 { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
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()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -35,6 +35,13 @@ export const HostIds = [
|
||||
"MONOGRAPH_HOST"
|
||||
] as const;
|
||||
export type HostId = (typeof HostIds)[number];
|
||||
|
||||
export enum ImageCompressionOptions {
|
||||
ASK_EVERY_TIME,
|
||||
ENABLE,
|
||||
DISABLE
|
||||
}
|
||||
|
||||
class SettingStore extends BaseStore<SettingStore> {
|
||||
encryptBackups = Config.get("encryptBackups", false);
|
||||
backupReminderOffset = Config.get("backupReminderOffset", 0);
|
||||
@@ -60,6 +67,10 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
|
||||
trashCleanupInterval: TrashCleanupInterval = 7;
|
||||
homepage = Config.get("homepage", 0);
|
||||
imageCompression = Config.get(
|
||||
"imageCompression",
|
||||
ImageCompressionOptions.ASK_EVERY_TIME
|
||||
);
|
||||
desktopIntegrationSettings?: DesktopIntegration;
|
||||
autoUpdates = true;
|
||||
isFlatpak = false;
|
||||
@@ -125,6 +136,11 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
Config.set("homepage", homepage);
|
||||
};
|
||||
|
||||
setImageCompression = (imageCompression: ImageCompressionOptions) => {
|
||||
this.set({ imageCompression });
|
||||
Config.set("imageCompression", imageCompression);
|
||||
};
|
||||
|
||||
setDesktopIntegration = async (settings: DesktopIntegration) => {
|
||||
const { desktopIntegrationSettings } = this.get();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1111,6 +1111,10 @@ $headline$: Use starting line of the note as title.`,
|
||||
behaviorDesc: () => t`Change how the app behaves in different situations`,
|
||||
homepage: () => t`Homepage`,
|
||||
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`,
|
||||
dateFormatDesc: () => t`Choose how dates are displayed in the app`,
|
||||
timeFormat: () => t`Time format`,
|
||||
|
||||
Reference in New Issue
Block a user