From 25487f536c131cd12af13a57553754e551cc1b50 Mon Sep 17 00:00:00 2001 From: mahsa shadi Date: Tue, 16 Nov 2021 14:03:02 +0330 Subject: [PATCH 1/2] pagination capability is added --- index.js | 151 ++++++++++++++++++++++++++++++++++++++++++++++------- locales.js | 20 ++++--- 2 files changed, 145 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index e216b27..dbfb32c 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,75 @@ import { listView } from '@osjs/gui'; +/** + * Check if current path is at mountpoint + */ +const checkMountPoint = dir => dir.split(':/').splice(1).join(':/'); + +/** + * Update some state properties when selected directory/file changed + */ +const updateState = (state) => { + state.currentList = []; + state.currentPage = 0; + state.fetchAllPages = false; + state.currentLastItem = ''; +}; + +/** + * Remove duplicate file objects if there is any + */ +const removeDuplicates = (list) => { + const filenameSet = new Set(); + return list.filter((obj) => { + const isPresent = filenameSet.has(obj.filename); + filenameSet.add(obj.filename); + return !isPresent; + }); +}; + +/** + * Detect pagination capability when selected mountpoint changed + */ +const getMountPointCapability = (core, path) => { + const vfs = core.make('osjs/vfs'); + return vfs.capabilities(path); +}; + +/** + * Create files list by concating new page by previous fetched pages + */ +const createPagesList = async (proc, state, vfs, dir) => { + const options = { + showHiddenFiles: proc.settings.showHiddenFiles, + page:{ + size: state.capability.page_size, + number: state.currentPage, + marker: state.currentLastItem, + token: '' + } + }; + + let list = await vfs.readdir(dir, options); + state.currentLastItem = list[list.length - 1].filename; + list = state.currentList.concat(list); + state.currentList = removeDuplicates(list); + if(state.currentList.length === state.totalCount) { + state.fetchAllPages = true; + } + return state.currentList; +}; + +/** + * Create total list of files when pagination is not supported + */ +const createTotalList = (proc, vfs, dir) => { + const options = { + showHiddenFiles: proc.settings.showHiddenFiles + }; + return vfs.readdir(dir, options); +}; + /** * Creates default settings */ @@ -133,16 +202,12 @@ const formatFileMessage = file => `${file.filename} (${file.size} bytes)`; /** * Formats directory status message */ -const formatStatusMessage = (core) => { +const formatStatusMessage = (core, state) => { const {translatable} = core.make('osjs/locale'); const __ = translatable(translations); return (path, files) => { - const directoryCount = files.filter(f => f.isDirectory).length; - const fileCount = files.filter(f => !f.isDirectory).length; - const totalSize = files.reduce((t, f) => t + (f.size || 0), 0); - - return __('LBL_STATUS', directoryCount, fileCount, totalSize); + return __('LBL_STATUS', files.length, state.totalCount, state.totalSize); }; }; @@ -152,13 +217,12 @@ const formatStatusMessage = (core) => { const mountViewRowsFactory = (core) => { const fs = core.make('osjs/fs'); const getMountpoints = () => fs.mountpoints(true); - return () => getMountpoints().map(m => ({ columns: [{ icon: m.icon, label: m.label }], - data: m + data: m, })); }; @@ -320,18 +384,38 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { return; } - try { - const message = __('LBL_LOADING', dir.path); + if(Object.keys(state.capability).length === 0) { + state.capability = await getMountPointCapability(core, dir); + } + + // if calling by scroll + if(dir === undefined) { + dir = state.currentPath; + state.currentPage += 1; + // else if mountpoint/directory is selcted + } else { const options = { showHiddenFiles: proc.settings.showHiddenFiles }; + const stat = await vfs.stat(dir, options); + state.totalCount = stat.totalCount; + checkMountPoint(dir.path) !== '' ? state.totalCount += 1 : null; + state.totalSize = stat.totalSize; + } + try { + const message = __('LBL_LOADING', dir.path); win.setState('loading', true); win.emit('filemanager:status', message); - - const list = await vfs.readdir(dir, options); + let list; + if(state.capability.pagination) { + list = await createPagesList(proc, state, vfs, dir); + }else { + list = await createTotalList(proc, vfs, dir); + } // NOTE: This sets a restore argument in the application session + proc.args.path = dir; state.currentPath = dir; @@ -675,12 +759,12 @@ const createView = (core, proc, win) => { /** * Creates a new FileManager user interface */ -const createApplication = (core, proc) => { +const createApplication = (core, proc, state) => { const createColumns = listViewColumnFactory(core, proc); const createRows = listViewRowFactory(core, proc); const createMounts = mountViewRowsFactory(core); const {draggable} = core.make('osjs/dnd'); - const statusMessage = formatStatusMessage(core); + const statusMessage = formatStatusMessage(core, state); const initialState = { path: '', @@ -761,17 +845,36 @@ const createApplication = (core, proc) => { }, mountview: listView.actions({ - select: ({data}) => win.emit('filemanager:navigate', {path: data.root}) + select: async ({data}) => { + await updateState(state); + state.capability = await getMountPointCapability(core, data.root); + win.emit('filemanager:navigate', {path: data.root}); + } }), fileview: listView.actions({ select: ({data}) => win.emit('filemanager:select', data), - activate: ({data}) => win.emit(`filemanager:${data.isFile ? 'open' : 'navigate'}`, data), + activate: async ({data}) => { + data.isDirectory ? await updateState(state) : null; + win.emit(`filemanager:${data.isFile ? 'open' : 'navigate'}`, data); + }, contextmenu: args => win.emit('filemanager:contextmenu', args), created: ({el, data}) => { if (data.isFile) { draggable(el, {data}); } + }, + scroll: (ev) => { + if (state.capability.pagination) { + if (state.fetchAllPages) { + return; + } + const el = ev.target; + const hitBottom = (el.scrollTop + el.offsetHeight) >= el.scrollHeight; + if(hitBottom) { + win.emit('filemanager:navigate'); + } + } } }) }); @@ -786,14 +889,24 @@ const createApplication = (core, proc) => { /** * Creates a new FileManager window */ -const createWindow = (core, proc) => { +const createWindow = async (core, proc) => { let wired; - const state = {currentFile: undefined, currentPath: undefined}; + const state = { + currentFile: undefined, + currentPath: undefined, + currentList: [], + currentPage:0, + fetchAllPages: false, + currentLastItem:'', + capability:{}, + totalCount:0, + totalSize:0 + }; const {homePath, initialPath} = createInitialPaths(core, proc); const title = core.make('osjs/locale').translatableFlat(proc.metadata.title); const win = proc.createWindow(createWindowOptions(core, proc, title)); - const render = createApplication(core, proc); + const render = createApplication(core, proc, state); const dialog = dialogFactory(core, proc, win); const createMenu = menuFactory(core, proc, win); const vfs = vfsActionFactory(core, proc, win, dialog, state); diff --git a/locales.js b/locales.js index 5a92aa1..f5a5d6d 100644 --- a/locales.js +++ b/locales.js @@ -3,7 +3,7 @@ export const en_EN = { LBL_MINIMALISTIC: 'Minimalistic', LBL_OPEN_WITH: 'Open with...', LBL_SHOW_DATE: 'Show date column', - LBL_STATUS: '{0} directories, {1} files, {2} bytes total', + LBL_STATUS: '{0} of {1} files, {2} bytes total', LBL_DATE: 'Date', // FIXME: Move to client LBL_LOADING: 'Loading {0}', DIALOG_MKDIR_MESSAGE: 'Create new directory', @@ -25,7 +25,8 @@ export const sv_SE = { LBL_MINIMALISTIC: 'Minimalistic', LBL_OPEN_WITH: 'Öppna med...', LBL_SHOW_DATE: 'Visa datumkolumn', - LBL_STATUS: '{0} kataloger, {1} filer, {2} byte totalt', + // LBL_STATUS: '{0} kataloger, {1} filer, {2} byte totalt', + LBL_STATUS: '{0}/{1} filer, {2} byte totalt', LBL_DATE: 'Datum', // FIXME: Move to client LBL_LOADING: 'Laddar {0}', DIALOG_MKDIR_MESSAGE: 'Skapa ny katalog', @@ -46,7 +47,8 @@ export const nb_NO = { LBL_MINIMALISTIC: 'Minimalistisk', LBL_OPEN_WITH: 'Åpne med...', LBL_SHOW_DATE: 'Vis dato kolonne', - LBL_STATUS: '{0} mapper, {1} filer, {2} bytes totalt', + // LBL_STATUS: '{0} mapper, {1} filer, {2} bytes totalt', + LBL_STATUS: '{0}/{1} filer, {2} byte totalt', LBL_DATE: 'Dato', LBL_LOADING: 'Laster {0}', DIALOG_MKDIR_MESSAGE: 'Lag ny mappe', @@ -67,7 +69,8 @@ export const vi_VN = { LBL_MINIMALISTIC: 'Tối giản', LBL_OPEN_WITH: 'Mở bằng...', LBL_SHOW_DATE: 'Hiện cột thời gian', - LBL_STATUS: '{0} thư mục, {1} tập tin, tổng dung lượng {2} bytes', + // LBL_STATUS: '{0} thư mục, {1} tập tin, tổng dung lượng {2} bytes', + LBL_STATUS: '{0}/{1} tập tin, tổng dung lượng {2} bytes', LBL_DATE: 'Thời gian', LBL_LOADING: 'Đang tải {0}', DIALOG_MKDIR_MESSAGE: 'Tạo thư mục mới', @@ -88,7 +91,8 @@ export const pt_BR = { LBL_MINIMALISTIC: 'Minimalista', LBL_OPEN_WITH: 'Abrir com...', LBL_SHOW_DATE: 'Mostrar coluna Data', - LBL_STATUS: '{0} diretórios, {1} arquivos, {2} bytes no total', + // LBL_STATUS: '{0} diretórios, {1} arquivos, {2} bytes no total', + LBL_STATUS: '{0}/{1} arquivos, {2} bytes no total', LBL_DATE: 'Data', // FIXME: Move to client LBL_LOADING: 'Carregando {0}', DIALOG_MKDIR_MESSAGE: 'Criar novo diretório', @@ -109,7 +113,8 @@ export const fr_FR = { LBL_MINIMALISTIC: 'Minimaliste', LBL_OPEN_WITH: 'Ouvrir avec...', LBL_SHOW_DATE: 'Affichier la colonne date', - LBL_STATUS: '{0} dossiers, {1} fichiers, {2} bytes au total', + // LBL_STATUS: '{0} dossiers, {1} fichiers, {2} bytes au total', + LBL_STATUS: '{0}/{1} fichiers, {2} bytes au total', LBL_DATE: 'Date', // FIXME: Move to client LBL_LOADING: 'Chargement en cours {0}', DIALOG_MKDIR_MESSAGE: 'Créer nouveau dossier', @@ -130,7 +135,8 @@ export const tr_TR = { LBL_MINIMALISTIC: 'Minimalist', LBL_OPEN_WITH: 'Şununla aç:', LBL_SHOW_DATE: 'Tarih sütununu göster', - LBL_STATUS: 'toplamda {0} dizin, {1} dosya, {2} byte var', + // LBL_STATUS: 'toplamda {0} dizin, {1} dosya, {2} byte var', + LBL_STATUS: '{0}/{1} dosya, {2} byte var', LBL_DATE: 'Tarih', // FIXME: Move to client LBL_LOADING: 'Yükleniyor {0}', DIALOG_MKDIR_MESSAGE: 'Yeni dizin oluştur', From 9a3cdc29fd12010d568a6d1605264e4ca03dde11 Mon Sep 17 00:00:00 2001 From: mahsa shadi Date: Wed, 9 Feb 2022 15:45:37 +0330 Subject: [PATCH 2/2] pagination capability and refreshing is fixed --- index.js | 190 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 31 deletions(-) diff --git a/index.js b/index.js index dbfb32c..a1f2184 100644 --- a/index.js +++ b/index.js @@ -48,11 +48,68 @@ import { listView } from '@osjs/gui'; +/** + * Get the human file size of n bytes + */ +const humanFileSize = (bytes, si = false) => { + if (isNaN(bytes) || typeof bytes !== 'number') { + bytes = 0; + } + + const thresh = si ? 1000 : 1024; + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + + if (bytes < thresh) { + return bytes + ' B'; + } + + let u = -1; + do { + bytes /= thresh; + ++u; + } while (bytes >= thresh); + + return `${bytes.toFixed(1)} ${units[u]}`; +}; + /** * Check if current path is at mountpoint */ const checkMountPoint = dir => dir.split(':/').splice(1).join(':/'); +/** + * Create object of file or directory + */ +const createFileItem = (item, path) => { + let basePath = path.endsWith('/') ? path : path + '/'; + if(item.filename) { + item.path = basePath + item.filename; + item.isDirectory ? item.path += '/' : null; + return item; + } else if(typeof item === 'string') { + return { + filename: item, + isDirectory:true, + isFile:false, + humanSize: '4 KiB', + size: 4096, + path: basePath + item + }; + } else { + return { + filename: item.name, + isDirectory:false, + isFile:true, + mime: item.type, + humanSize: humanFileSize(item.size), + size:item.size, + path: basePath + item.name + }; + } +}; + /** * Update some state properties when selected directory/file changed */ @@ -64,7 +121,7 @@ const updateState = (state) => { }; /** - * Remove duplicate file objects if there is any + * Remove duplicate file objects in file list, if there is any */ const removeDuplicates = (list) => { const filenameSet = new Set(); @@ -92,13 +149,13 @@ const createPagesList = async (proc, state, vfs, dir) => { page:{ size: state.capability.page_size, number: state.currentPage, - marker: state.currentLastItem, - token: '' + marker: state.currentLastItem } }; let list = await vfs.readdir(dir, options); state.currentLastItem = list[list.length - 1].filename; + state.currentPage += 1; list = state.currentList.concat(list); state.currentList = removeDuplicates(list); if(state.currentList.length === state.totalCount) { @@ -110,11 +167,65 @@ const createPagesList = async (proc, state, vfs, dir) => { /** * Create total list of files when pagination is not supported */ -const createTotalList = (proc, vfs, dir) => { +const createTotalList = async (proc, state, vfs, dir) => { const options = { showHiddenFiles: proc.settings.showHiddenFiles }; - return vfs.readdir(dir, options); + state.currentList = await vfs.readdir(dir, options); + return state.currentList; +}; + +/** + * Add an item to the sorted files list + */ +const addToRenderedList = (state, item) => { + let list = state.currentList; + let len = list.length; + let addItem = createFileItem(item, state.currentPath.path); + if(len === 0) { + list.splice(0, 0, addItem); + }else { + // FIXME: should support other kinds of sorting like desc, ... + for (let i = 0; i < len; i++) { + if (addItem.filename === list[i].filename) { + list.splice(i, 1, addItem); + break; + } + if (addItem.filename < list[i].filename) { + list.splice(i, 0, addItem); + break; + } + if (i === len - 1 && addItem.filename > list[i].filename) { + list.splice(i + 1, 0, addItem); + break; + } + } + } + state.totalSize += addItem.size; + state.totalCount += 1; +}; + +/** + * Remove an item from the sorted files list + */ +const removeFromRenderedList = (state, item) => { + let index = state.currentList.indexOf(item, 0); + state.currentList.splice(index, 1); + state.totalCount -= 1; + state.totalSize -= item.size; +}; + +/** + * Rename an item from the sorted files list + */ +const renameFromRenderedList = (state, item, name) => { + removeFromRenderedList(state, item); + item.path = item.path.endsWith('/') ? item.path.slice(0, -1) : item.path; + let splitPath = item.path.split('/'); + let basePath = splitPath.slice(0, splitPath.length - 1).join('/'); + item.filename = name; + item.path = basePath; + addToRenderedList(state, item); }; /** @@ -222,7 +333,7 @@ const mountViewRowsFactory = (core) => { icon: m.icon, label: m.label }], - data: m, + data: m })); }; @@ -320,36 +431,46 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { const {translatable} = core.make('osjs/locale'); const __ = translatable(translations); - const refresh = (fileOrWatch) => { + const refresh = ({addElement, removeElement, renameElement, renameString}) => { + let selectFile; + if(addElement) { + selectFile = addElement.name || addElement.filename || addElement; + addToRenderedList(state, addElement); + } else if(removeElement) { + removeFromRenderedList(state, removeElement); + } else if(renameElement) { + selectFile = renameString; + renameFromRenderedList(state, renameElement, renameString); + } + + win.emit('filemanager:readdir', {list:state.currentList, path: state.currentPath.path, selectFile}); // FIXME This should be implemented a bit better /* if (fileOrWatch === true && core.config('vfs.watch')) { return; } */ - - win.emit('filemanager:navigate', state.currentPath, undefined, fileOrWatch); }; const action = async (promiseCallback, refreshValue, defaultError) => { try { win.setState('loading', true); - - const result = await promiseCallback(); - refresh(refreshValue); - return result; + promiseCallback().then((result)=>{ + refreshValue.mkdirString ? refresh({addElement:refreshValue.mkdirString}) : + refreshValue.deleteItem ? refresh({removeElement:refreshValue.deleteItem}) : + refreshValue.renameItem ? refresh({renameElement:refreshValue.renameItem, renameString: refreshValue.renameString}) : null; + return result; + }); } catch (error) { dialog('error', error, defaultError || __('MSG_ERROR')); } finally { win.setState('loading', false); } - return []; }; const writeRelative = f => { const d = dialog('progress', f); - return vfs.writefile({ path: pathJoin(state.currentPath.path, f.name) }, f, { @@ -366,7 +487,9 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { const uploadBrowserFiles = (files) => { Promise.all(files.map(writeRelative)) - .then(() => refresh(files[0].name)) // FIXME: Select all ? + .then(files.forEach(el => { + refresh({addElement:el}); + })) .catch(error => dialog('error', error, __('MSG_UPLOAD_ERROR'))); }; @@ -391,8 +514,7 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { // if calling by scroll if(dir === undefined) { dir = state.currentPath; - state.currentPage += 1; - // else if mountpoint/directory is selcted + // else if mountpoint/directory is selected } else { const options = { showHiddenFiles: proc.settings.showHiddenFiles @@ -411,7 +533,7 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { if(state.capability.pagination) { list = await createPagesList(proc, state, vfs, dir); }else { - list = await createTotalList(proc, vfs, dir); + list = await createTotalList(proc, state, vfs, dir); } // NOTE: This sets a restore argument in the application session @@ -438,7 +560,7 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { const upload = () => triggerBrowserUpload(files => { writeRelative(files[0]) - .then(() => refresh(files[0].name)) + .then(() => refresh({addElement: files[0]})) .catch(error => dialog('error', error, __('MSG_UPLOAD_ERROR'))); }); @@ -451,7 +573,7 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { return fn .then(() => { - refresh(true); + refresh({addElement: item}); if (typeof callback === 'function') { callback(); @@ -514,7 +636,7 @@ const dialogFactory = (core, proc, win) => { value: __('DIALOG_MKDIR_PLACEHOLDER') }, usingPositiveButton(value => { const newPath = pathJoin(currentPath.path, value); - action(() => vfs.mkdir({path: newPath}, {pid: proc.pid}), value, __('MSG_MKDIR_ERROR')); + action(() => vfs.mkdir({path: newPath}, {pid: proc.pid}), {mkdirString:value}, __('MSG_MKDIR_ERROR')); })); const renameDialog = (action, file) => dialog('prompt', { @@ -523,14 +645,13 @@ const dialogFactory = (core, proc, win) => { }, usingPositiveButton(value => { const idx = file.path.lastIndexOf(file.filename); const newPath = file.path.substr(0, idx) + value; - - action(() => vfs.rename(file, {path: newPath}), value, __('MSG_RENAME_ERROR')); + action(() => vfs.rename(file, {path: newPath}), {renameItem:file, renameString:value}, __('MSG_RENAME_ERROR')); })); const deleteDialog = (action, file) => dialog('confirm', { message: __('DIALOG_DELETE_MESSAGE', file.filename), }, usingPositiveButton(() => { - action(() => vfs.unlink(file, {pid: proc.pid}), true, __('MSG_DELETE_ERROR')); + action(() => vfs.unlink(file, {pid: proc.pid}), {deleteItem:file}, __('MSG_DELETE_ERROR')); })); const progressDialog = (file) => dialog('progress', { @@ -564,7 +685,7 @@ const dialogFactory = (core, proc, win) => { /** * Creates Menus */ -const menuFactory = (core, proc, win) => { +const menuFactory = (core, proc, win, state) => { const fs = core.make('osjs/fs'); const clipboard = core.make('osjs/clipboard'); const contextmenu = core.make('osjs/contextmenu'); @@ -602,7 +723,10 @@ const menuFactory = (core, proc, win) => { if (item && isSpecialFile(item.filename)) { return [{ label: _('LBL_GO'), - onclick: () => emitter('filemanager:navigate') + onclick: async () => { + await updateState(state); + emitter('filemanager:navigate'); + } }]; } @@ -612,7 +736,10 @@ const menuFactory = (core, proc, win) => { const openMenu = isDirectory ? [{ label: _('LBL_GO'), disabled: !item, - onclick: () => emitter('filemanager:navigate') + onclick: async () => { + await updateState(state); + emitter('filemanager:navigate'); + } }] : [{ label: _('LBL_OPEN'), disabled: !item, @@ -822,7 +949,6 @@ const createApplication = (core, proc, state) => { setMinimalistic: minimalistic => ({minimalistic}), setList: ({list, path, selectFile}) => ({fileview, mountview}) => { let selectedIndex; - if (selectFile) { const foundIndex = list.findIndex(file => file.filename === selectFile); if (foundIndex !== -1) { @@ -908,7 +1034,7 @@ const createWindow = async (core, proc) => { const win = proc.createWindow(createWindowOptions(core, proc, title)); const render = createApplication(core, proc, state); const dialog = dialogFactory(core, proc, win); - const createMenu = menuFactory(core, proc, win); + const createMenu = menuFactory(core, proc, win, state); const vfs = vfsActionFactory(core, proc, win, dialog, state); const clipboard = clipboardActionFactory(core, state, vfs); @@ -1016,7 +1142,7 @@ const createProcess = (core, args, options, metadata) => { return; } - const currentPath = String(proc.args.path.path).replace(/\/$/, ''); + const currentPath = proc.args.path ? String(proc.args.path.path).replace(/\/$/, '') : null; const watchPath = String(args.path).replace(/\/$/, ''); if (currentPath === watchPath) { win.emit('filemanager:refresh'); @@ -1030,3 +1156,5 @@ const createProcess = (core, args, options, metadata) => { }; osjs.register(applicationName, createProcess); + +