This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { IoFileTray } from 'react-icons/io5';
|
||||
import { MdRssFeed } from 'react-icons/md';
|
||||
@@ -9,20 +8,26 @@ import Menu from '@/components/Menu';
|
||||
|
||||
interface ImportMenuProps {
|
||||
setIsDropdownOpen?: (open: boolean) => void;
|
||||
onImportBooks: () => void;
|
||||
onImportBooksFromFiles: () => void;
|
||||
onImportBooksFromDirectory?: () => void;
|
||||
onOpenCatalogManager: () => void;
|
||||
}
|
||||
|
||||
const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
setIsDropdownOpen,
|
||||
onImportBooks,
|
||||
onImportBooksFromFiles,
|
||||
onImportBooksFromDirectory,
|
||||
onOpenCatalogManager,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
|
||||
const handleImportBooks = () => {
|
||||
onImportBooks();
|
||||
const handleImportFromFiles = () => {
|
||||
onImportBooksFromFiles();
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const handleImportFromDirectory = () => {
|
||||
onImportBooksFromDirectory?.();
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
@@ -33,17 +38,21 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={clsx(
|
||||
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
|
||||
)}
|
||||
className={clsx('dropdown-content bg-base-100 rounded-box z-[1] mt-3 p-2 shadow')}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
<MenuItem
|
||||
label={_('From Local File')}
|
||||
Icon={<IoFileTray className='h-5 w-5' />}
|
||||
onClick={handleImportBooks}
|
||||
onClick={handleImportFromFiles}
|
||||
/>
|
||||
{onImportBooksFromDirectory && (
|
||||
<MenuItem
|
||||
label={_('From Directory')}
|
||||
Icon={<IoFileTray className='h-5 w-5' />}
|
||||
onClick={handleImportFromDirectory}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Online Library')}
|
||||
Icon={<MdRssFeed className='h-5 w-5' />}
|
||||
|
||||
@@ -26,7 +26,8 @@ import ViewMenu from './ViewMenu';
|
||||
interface LibraryHeaderProps {
|
||||
isSelectMode: boolean;
|
||||
isSelectAll: boolean;
|
||||
onImportBooks: () => void;
|
||||
onImportBooksFromFiles: () => void;
|
||||
onImportBooksFromDirectory?: () => void;
|
||||
onOpenCatalogManager: () => void;
|
||||
onToggleSelectMode: () => void;
|
||||
onSelectAll: () => void;
|
||||
@@ -36,7 +37,8 @@ interface LibraryHeaderProps {
|
||||
const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isSelectMode,
|
||||
isSelectAll,
|
||||
onImportBooks,
|
||||
onImportBooksFromFiles,
|
||||
onImportBooksFromDirectory,
|
||||
onOpenCatalogManager,
|
||||
onToggleSelectMode,
|
||||
onSelectAll,
|
||||
@@ -152,14 +154,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<Dropdown
|
||||
label={_('Import Books')}
|
||||
className={clsx(
|
||||
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
|
||||
isMobile ? 'dropdown-end' : 'dropdown-center',
|
||||
'exclude-title-bar-mousedown dropdown-bottom dropdown-center flex h-6 cursor-pointer justify-center',
|
||||
)}
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center !bg-transparent'
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex touch-target items-center justify-center !bg-transparent'
|
||||
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
|
||||
>
|
||||
<ImportMenu
|
||||
onImportBooks={onImportBooks}
|
||||
onImportBooksFromFiles={onImportBooksFromFiles}
|
||||
onImportBooksFromDirectory={onImportBooksFromDirectory}
|
||||
onOpenCatalogManager={onOpenCatalogManager}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
RiLoader2Line,
|
||||
} from 'react-icons/ri';
|
||||
import { documentDir, join } from '@tauri-apps/api/path';
|
||||
import { invoke, PermissionState } from '@tauri-apps/api/core';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -20,6 +19,7 @@ import { formatBytes } from '@/utils/book';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getExternalSDCardPath } from '@/utils/bridge';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { requestStoragePermission } from '@/utils/permission';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
@@ -42,10 +42,6 @@ interface MigrationProgress {
|
||||
currentFile?: string;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
manageStorage: PermissionState;
|
||||
}
|
||||
|
||||
export const MigrateDataWindow = () => {
|
||||
const _ = useTranslation();
|
||||
const { appService, envConfig } = useEnv();
|
||||
@@ -158,13 +154,7 @@ export const MigrateDataWindow = () => {
|
||||
setErrorMessage('');
|
||||
|
||||
if (!dir.includes('Android/data')) {
|
||||
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
|
||||
if (permission.manageStorage !== 'granted') {
|
||||
permission = await invoke<Permissions>(
|
||||
'plugin:native-bridge|request_manage_storage_permission',
|
||||
);
|
||||
}
|
||||
if (permission.manageStorage !== 'granted') return;
|
||||
if (!(await requestStoragePermission())) return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/
|
||||
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
|
||||
import { requestStoragePermission } from '@/utils/permission';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import { selectDirectory } from '@/utils/bridge';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
@@ -175,13 +176,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
};
|
||||
|
||||
const handleSetSavedBookCoverForLockScreen = async () => {
|
||||
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
|
||||
if (permission.manageStorage !== 'granted') {
|
||||
permission = await invoke<Permissions>(
|
||||
'plugin:native-bridge|request_manage_storage_permission',
|
||||
);
|
||||
}
|
||||
if (permission.manageStorage !== 'granted' && appService?.distChannel === 'readest') return;
|
||||
if (!(await requestStoragePermission()) && appService?.distChannel === 'readest') return;
|
||||
|
||||
const newValue = settings.savedBookCoverForLockScreen ? '' : 'default';
|
||||
if (newValue) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { formatAuthors, formatTitle, getPrimaryLanguage, listFormater } from '@/
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { ProgressPayload } from '@/utils/transfer';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { getFilename } from '@/utils/path';
|
||||
import { getDirPath, getFilename, joinPaths } from '@/utils/path';
|
||||
import { parseOpenWithFiles } from '@/helpers/openWith';
|
||||
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
|
||||
@@ -38,7 +38,9 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
||||
import { SelectedFile, useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { lockScreenOrientation, selectDirectory } from '@/utils/bridge';
|
||||
import { requestStoragePermission } from '@/utils/permission';
|
||||
import { SUPPORTED_BOOK_EXTS } from '@/services/constants';
|
||||
import {
|
||||
tauriHandleClose,
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
@@ -143,7 +145,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
setSettingsDialogOpen(true);
|
||||
},
|
||||
onOpenBooks: () => {
|
||||
handleImportBooks();
|
||||
handleImportBooksFromFiles();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -366,6 +368,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
setLoading(true);
|
||||
const { library } = useLibraryStore.getState();
|
||||
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
|
||||
const successfulImports: string[] = [];
|
||||
const errorMap: [string, string][] = [
|
||||
['No chapters detected', _('No chapters detected')],
|
||||
['Failed to parse EPUB', _('Failed to parse the EPUB file')],
|
||||
@@ -380,15 +383,25 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
if (!file) return;
|
||||
try {
|
||||
const book = await appService?.importBook(file, library);
|
||||
const { path, basePath } = selectedFile;
|
||||
if (book && groupId) {
|
||||
book.groupId = groupId;
|
||||
book.groupName = getGroupName(groupId);
|
||||
await updateBook(envConfig, book);
|
||||
} else if (book && path && basePath) {
|
||||
const rootPath = getDirPath(basePath);
|
||||
const groupName = getDirPath(path).replace(rootPath, '').replace(/^\//, '');
|
||||
book.groupName = groupName;
|
||||
book.groupId = getGroupId(groupName);
|
||||
await updateBook(envConfig, book);
|
||||
}
|
||||
if (user && book && !book.uploadedAt && settings.autoUpload) {
|
||||
console.log('Uploading book:', book.title);
|
||||
handleBookUpload(book, false);
|
||||
}
|
||||
if (book) {
|
||||
successfulImports.push(book.title);
|
||||
}
|
||||
} catch (error) {
|
||||
const filename = typeof file === 'string' ? file : file.name;
|
||||
const baseFilename = getFilename(filename);
|
||||
@@ -417,8 +430,17 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
_('Failed to import book(s): {{filenames}}', {
|
||||
filenames: listFormater(false).format(filenames),
|
||||
}) + (errorMessage ? `\n${errorMessage}` : ''),
|
||||
timeout: 5000,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (successfulImports.length > 0) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Successfully imported {{count}} book(s)', {
|
||||
count: successfulImports.length,
|
||||
}),
|
||||
timeout: 2000,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
setLibrary([...library]);
|
||||
@@ -582,9 +604,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
await updateBook(envConfig, book);
|
||||
};
|
||||
|
||||
const handleImportBooks = async () => {
|
||||
const handleImportBooksFromFiles = async () => {
|
||||
setIsSelectMode(false);
|
||||
console.log('Importing books...');
|
||||
console.log('Importing books from files...');
|
||||
selectFiles({ type: 'books', multiple: true }).then((result) => {
|
||||
if (result.files.length === 0 || result.error) return;
|
||||
const groupId = searchParams?.get('group') || '';
|
||||
@@ -592,6 +614,40 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportBooksFromDirectory = async () => {
|
||||
if (!appService || !isTauriAppPlatform()) return;
|
||||
|
||||
setIsSelectMode(false);
|
||||
console.log('Importing books from directory...');
|
||||
let importDirectory: string | undefined = '';
|
||||
if (appService.isAndroidApp) {
|
||||
if (!(await requestStoragePermission())) return;
|
||||
const response = await selectDirectory();
|
||||
importDirectory = response.path;
|
||||
} else {
|
||||
const selectedDir = await appService.selectDirectory?.('read');
|
||||
importDirectory = selectedDir;
|
||||
}
|
||||
if (!importDirectory) {
|
||||
console.log('No directory selected');
|
||||
return;
|
||||
}
|
||||
const files = await appService.readDirectory(importDirectory, 'None');
|
||||
const supportedFiles = files.filter((file) => {
|
||||
const ext = file.path.split('.').pop()?.toLowerCase() || '';
|
||||
return SUPPORTED_BOOK_EXTS.includes(ext);
|
||||
});
|
||||
const toImportFiles = await Promise.all(
|
||||
supportedFiles.map(async (file) => {
|
||||
return {
|
||||
path: await joinPaths(importDirectory, file.path),
|
||||
basePath: importDirectory,
|
||||
};
|
||||
}),
|
||||
);
|
||||
importBooks(toImportFiles, undefined);
|
||||
};
|
||||
|
||||
const handleSetSelectMode = (selectMode: boolean) => {
|
||||
if (selectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
@@ -656,7 +712,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
onImportBooks={handleImportBooks}
|
||||
onImportBooksFromFiles={handleImportBooksFromFiles}
|
||||
onImportBooksFromDirectory={
|
||||
appService?.canReadExternalDir ? handleImportBooksFromDirectory : undefined
|
||||
}
|
||||
onOpenCatalogManager={() => setShowCatalogManager(true)}
|
||||
onToggleSelectMode={() => handleSetSelectMode(!isSelectMode)}
|
||||
onSelectAll={handleSelectAll}
|
||||
@@ -742,7 +801,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
isSelectNone={isSelectNone}
|
||||
handleImportBooks={handleImportBooks}
|
||||
handleImportBooks={handleImportBooksFromFiles}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete('both')}
|
||||
@@ -764,7 +823,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
'Welcome to your library. You can import your books here and read them anytime.',
|
||||
)}
|
||||
</p>
|
||||
<button className='btn btn-primary rounded-xl' onClick={handleImportBooks}>
|
||||
<button className='btn btn-primary rounded-xl' onClick={handleImportBooksFromFiles}>
|
||||
{_('Import Books')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface SelectedFile {
|
||||
|
||||
// For Tauri file
|
||||
path?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export interface FileSelectionResult {
|
||||
|
||||
@@ -98,6 +98,7 @@ export abstract class BaseAppService implements AppService {
|
||||
hasScreenBrightness = false;
|
||||
hasIAP = false;
|
||||
canCustomizeRootDir = false;
|
||||
canReadExternalDir = false;
|
||||
distChannel = 'readest' as DistChannel;
|
||||
|
||||
protected CURRENT_MIGRATION_VERSION = 20251124;
|
||||
@@ -354,7 +355,6 @@ export abstract class BaseAppService implements AppService {
|
||||
loadedBook.metadata.title = getBaseFilename(filename);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(`Failed to open the book: ${(error as Error).message || error}`);
|
||||
}
|
||||
|
||||
@@ -405,7 +405,14 @@ export abstract class BaseAppService implements AppService {
|
||||
} else if (typeof file === 'string' && isContentURI(file)) {
|
||||
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
|
||||
} else if (typeof file === 'string' && !isValidURL(file)) {
|
||||
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
|
||||
try {
|
||||
// try to copy the file directly first in case of large files to avoid memory issues
|
||||
// on desktop when reading recursively from selected directory the direct copy will fail
|
||||
// due to permission issues, then fallback to read and write files
|
||||
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
|
||||
} catch {
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
|
||||
}
|
||||
} else {
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
|
||||
}
|
||||
@@ -441,6 +448,7 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
return existingBook || book;
|
||||
} catch (error) {
|
||||
console.error('Error importing book:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
FileItem,
|
||||
DistChannel,
|
||||
} from '@/types/system';
|
||||
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getOSPlatform, isContentURI, isFileURI, isValidURL } from '@/utils/misc';
|
||||
import { getDirPath, getFilename } from '@/utils/path';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
@@ -212,17 +212,20 @@ export const nativeFileSystem: FileSystem = {
|
||||
}
|
||||
return await new NativeFile(dst, fname, baseDir ? baseDir : null).open();
|
||||
}
|
||||
} else if (isFileURI(path)) {
|
||||
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
|
||||
} else {
|
||||
const prefix = await this.getPrefix(base);
|
||||
const absolutePath = path.startsWith('/') ? path : prefix ? await join(prefix, path) : null;
|
||||
if (absolutePath && OS_TYPE !== 'android') {
|
||||
if (OS_TYPE === 'android') {
|
||||
// NOTE: RemoteFile is not usable on Android due to a known issue of range request in Android WebView.
|
||||
// see https://issues.chromium.org/issues/40739128
|
||||
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
|
||||
} else {
|
||||
// NOTE: RemoteFile currently performs about 2× faster than NativeFile
|
||||
// due to an unresolved performance issue in Tauri (see tauri-apps/tauri#9190).
|
||||
// Once the bug is resolved, we should switch back to using NativeFile.
|
||||
// RemoteFile is not usable on Android due to unknown issues of range fetch with Android WebView.
|
||||
const prefix = await this.getPrefix(base);
|
||||
const absolutePath = prefix ? await join(prefix, path) : path;
|
||||
return await new RemoteFile(this.getURL(absolutePath), fname).open();
|
||||
} else {
|
||||
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -319,10 +322,13 @@ export const nativeFileSystem: FileSystem = {
|
||||
} else {
|
||||
const filePath = await join(parent, entry.name);
|
||||
const relativePath = relative ? await join(relative, entry.name) : entry.name;
|
||||
const fileInfo = await stat(filePath, baseDir ? { baseDir } : undefined);
|
||||
const opts = baseDir ? { baseDir } : undefined;
|
||||
const fileSize = await stat(filePath, opts)
|
||||
.then((info) => info.size)
|
||||
.catch(() => 0);
|
||||
fileList.push({
|
||||
path: relativePath,
|
||||
size: fileInfo.size,
|
||||
size: fileSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -374,6 +380,7 @@ export class NativeAppService extends BaseAppService {
|
||||
// CustomizeRootDir has a blocker on macOS App Store builds due to Security Scoped Resource restrictions.
|
||||
// See: https://github.com/tauri-apps/tauri/issues/3716
|
||||
override canCustomizeRootDir = DIST_CHANNEL !== 'appstore';
|
||||
override canReadExternalDir = DIST_CHANNEL !== 'appstore' && DIST_CHANNEL !== 'playstore';
|
||||
override distChannel = DIST_CHANNEL;
|
||||
|
||||
private execDir?: string = undefined;
|
||||
|
||||
@@ -371,6 +371,16 @@ foliate-fxl {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.touch-target {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.touch-target::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12px;
|
||||
}
|
||||
|
||||
.no-context-menu {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface AppService {
|
||||
isPortableApp: boolean;
|
||||
isDesktopApp: boolean;
|
||||
canCustomizeRootDir: boolean;
|
||||
canReadExternalDir: boolean;
|
||||
distChannel: DistChannel;
|
||||
|
||||
init(): Promise<void>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { isContentURI, isFileURI, isValidURL } from './misc';
|
||||
|
||||
export const getFilename = (fileOrUri: string) => {
|
||||
@@ -28,3 +29,7 @@ export const getDirPath = (filePath: string) => {
|
||||
parts.pop();
|
||||
return parts.join('/');
|
||||
};
|
||||
|
||||
export const joinPaths = async (...paths: string[]) => {
|
||||
return await join(...paths);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { invoke, PermissionState } from '@tauri-apps/api/core';
|
||||
|
||||
interface Permissions {
|
||||
manageStorage: PermissionState;
|
||||
}
|
||||
|
||||
export const requestStoragePermission = async (): Promise<boolean> => {
|
||||
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
|
||||
if (permission.manageStorage !== 'granted') {
|
||||
permission = await invoke<Permissions>(
|
||||
'plugin:native-bridge|request_manage_storage_permission',
|
||||
);
|
||||
}
|
||||
return permission.manageStorage === 'granted';
|
||||
};
|
||||
Reference in New Issue
Block a user