Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[F2V] favorites view #37727

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/files/js/favoritesfilelist.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ window.addEventListener('DOMContentLoaded', function() {
};
FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Files.FavoritesFileList.prototype */ {
id: 'favorites',
appName: t('files','Favorites'),
id: 'oldfavorites',
appName: t('files','Favorites (old)'),

_clientSideSort: true,
_allowSelection: false,
Expand Down
10 changes: 5 additions & 5 deletions apps/files/js/favoritesplugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@

attach: function() {
var self = this;
$('#app-content-favorites').on('show.plugin-favorites', function(e) {
$('#app-content-oldfavorites').on('show.plugin-oldfavorites', function(e) {
self.showFileList($(e.target));
});
$('#app-content-favorites').on('hide.plugin-favorites', function() {
$('#app-content-oldfavorites').on('hide.plugin-oldfavorites', function() {
self.hideFileList();
});
},

detach: function() {
if (this.favoritesFileList) {
this.favoritesFileList.destroy();
OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated);
$('#app-content-favorites').off('.plugin-favorites');
OCA.Files.fileActions.off('setDefault.plugin-oldfavorites', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.plugin-oldfavorites', this._onActionsUpdated);
$('#app-content-oldfavorites').off('.plugin-oldfavorites');
this.favoritesFileList = null;
}
},
Expand Down
2 changes: 1 addition & 1 deletion apps/files/js/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
/**
* Key for the quick-acces-list
*/
$quickAccessListKey: 'sublist-favorites',
$quickAccessListKey: 'sublist-oldfavorites',
/**
* Initializes the navigation from the given container
*
Expand Down
12 changes: 12 additions & 0 deletions apps/files/js/tagsplugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,25 @@
tags = tags.split('|');
tags = _.without(tags, '');
var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0;

// Fake Node object for vue compatibility
const node = {
type: 'folder',
path: (dir + '/' + fileName).replace(/\/\/+/g, '/'),
root: '/files/' + OC.getCurrentUser().uid
}

if (isFavorite) {
// remove tag from list
tags = _.without(tags, OC.TAG_FAVORITE);
removeFavoriteFromList(dir + '/' + fileName);
// vue compatibility
window._nc_event_bus.emit('files:favorites:removed', node)
} else {
tags.push(OC.TAG_FAVORITE);
addFavoriteToList(dir + '/' + fileName);
// vue compatibility
window._nc_event_bus.emit('files:favorites:added', node)
}

// pre-toggle the star
Expand Down
4 changes: 2 additions & 2 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@ private function registerNavigation(IL10N $l10n): void {
});
\OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
return [
'id' => 'favorites',
'id' => 'oldfavorites',
'appname' => 'files',
'script' => 'simplelist.php',
'order' => 5,
'name' => $l10n->t('Favorites'),
'name' => $l10n->t('Favorites (old)'),
];
});
}
Expand Down
5 changes: 3 additions & 2 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
$navItems = \OCA\Files\App::getNavigationManager()->getAll();

// add the favorites entry in menu
$navItems['favorites']['sublist'] = $favoritesSublistArray;
$navItems['favorites']['classes'] = $collapseClasses;
$navItems['oldfavorites']['sublist'] = $favoritesSublistArray;
$navItems['oldfavorites']['classes'] = $collapseClasses;

// parse every menu and add the expanded user value
foreach ($navItems as $key => $item) {
Expand All @@ -253,6 +253,7 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
$this->initialState->provideInitialState('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
$this->initialState->provideInitialState('favoriteFolders', $favElements['folders']);

Check notice

Code scanning / Psalm

PossiblyUndefinedVariable

Possibly undefined variable $favElements defined in try block

// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
Expand Down
8 changes: 7 additions & 1 deletion apps/files/lib/Service/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class UserConfig {
'default' => false,
'allowed' => [true, false],
],
[
// Whether to sort favorites first in the list or not
'key' => 'sort_favorites_first',
'default' => true,
'allowed' => [true, false],
],
];

protected IConfig $config;
Expand Down Expand Up @@ -133,7 +139,7 @@ public function getConfigs(): array {
$userConfigs = array_map(function(string $key) use ($userId) {
$value = $this->config->getUserValue($userId, Application::APP_ID, $key, $this->getDefaultConfigValue($key));
// If the default is expected to be a boolean, we need to cast the value
if (is_bool($this->getDefaultConfigValue($key))) {
if (is_bool($this->getDefaultConfigValue($key)) && is_string($value)) {
return $value === '1';
}
return $value;
Expand Down
2 changes: 1 addition & 1 deletion apps/files/src/actions/deleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import TrashCan from '@mdi/svg/svg/trash-can.svg?raw'

import { registerFileAction, FileAction } from '../services/FileAction.ts'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
import type { Navigation } from '../services/Navigation.ts'

Expand Down
80 changes: 80 additions & 0 deletions apps/files/src/actions/downloadAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, FileType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDown from '@mdi/svg/svg/arrow-down.svg?raw'

import { registerFileAction, FileAction } from '../services/FileAction'
import { generateUrl } from '@nextcloud/router'
import type { Navigation } from '../services/Navigation.ts'

const triggerDownload = function(url: string) {
const hiddenElement = document.createElement('a')
hiddenElement.download = ''
hiddenElement.href = url
hiddenElement.click()
}

const downloadNodes = function(dir = '/', nodes: Node[]) {
const secret = Math.random().toString(36).substring(2)
const url = generateUrl('/apps/files/ajax/download.php?dir={dir}&files={files}&downloadStartSecret={secret}', {
dir,
secret,
files: JSON.stringify(nodes.map(node => node.basename)),
})
triggerDownload(url)
}

registerFileAction(new FileAction({
id: 'download',
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDown,

enabled(nodes: Node[]) {
return nodes.length > 0 && nodes
.map(node => node.permissions)
.every(permission => (permission & Permission.READ) !== 0)
},

async exec(node: Node, view: Navigation, dir: string) {
if (node.type === FileType.Folder) {
downloadNodes(dir, [node])
return null
}

triggerDownload(node.source)
return null
},

async execBatch(nodes: Node[], view: Navigation, dir: string) {
if (nodes.length === 1) {
this.exec(nodes[0], view, dir)
return [null]
}

downloadNodes(dir, nodes)
return new Array(nodes.length).fill(null)
},

order: 30,
}))
78 changes: 78 additions & 0 deletions apps/files/src/actions/editLocallyAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { encodePath } from '@nextcloud/paths'
import { Permission, type Node } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import DevicesSvg from '@mdi/svg/svg/devices.svg?raw'

import { generateOcsUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { registerFileAction, FileAction } from '../services/FileAction'
import { showError } from '@nextcloud/dialogs'

const openLocalClient = function(path: string) {
const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'

axios.post(link, { path })
.then(function(result) {
const scheme = 'nc://'
const command = 'open'
const uid = getCurrentUser()?.uid
let url = scheme + command + '/' + uid + '@' + window.location.host + encodePath(path)
url += '?token=' + result.data.ocs.data.token

window.location.href = url
})
.catch(function() {
showError(t('files', 'Failed to redirect to client'))
})
}

if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
registerFileAction(new FileAction({
id: 'edit-locally',
displayName: () => t('files', 'Edit locally'),
iconSvgInline: () => DevicesSvg,

// Only works on single files
enabled(nodes: Node[]) {
// Only works on single node
if (nodes.length !== 1) {
return false
}

return (nodes[0].permissions & Permission.UPDATE) !== 0
},

async exec(node: Node) {
if (!node.path) {
return false
}
openLocalClient(node.path)
return null
},

default: true,
order: 25,
}))
}
95 changes: 95 additions & 0 deletions apps/files/src/actions/favoriteAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { emit } from '@nextcloud/event-bus'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import Star from '@mdi/svg/svg/star.svg?raw'
import type { Node } from '@nextcloud/files'

import { generateUrl } from '@nextcloud/router'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
import type { Navigation } from '../services/Navigation'

/**
* If any of the nodes is not favorited
* we display the favorite action.
*/
const shouldFavorite = (nodes: Node[]): boolean => {
return nodes.some(node => node.attributes.favorite === 0)
}

registerFileAction(new FileAction({
id: 'favorite',
displayName(nodes: Node[]) {
return shouldFavorite(nodes)
? t('files', 'Add to favorites')
: t('files', 'Remove from favorites')
},
iconSvgInline: () => Star,

enabled(nodes: Node[]) {
// We can only favorite nodes within files
return !nodes.some(node => !node.root?.startsWith?.('/files'))
},

async exec(node: Node, view: Navigation) {
const willFavorite = shouldFavorite([node])
try {
// TODO: migrate to webdav tags plugin
const url = generateUrl('/apps/files/api/v1/files/') + node.path
await axios.post(url, {
tags: willFavorite
? [OC.TAG_FAVORITE]
: [],
})

// Let's delete even if we are in the favourites view
// AND if it is removed from the user favorites
// AND it's in the root of the favorites view
if (view.id === 'favorites' && !willFavorite && node.dirname === '/') {
emit('files:node:deleted', node)
}

// Update the node webdav attribute
node.attributes.favorite = willFavorite ? 1 : 0

// Dispatch event to whoever is interested
if (willFavorite) {
emit('files:favorites:added', node)
} else {
emit('files:favorites:removed', node)
}

return true
} catch (error) {
const action = willFavorite ? 'adding a file to favourites' : 'removing a file from favourites'
logger.error('Error while ' + action, { error, source: node.source, node })
return false
}
},
async execBatch(nodes: Node[], view: Navigation) {
return Promise.all(nodes.map(node => this.exec(node, view)))
},

order: -50,
}))
Loading