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;
|
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");
|
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,
|
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/"));
|
||||||
images =
|
const imageCompressionConfig = Config.get<ImageCompressionOptions>(
|
||||||
images.length > 0
|
"imageCompression",
|
||||||
? (await ImagePickerDialog.show({
|
ImageCompressionOptions.ASK_EVERY_TIME
|
||||||
images
|
);
|
||||||
})) || []
|
|
||||||
: [];
|
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 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]) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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`,
|
||||||
|
|||||||
Reference in New Issue
Block a user