mirror of
https://github.com/nextcloud/richdocuments.git
synced 2025-12-18 05:20:43 +01:00
chore: refactor iframes to load collabora directly
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
parent
193640dbc6
commit
ba45233bff
19 changed files with 662 additions and 477 deletions
|
|
@ -32,6 +32,7 @@ return [
|
||||||
['name' => 'document#remote', 'url' => 'remote', 'verb' => 'GET'],
|
['name' => 'document#remote', 'url' => 'remote', 'verb' => 'GET'],
|
||||||
['name' => 'document#createFromTemplate', 'url' => 'indexTemplate', 'verb' => 'GET'],
|
['name' => 'document#createFromTemplate', 'url' => 'indexTemplate', 'verb' => 'GET'],
|
||||||
['name' => 'document#publicPage', 'url' => '/public', 'verb' => 'GET'],
|
['name' => 'document#publicPage', 'url' => '/public', 'verb' => 'GET'],
|
||||||
|
['name' => 'document#token', 'url' => '/token', 'verb' => 'POST'],
|
||||||
|
|
||||||
['name' => 'document#editOnline', 'url' => 'editonline', 'verb' => 'GET'],
|
['name' => 'document#editOnline', 'url' => 'editonline', 'verb' => 'GET'],
|
||||||
['name' => 'document#editOnlineTarget', 'url' => 'editonline/{fileId}/{target}', 'verb' => 'GET'],
|
['name' => 'document#editOnlineTarget', 'url' => 'editonline/{fileId}/{target}', 'verb' => 'GET'],
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ describe('Public sharing of office documents', function() {
|
||||||
cy.spy(win, 'postMessage').as('postMessage')
|
cy.spy(win, 'postMessage').as('postMessage')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cy.waitForCollabora()
|
cy.waitForCollabora(true)
|
||||||
cy.get('@loleafletframe').within(() => {
|
cy.get('@loleafletframe').within(() => {
|
||||||
cy.get('#closebutton').click()
|
cy.get('#closebutton').click()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -46,57 +46,20 @@ use Psr\Log\LoggerInterface;
|
||||||
class DirectViewController extends Controller {
|
class DirectViewController extends Controller {
|
||||||
use DocumentTrait;
|
use DocumentTrait;
|
||||||
|
|
||||||
/** @var IRootFolder */
|
|
||||||
private $rootFolder;
|
|
||||||
|
|
||||||
/** @var TokenManager */
|
|
||||||
private $tokenManager;
|
|
||||||
|
|
||||||
/** @var DirectMapper */
|
|
||||||
private $directMapper;
|
|
||||||
|
|
||||||
/** @var IConfig */
|
|
||||||
private $config;
|
|
||||||
|
|
||||||
/** @var AppConfig */
|
|
||||||
private $appConfig;
|
|
||||||
|
|
||||||
/** @var TemplateManager */
|
|
||||||
private $templateManager;
|
|
||||||
|
|
||||||
/** @var FederationService */
|
|
||||||
private $federationService;
|
|
||||||
|
|
||||||
/** @var LoggerInterface */
|
|
||||||
private $logger;
|
|
||||||
|
|
||||||
/** @var InitialStateService */
|
|
||||||
private $initialState;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$appName,
|
string $appName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
IRootFolder $rootFolder,
|
private IRootFolder $rootFolder,
|
||||||
TokenManager $tokenManager,
|
private TokenManager $tokenManager,
|
||||||
DirectMapper $directMapper,
|
private DirectMapper $directMapper,
|
||||||
InitialStateService $initialState,
|
private InitialStateService $initialState,
|
||||||
IConfig $config,
|
private IConfig $config,
|
||||||
AppConfig $appConfig,
|
private AppConfig $appConfig,
|
||||||
TemplateManager $templateManager,
|
private TemplateManager $templateManager,
|
||||||
FederationService $federationService,
|
private FederationService $federationService,
|
||||||
LoggerInterface $logger
|
private LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
|
|
||||||
$this->rootFolder = $rootFolder;
|
|
||||||
$this->tokenManager = $tokenManager;
|
|
||||||
$this->directMapper = $directMapper;
|
|
||||||
$this->initialState = $initialState;
|
|
||||||
$this->config = $config;
|
|
||||||
$this->appConfig = $appConfig;
|
|
||||||
$this->templateManager = $templateManager;
|
|
||||||
$this->federationService = $federationService;
|
|
||||||
$this->logger = $logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,7 +120,7 @@ class DirectViewController extends Controller {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($item->getId(), null, $direct->getUid(), true);
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($item->getId(), null, $direct->getUid(), true);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error('Failed to generate token for existing file on direct editing', ['exception' => $e]);
|
$this->logger->error('Failed to generate token for existing file on direct editing', ['exception' => $e]);
|
||||||
return $this->renderErrorPage('Failed to open the requested file.');
|
return $this->renderErrorPage('Failed to open the requested file.');
|
||||||
|
|
@ -218,11 +181,11 @@ class DirectViewController extends Controller {
|
||||||
'directGuest' => empty($direct->getUid()),
|
'directGuest' => empty($direct->getUid()),
|
||||||
];
|
];
|
||||||
|
|
||||||
list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($node->getId(), $direct->getShare(), $direct->getUid(), true);
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($node->getId(), $direct->getShare(), $direct->getUid(), true);
|
||||||
if (!empty($direct->getInitiatorHost())) {
|
if (!empty($direct->getInitiatorHost())) {
|
||||||
$this->tokenManager->upgradeFromDirectInitiator($direct, $wopi);
|
$this->tokenManager->upgradeFromDirectInitiator($direct, $wopi);
|
||||||
}
|
}
|
||||||
$params['token'] = $token;
|
$params['token'] = $wopi->getToken();
|
||||||
$params['token_ttl'] = $wopi->getExpiry();
|
$params['token_ttl'] = $wopi->getExpiry();
|
||||||
$params['urlsrc'] = $urlSrc;
|
$params['urlsrc'] = $urlSrc;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,17 @@ use \OCP\AppFramework\Controller;
|
||||||
use \OCP\AppFramework\Http\TemplateResponse;
|
use \OCP\AppFramework\Http\TemplateResponse;
|
||||||
use \OCP\IConfig;
|
use \OCP\IConfig;
|
||||||
use \OCP\IRequest;
|
use \OCP\IRequest;
|
||||||
|
use Exception;
|
||||||
use OC\User\NoUserException;
|
use OC\User\NoUserException;
|
||||||
use OCA\Richdocuments\Service\FederationService;
|
use OCA\Richdocuments\Service\FederationService;
|
||||||
use OCA\Richdocuments\Service\InitialStateService;
|
use OCA\Richdocuments\Service\InitialStateService;
|
||||||
use OCA\Richdocuments\TemplateManager;
|
use OCA\Richdocuments\TemplateManager;
|
||||||
use OCA\Richdocuments\TokenManager;
|
use OCA\Richdocuments\TokenManager;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||||
|
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||||
|
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
use OCP\Constants;
|
use OCP\Constants;
|
||||||
use OCP\Files\File;
|
use OCP\Files\File;
|
||||||
|
|
@ -41,59 +46,23 @@ class DocumentController extends Controller {
|
||||||
|
|
||||||
public const SESSION_FILE_TARGET = 'richdocuments_openfile_target';
|
public const SESSION_FILE_TARGET = 'richdocuments_openfile_target';
|
||||||
|
|
||||||
/** @var ?string */
|
|
||||||
private $uid;
|
|
||||||
/** @var IConfig */
|
|
||||||
private $config;
|
|
||||||
/** @var AppConfig */
|
|
||||||
private $appConfig;
|
|
||||||
/** @var LoggerInterface */
|
|
||||||
private $logger;
|
|
||||||
/** @var IManager */
|
|
||||||
private $shareManager;
|
|
||||||
/** @var TokenManager */
|
|
||||||
private $tokenManager;
|
|
||||||
/** @var ISession */
|
|
||||||
private $session;
|
|
||||||
/** @var IRootFolder */
|
|
||||||
private $rootFolder;
|
|
||||||
/** @var TemplateManager */
|
|
||||||
private $templateManager;
|
|
||||||
/** @var FederationService */
|
|
||||||
private $federationService;
|
|
||||||
/** @var InitialStateService */
|
|
||||||
private $initialState;
|
|
||||||
private IURLGenerator $urlGenerator;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$appName,
|
string $appName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
IConfig $config,
|
private IConfig $config,
|
||||||
AppConfig $appConfig,
|
private AppConfig $appConfig,
|
||||||
IManager $shareManager,
|
private IManager $shareManager,
|
||||||
TokenManager $tokenManager,
|
private TokenManager $tokenManager,
|
||||||
IRootFolder $rootFolder,
|
private IRootFolder $rootFolder,
|
||||||
ISession $session,
|
private ISession $session,
|
||||||
$UserId,
|
private ?string $userId,
|
||||||
LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
TemplateManager $templateManager,
|
private TemplateManager $templateManager,
|
||||||
FederationService $federationService,
|
private FederationService $federationService,
|
||||||
InitialStateService $initialState,
|
private InitialStateService $initialState,
|
||||||
IURLGenerator $urlGenerator
|
private IURLGenerator $urlGenerator
|
||||||
) {
|
) {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
$this->uid = $UserId;
|
|
||||||
$this->config = $config;
|
|
||||||
$this->appConfig = $appConfig;
|
|
||||||
$this->shareManager = $shareManager;
|
|
||||||
$this->tokenManager = $tokenManager;
|
|
||||||
$this->rootFolder = $rootFolder;
|
|
||||||
$this->session = $session;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->templateManager = $templateManager;
|
|
||||||
$this->federationService = $federationService;
|
|
||||||
$this->initialState = $initialState;
|
|
||||||
$this->urlGenerator = $urlGenerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,7 +75,7 @@ class DocumentController extends Controller {
|
||||||
*
|
*
|
||||||
* @return array access_token, urlsrc
|
* @return array access_token, urlsrc
|
||||||
*/
|
*/
|
||||||
public function extAppGetData(int $fileId) {
|
public function extAppGetData(int $fileId): array {
|
||||||
$secretToken = $this->request->getParam('secret_token');
|
$secretToken = $this->request->getParam('secret_token');
|
||||||
$apps = array_filter(explode(',', $this->appConfig->getAppValue('external_apps')));
|
$apps = array_filter(explode(',', $this->appConfig->getAppValue('external_apps')));
|
||||||
foreach ($apps as $app) {
|
foreach ($apps as $app) {
|
||||||
|
|
@ -118,18 +87,18 @@ class DocumentController extends Controller {
|
||||||
'fileId' => $fileId
|
'fileId' => $fileId
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
$folder = $this->rootFolder->getUserFolder($this->uid);
|
$folder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
$item = $folder->getById($fileId)[0];
|
$item = $folder->getById($fileId)[0];
|
||||||
if (!($item instanceof Node)) {
|
if (!($item instanceof Node)) {
|
||||||
throw new \Exception();
|
throw new Exception();
|
||||||
}
|
}
|
||||||
list($urlSrc, $token) = $this->tokenManager->getToken($item->getId());
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($item->getId());
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'urlsrc' => $urlSrc,
|
'urlsrc' => $urlSrc,
|
||||||
'token' => $token
|
'token' => $wopi->getToken()
|
||||||
];
|
];
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +119,7 @@ class DocumentController extends Controller {
|
||||||
*/
|
*/
|
||||||
public function index($fileId, ?string $path = null) {
|
public function index($fileId, ?string $path = null) {
|
||||||
try {
|
try {
|
||||||
$folder = $this->rootFolder->getUserFolder($this->uid);
|
$folder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
|
|
||||||
if ($path !== null) {
|
if ($path !== null) {
|
||||||
$item = $folder->get($path);
|
$item = $folder->get($path);
|
||||||
|
|
@ -159,7 +128,7 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($item instanceof File)) {
|
if (!($item instanceof File)) {
|
||||||
throw new \Exception();
|
throw new Exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,17 +144,17 @@ class DocumentController extends Controller {
|
||||||
|
|
||||||
$templateFile = $this->templateManager->getTemplateSource($item->getId());
|
$templateFile = $this->templateManager->getTemplateSource($item->getId());
|
||||||
if ($templateFile) {
|
if ($templateFile) {
|
||||||
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($templateFile, $this->uid, $item->getId());
|
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($templateFile, $this->userId, $item->getId());
|
||||||
$token = $wopi->getToken();
|
$token = $wopi->getToken();
|
||||||
} else {
|
} else {
|
||||||
list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($item->getId());
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($item->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'permissions' => $item->getPermissions(),
|
'permissions' => $item->getPermissions(),
|
||||||
'title' => $item->getName(),
|
'title' => $item->getName(),
|
||||||
'fileId' => $item->getId() . '_' . $this->config->getSystemValue('instanceid'),
|
'fileId' => $item->getId() . '_' . $this->config->getSystemValue('instanceid'),
|
||||||
'token' => $token,
|
'token' => $wopi->getToken(),
|
||||||
'token_ttl' => $wopi->getExpiry(),
|
'token_ttl' => $wopi->getExpiry(),
|
||||||
'urlsrc' => $urlSrc,
|
'urlsrc' => $urlSrc,
|
||||||
'path' => $folder->getRelativePath($item->getPath()),
|
'path' => $folder->getRelativePath($item->getPath()),
|
||||||
|
|
@ -210,7 +179,7 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->documentTemplateResponse($wopi, $params);
|
return $this->documentTemplateResponse($wopi, $params);
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||||
return $this->renderErrorPage('Failed to open the requested file.');
|
return $this->renderErrorPage('Failed to open the requested file.');
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +203,7 @@ class DocumentController extends Controller {
|
||||||
return new TemplateResponse('core', '403', [], 'guest');
|
return new TemplateResponse('core', '403', [], 'guest');
|
||||||
}
|
}
|
||||||
|
|
||||||
$userFolder = $this->rootFolder->getUserFolder($this->uid);
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
try {
|
try {
|
||||||
$folder = $userFolder->get($dir);
|
$folder = $userFolder->get($dir);
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException $e) {
|
||||||
|
|
@ -248,7 +217,7 @@ class DocumentController extends Controller {
|
||||||
$file = $folder->newFile($fileName);
|
$file = $folder->newFile($fileName);
|
||||||
|
|
||||||
$template = $this->templateManager->get($templateId);
|
$template = $this->templateManager->get($templateId);
|
||||||
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($template, $this->uid, $file->getId());
|
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($template, $this->userId, $file->getId());
|
||||||
|
|
||||||
$wopiFileId = $wopi->getFileid() . '_' . $this->config->getSystemValue('instanceid');
|
$wopiFileId = $wopi->getFileid() . '_' . $this->config->getSystemValue('instanceid');
|
||||||
|
|
||||||
|
|
@ -272,7 +241,7 @@ class DocumentController extends Controller {
|
||||||
* @param string $shareToken
|
* @param string $shareToken
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @return TemplateResponse|RedirectResponse
|
* @return TemplateResponse|RedirectResponse
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function publicPage($shareToken, $fileName, $fileId) {
|
public function publicPage($shareToken, $fileName, $fileId) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -282,7 +251,7 @@ class DocumentController extends Controller {
|
||||||
if (!$this->session->exists('public_link_authenticated')
|
if (!$this->session->exists('public_link_authenticated')
|
||||||
|| $this->session->get('public_link_authenticated') !== (string)$share->getId()
|
|| $this->session->get('public_link_authenticated') !== (string)$share->getId()
|
||||||
) {
|
) {
|
||||||
throw new \Exception('Invalid password');
|
throw new Exception('Invalid password');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,7 +284,7 @@ class DocumentController extends Controller {
|
||||||
if ($templateFile) {
|
if ($templateFile) {
|
||||||
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($templateFile, $share->getShareOwner(), $item->getId());
|
list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($templateFile, $share->getShareOwner(), $item->getId());
|
||||||
} else {
|
} else {
|
||||||
list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($item->getId(), $shareToken, $this->uid);
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($item->getId(), $shareToken, $this->userId);
|
||||||
}
|
}
|
||||||
$params['token'] = $wopi->getToken();
|
$params['token'] = $wopi->getToken();
|
||||||
$params['token_ttl'] = $wopi->getExpiry();
|
$params['token_ttl'] = $wopi->getExpiry();
|
||||||
|
|
@ -324,7 +293,7 @@ class DocumentController extends Controller {
|
||||||
|
|
||||||
return $this->documentTemplateResponse($wopi, $params);
|
return $this->documentTemplateResponse($wopi, $params);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||||
return $this->renderErrorPage('Failed to open the requested file.');
|
return $this->renderErrorPage('Failed to open the requested file.');
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +321,7 @@ class DocumentController extends Controller {
|
||||||
if (!$this->session->exists('public_link_authenticated')
|
if (!$this->session->exists('public_link_authenticated')
|
||||||
|| $this->session->get('public_link_authenticated') !== (string)$share->getId()
|
|| $this->session->get('public_link_authenticated') !== (string)$share->getId()
|
||||||
) {
|
) {
|
||||||
throw new \Exception('Invalid password');
|
throw new Exception('Invalid password');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,11 +335,11 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($node instanceof Node) {
|
if ($node instanceof Node) {
|
||||||
list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($node->getId(), $shareToken, $this->uid);
|
list($urlSrc, $wopi) = $this->tokenManager->getToken($node->getId(), $shareToken, $this->userId);
|
||||||
|
|
||||||
$remoteWopi = $this->federationService->getRemoteFileDetails($remoteServer, $remoteServerToken);
|
$remoteWopi = $this->federationService->getRemoteFileDetails($remoteServer, $remoteServerToken);
|
||||||
if ($remoteWopi === null) {
|
if ($remoteWopi === null) {
|
||||||
throw new \Exception('Invalid remote file details for ' . $remoteServerToken);
|
throw new Exception('Invalid remote file details for ' . $remoteServerToken);
|
||||||
}
|
}
|
||||||
$this->tokenManager->upgradeToRemoteToken($wopi, $remoteWopi, $shareToken, $remoteServer, $remoteServerToken);
|
$this->tokenManager->upgradeToRemoteToken($wopi, $remoteWopi, $shareToken, $remoteServer, $remoteServerToken);
|
||||||
|
|
||||||
|
|
@ -383,7 +352,7 @@ class DocumentController extends Controller {
|
||||||
'permissions' => $permissions,
|
'permissions' => $permissions,
|
||||||
'title' => $node->getName(),
|
'title' => $node->getName(),
|
||||||
'fileId' => $node->getId() . '_' . $this->config->getSystemValue('instanceid'),
|
'fileId' => $node->getId() . '_' . $this->config->getSystemValue('instanceid'),
|
||||||
'token' => $token,
|
'token' => $wopi->getToken(),
|
||||||
'token_ttl' => $wopi->getExpiry(),
|
'token_ttl' => $wopi->getExpiry(),
|
||||||
'urlsrc' => $urlSrc,
|
'urlsrc' => $urlSrc,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
|
|
@ -394,7 +363,7 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
} catch (ShareNotFound $e) {
|
} catch (ShareNotFound $e) {
|
||||||
return new TemplateResponse('core', '404', [], 'guest');
|
return new TemplateResponse('core', '404', [], 'guest');
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||||
return $this->renderErrorPage('Failed to open the requested file.');
|
return $this->renderErrorPage('Failed to open the requested file.');
|
||||||
}
|
}
|
||||||
|
|
@ -422,10 +391,10 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userId === null) {
|
if ($userId === null) {
|
||||||
$userId = $this->uid;
|
$userId = $this->userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userId !== null && $userId !== $this->uid) {
|
if ($userId !== null && $userId !== $this->userId) {
|
||||||
return $this->renderErrorPage('You are trying to open a file from another user account than the one you are currently logged in with.');
|
return $this->renderErrorPage('You are trying to open a file from another user account than the one you are currently logged in with.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,12 +431,12 @@ class DocumentController extends Controller {
|
||||||
* @UseSession
|
* @UseSession
|
||||||
*/
|
*/
|
||||||
public function editOnlineTarget(int $fileId, ?string $target = null) {
|
public function editOnlineTarget(int $fileId, ?string $target = null) {
|
||||||
if (!$this->uid) {
|
if (!$this->userId) {
|
||||||
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
|
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$userFolder = $this->rootFolder->getUserFolder($this->uid);
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
$files = $userFolder->getById($fileId);
|
$files = $userFolder->getById($fileId);
|
||||||
$file = array_shift($files);
|
$file = array_shift($files);
|
||||||
if (!$file) {
|
if (!$file) {
|
||||||
|
|
@ -482,11 +451,32 @@ class DocumentController extends Controller {
|
||||||
}
|
}
|
||||||
$redirectUrl = $this->urlGenerator->getAbsoluteURL('/index.php/f/' . $file->getId());
|
$redirectUrl = $this->urlGenerator->getAbsoluteURL('/index.php/f/' . $file->getId());
|
||||||
return new RedirectResponse($redirectUrl);
|
return new RedirectResponse($redirectUrl);
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException|NotPermittedException|NoUserException) {
|
||||||
} catch (NotPermittedException $e) {
|
|
||||||
} catch (NoUserException $e) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
|
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[NoAdminRequired]
|
||||||
|
#[NoCSRFRequired]
|
||||||
|
#[PublicPage]
|
||||||
|
public function token(int $fileId, ?string $shareToken = null): DataResponse {
|
||||||
|
try {
|
||||||
|
// Get file and share
|
||||||
|
$templateFile = $this->templateManager->getTemplateSource($fileId);
|
||||||
|
if ($templateFile) {
|
||||||
|
[$urlSrc, $wopi] = $this->tokenManager->getTokenForTemplate($templateFile, $share->getShareOwner(), $item->getId());
|
||||||
|
} else {
|
||||||
|
[$urlSrc, $wopi] = $this->tokenManager->getToken($fileId, $shareToken, $this->userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataResponse(array_merge(
|
||||||
|
[ 'urlSrc' => $urlSrc ],
|
||||||
|
$wopi->jsonSerialize(),
|
||||||
|
));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->error('Failed to generate token for file', [ 'exception' => $e ]);
|
||||||
|
return new DataResponse('Failed to generate token', Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace OCA\Richdocuments\Controller;
|
namespace OCA\Richdocuments\Controller;
|
||||||
|
|
||||||
|
use OCA\Richdocuments\AppConfig;
|
||||||
use OCA\Richdocuments\Db\Wopi;
|
use OCA\Richdocuments\Db\Wopi;
|
||||||
use OCP\AppFramework\Http\FeaturePolicy;
|
use OCP\AppFramework\Http\FeaturePolicy;
|
||||||
use OCP\AppFramework\Http\TemplateResponse;
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
|
@ -9,7 +10,7 @@ use OCP\Collaboration\Reference\RenderReferenceEvent;
|
||||||
use OCP\EventDispatcher\IEventDispatcher;
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
|
|
||||||
trait DocumentTrait {
|
trait DocumentTrait {
|
||||||
private $appConfig;
|
private AppConfig $appConfig;
|
||||||
|
|
||||||
private function documentTemplateResponse(Wopi $wopi, array $params): TemplateResponse {
|
private function documentTemplateResponse(Wopi $wopi, array $params): TemplateResponse {
|
||||||
$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
|
$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
|
||||||
|
|
|
||||||
|
|
@ -510,9 +510,9 @@ class WopiController extends Controller {
|
||||||
|
|
||||||
if ($isPutRelative) {
|
if ($isPutRelative) {
|
||||||
// generate a token for the new file (the user still has to be logged in)
|
// generate a token for the new file (the user still has to be logged in)
|
||||||
list(, $wopiToken) = $this->tokenManager->getToken((string)$file->getId(), null, $wopi->getEditorUid(), $wopi->getDirect());
|
list(, $wopi) = $this->tokenManager->getToken((string)$file->getId(), null, $wopi->getEditorUid(), $wopi->getDirect());
|
||||||
|
|
||||||
$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
|
$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopi->getToken();
|
||||||
$url = $this->urlGenerator->getAbsoluteURL($wopi);
|
$url = $this->urlGenerator->getAbsoluteURL($wopi);
|
||||||
|
|
||||||
return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
|
return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
|
||||||
|
|
@ -684,9 +684,9 @@ class WopiController extends Controller {
|
||||||
|
|
||||||
// generate a token for the new file (the user still has to be
|
// generate a token for the new file (the user still has to be
|
||||||
// logged in)
|
// logged in)
|
||||||
list(, $wopiToken) = $this->tokenManager->getToken((string)$file->getId(), null, $wopi->getEditorUid(), $wopi->getDirect());
|
list(, $wopi) = $this->tokenManager->getToken((string)$file->getId(), null, $wopi->getEditorUid(), $wopi->getDirect());
|
||||||
|
|
||||||
$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
|
$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopi->getToken();
|
||||||
$url = $this->urlGenerator->getAbsoluteURL($wopi);
|
$url = $this->urlGenerator->getAbsoluteURL($wopi);
|
||||||
|
|
||||||
return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
|
return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ class InitialStateService {
|
||||||
$this->initialState->provideInitialState('hasDrawSupport', $this->capabilitiesService->hasDrawSupport());
|
$this->initialState->provideInitialState('hasDrawSupport', $this->capabilitiesService->hasDrawSupport());
|
||||||
$this->initialState->provideInitialState('hasNextcloudBranding', $this->capabilitiesService->hasNextcloudBranding());
|
$this->initialState->provideInitialState('hasNextcloudBranding', $this->capabilitiesService->hasNextcloudBranding());
|
||||||
|
|
||||||
|
$this->provideOptions();
|
||||||
|
|
||||||
$this->hasProvidedCapabilities = true;
|
$this->hasProvidedCapabilities = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,17 +78,8 @@ class InitialStateService {
|
||||||
$this->initialState->provideInitialState('document', $this->prepareParams($params));
|
$this->initialState->provideInitialState('document', $this->prepareParams($params));
|
||||||
|
|
||||||
$this->initialState->provideInitialState('wopi', $wopi);
|
$this->initialState->provideInitialState('wopi', $wopi);
|
||||||
$this->initialState->provideInitialState('theme', $this->config->getAppValue(Application::APPNAME, 'theme', 'nextcloud'));
|
|
||||||
$this->initialState->provideInitialState('uiDefaults', [
|
$this->provideOptions();
|
||||||
'UIMode' => $this->config->getAppValue(Application::APPNAME, 'uiDefaults-UIMode', 'notebookbar')
|
|
||||||
]);
|
|
||||||
$logoSet = $this->config->getAppValue('theming', 'logoheaderMime', '') !== '';
|
|
||||||
if (!$logoSet) {
|
|
||||||
$logoSet = $this->config->getAppValue('theming', 'logoMime', '') !== '';
|
|
||||||
}
|
|
||||||
$this->initialState->provideInitialState('theming-customLogo', ($logoSet ?
|
|
||||||
\OC::$server->getURLGenerator()->getAbsoluteURL(\OC::$server->getThemingDefaults()->getLogo())
|
|
||||||
: false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareParams(array $params): array {
|
public function prepareParams(array $params): array {
|
||||||
|
|
@ -108,4 +101,18 @@ class InitialStateService {
|
||||||
|
|
||||||
return array_merge($defaults, $params);
|
return array_merge($defaults, $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function provideOptions(): void {
|
||||||
|
$this->initialState->provideInitialState('theme', $this->config->getAppValue(Application::APPNAME, 'theme', 'nextcloud'));
|
||||||
|
$this->initialState->provideInitialState('uiDefaults', [
|
||||||
|
'UIMode' => $this->config->getAppValue(Application::APPNAME, 'uiDefaults-UIMode', 'notebookbar')
|
||||||
|
]);
|
||||||
|
$logoSet = $this->config->getAppValue('theming', 'logoheaderMime', '') !== '';
|
||||||
|
if (!$logoSet) {
|
||||||
|
$logoSet = $this->config->getAppValue('theming', 'logoMime', '') !== '';
|
||||||
|
}
|
||||||
|
$this->initialState->provideInitialState('theming-customLogo', ($logoSet ?
|
||||||
|
\OC::$server->getURLGenerator()->getAbsoluteURL(\OC::$server->getThemingDefaults()->getLogo())
|
||||||
|
: false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ class TokenManager {
|
||||||
$updatable = (bool)($share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE);
|
$updatable = (bool)($share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE);
|
||||||
$hideDownload = $share->getHideDownload();
|
$hideDownload = $share->getHideDownload();
|
||||||
$owneruid = $share->getShareOwner();
|
$owneruid = $share->getShareOwner();
|
||||||
|
$rootFolder = $this->rootFolder->getUserFolder($owneruid);
|
||||||
|
|
||||||
} elseif ($this->userId !== null) {
|
} elseif ($this->userId !== null) {
|
||||||
try {
|
try {
|
||||||
$editoruid = $this->userId;
|
$editoruid = $this->userId;
|
||||||
|
|
@ -205,7 +207,6 @@ class TokenManager {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$this->wopiParser->getUrlSrc($file->getMimeType())['urlsrc'], // url src might not be found ehre
|
$this->wopiParser->getUrlSrc($file->getMimeType())['urlsrc'], // url src might not be found ehre
|
||||||
$wopi->getToken(),
|
|
||||||
$wopi
|
$wopi
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +280,7 @@ class TokenManager {
|
||||||
|
|
||||||
public function newInitiatorToken($sourceServer, Node $node = null, $shareToken = null, bool $direct = false, $userId = null): Wopi {
|
public function newInitiatorToken($sourceServer, Node $node = null, $shareToken = null, bool $direct = false, $userId = null): Wopi {
|
||||||
if ($node !== null) {
|
if ($node !== null) {
|
||||||
list($urlSrc, $token, $wopi) = $this->getToken($node->getId(), $shareToken, $userId, $direct);
|
list($urlSrc, $wopi) = $this->getToken($node->getId(), $shareToken, $userId, $direct);
|
||||||
$wopi->setServerHost($sourceServer);
|
$wopi->setServerHost($sourceServer);
|
||||||
$wopi->setTokenType(Wopi::TOKEN_TYPE_INITIATOR);
|
$wopi->setTokenType(Wopi::TOKEN_TYPE_INITIATOR);
|
||||||
$this->wopiMapper->update($wopi);
|
$this->wopiMapper->update($wopi);
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ const odfViewer = {
|
||||||
$iframe.addClass('full')
|
$iframe.addClass('full')
|
||||||
$('#content').addClass('full-height')
|
$('#content').addClass('full-height')
|
||||||
$('footer').addClass('hidden')
|
$('footer').addClass('hidden')
|
||||||
$('#imgframe').addClass('hidden')
|
|
||||||
$('#controls').addClass('hidden')
|
$('#controls').addClass('hidden')
|
||||||
$('#content').addClass('loading')
|
$('#content').addClass('loading')
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -162,7 +161,6 @@ const odfViewer = {
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
$('#content').removeClass('full-height')
|
$('#content').removeClass('full-height')
|
||||||
$('footer').removeClass('hidden')
|
$('footer').removeClass('hidden')
|
||||||
$('#imgframe').removeClass('hidden')
|
|
||||||
$('.directLink').removeClass('hidden')
|
$('.directLink').removeClass('hidden')
|
||||||
$('.directDownload').removeClass('hidden')
|
$('.directDownload').removeClass('hidden')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const getUIDefaults = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCollaboraTheme = () => {
|
const getCollaboraTheme = () => {
|
||||||
return loadState('richdocuments', 'theme', '')
|
return loadState('richdocuments', 'theme', 'nextcloud')
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateCSSVarTokens = () => {
|
const generateCSSVarTokens = () => {
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,17 @@ const splitPath = (path) => {
|
||||||
return [directory, fileName]
|
return [directory, fileName]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRandomId = (length = 5) => {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.replace(/[^a-z]+/g, '')
|
||||||
|
.slice(0, length || 5)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
languageToBCP47,
|
languageToBCP47,
|
||||||
getNextcloudVersion,
|
getNextcloudVersion,
|
||||||
splitPath,
|
splitPath,
|
||||||
|
getRandomId,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
src/mixins/openLocal.js
Normal file
91
src/mixins/openLocal.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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 { getRootUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import { getNextcloudUrl } from '../helpers/url.js'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
|
// FIXME: Migrate to vue component
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
openingLocally: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unlockAndOpenLocally() {
|
||||||
|
if (this.openingLocally) {
|
||||||
|
this.unlockFile()
|
||||||
|
.catch(_ => {}) // Unlocking failed, possibly because file was not locked, we want to proceed regardless.
|
||||||
|
.then(() => {
|
||||||
|
this.openLocally()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showOpenLocalConfirmation() {
|
||||||
|
// FIXME: Migrate to vue
|
||||||
|
window.OC.dialogs.confirmDestructive(
|
||||||
|
t('richdocuments', 'When opening a file locally, the document will close for all users currently viewing the document.'),
|
||||||
|
t('richdocuments', 'Open file locally'),
|
||||||
|
{
|
||||||
|
type: OC.dialogs.YES_NO_BUTTONS,
|
||||||
|
confirm: t('richdocuments', 'Open locally'),
|
||||||
|
confirmClasses: 'error',
|
||||||
|
cancel: t('richdocuments', 'Continue editing online'),
|
||||||
|
},
|
||||||
|
(decision) => {
|
||||||
|
if (!decision) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.openingLocally = true
|
||||||
|
this.sendPostMessage('Get_Views')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
unlockFile() {
|
||||||
|
const unlockUrl = getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + this.fileid
|
||||||
|
const unlockConfig = {
|
||||||
|
headers: { 'X-WOPI-Override': 'UNLOCK' },
|
||||||
|
}
|
||||||
|
return axios.post(unlockUrl, { access_token: this.formData.accessToken }, unlockConfig)
|
||||||
|
},
|
||||||
|
|
||||||
|
openLocally() {
|
||||||
|
if (this.openingLocally) {
|
||||||
|
this.openingLocally = false
|
||||||
|
|
||||||
|
axios.post(
|
||||||
|
OC.linkToOCS('apps/files/api/v1', 2) + 'openlocaleditor?format=json',
|
||||||
|
{ path: this.filename }
|
||||||
|
).then((result) => {
|
||||||
|
const url = 'nc://open/'
|
||||||
|
+ getCurrentUser()?.uid + '@' + getNextcloudUrl()
|
||||||
|
+ OC.encodePath(this.filename)
|
||||||
|
+ '?token=' + result.data.ocs.data.token
|
||||||
|
|
||||||
|
this.showOpenLocalConfirmation(url, window.top)
|
||||||
|
window.location.href = url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
84
src/mixins/pickLink.js
Normal file
84
src/mixins/pickLink.js
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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 { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
|
||||||
|
|
||||||
|
// FIXME: Migrate to vue component
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
async pickLink() {
|
||||||
|
try {
|
||||||
|
if (this.showLinkPicker) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.showLinkPicker = true
|
||||||
|
const link = await getLinkWithPicker(null, true)
|
||||||
|
try {
|
||||||
|
const url = new URL(link)
|
||||||
|
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||||
|
this.sendPostMessage('Action_InsertLink', { url: link })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug('error when parsing the link picker result')
|
||||||
|
}
|
||||||
|
this.sendPostMessage('Action_Paste', { Mimetype: 'text/plain', Data: link })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Link picker promise rejected :', e)
|
||||||
|
} finally {
|
||||||
|
this.showLinkPicker = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async resolveLink(url) {
|
||||||
|
try {
|
||||||
|
const result = await axios.get(generateOcsUrl('references/resolve', 2), {
|
||||||
|
params: {
|
||||||
|
reference: url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const resolvedLink = result.data.ocs.data.references[url]
|
||||||
|
const title = resolvedLink?.openGraphObject?.name
|
||||||
|
const thumbnailUrl = resolvedLink?.openGraphObject?.thumb
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
try {
|
||||||
|
const imageResponse = await axios.get(thumbnailUrl, { responseType: 'blob' })
|
||||||
|
if (imageResponse?.status === 200 && imageResponse?.data) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.addEventListener('loadend', (e) => {
|
||||||
|
const b64Image = e.target.result
|
||||||
|
this.sendPostMessage('Action_GetLinkPreview_Resp', { url, title, image: b64Image })
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(imageResponse.data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading the reference image', e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sendPostMessage('Action_GetLinkPreview_Resp', { url, title, image: null })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error resolving a reference', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
51
src/mixins/saveAs.js
Normal file
51
src/mixins/saveAs.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// FIXME: Migrate to vue component
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
async saveAs(format) {
|
||||||
|
window.OC.dialogs.prompt(
|
||||||
|
t('richdocuments', 'Please enter the filename to store the document as.'),
|
||||||
|
t('richdocuments', 'Save As'),
|
||||||
|
(result, value) => {
|
||||||
|
if (result === true && value) {
|
||||||
|
this.sendPostMessage('Action_SaveAs', { Filename: value, Notify: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
t('richdocuments', 'New filename'),
|
||||||
|
false
|
||||||
|
).then(() => {
|
||||||
|
const $dialog = $('.oc-dialog:visible')
|
||||||
|
const $buttons = $dialog.find('.oc-dialog-buttonrow button')
|
||||||
|
$buttons.eq(0).text(t('richdocuments', 'Cancel'))
|
||||||
|
$buttons.eq(1).text(t('richdocuments', 'Save'))
|
||||||
|
const nameInput = $dialog.find('input')[0]
|
||||||
|
nameInput.style.minWidth = '250px'
|
||||||
|
nameInput.style.maxWidth = '400px'
|
||||||
|
nameInput.value = format ? this.basename.substring(0, this.basename.lastIndexOf('.') + 1) + format : this.basename
|
||||||
|
nameInput.selectionStart = 0
|
||||||
|
nameInput.selectionEnd = this.basename.lastIndexOf('.')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
49
src/mixins/uiMention.js
Normal file
49
src/mixins/uiMention.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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 { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import Config from '../services/config.tsx'
|
||||||
|
import { getNextcloudUrl } from '../helpers/url.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
async uiMention(search) {
|
||||||
|
let users = []
|
||||||
|
|
||||||
|
if (Config.get('userId') !== null) {
|
||||||
|
try {
|
||||||
|
const result = await axios.get(generateOcsUrl('core/autocomplete/get'), {
|
||||||
|
params: { search },
|
||||||
|
})
|
||||||
|
users = result.data.ocs.data
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = users.map((user) => {
|
||||||
|
const profile = window.location.protocol + '//' + getNextcloudUrl() + '/index.php/u/' + user.id
|
||||||
|
return { username: user.label, profile }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sendPostMessage('Action_Mention', { list })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
74
src/mixins/version.js
Normal file
74
src/mixins/version.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||||
|
import { generateRemoteUrl, getRootUrl } from '@nextcloud/router'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
import axios from '@nextcloud/axios'
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
versionToRestore: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
subscribe('files_versions:restore:requested', this.onRestoreRequested)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
unsubscribe('files_versions:restore:requested', this.onRestoreRequested)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onRestoreRequested(eventState) {
|
||||||
|
// Tell Collabora that we are about to restore a version
|
||||||
|
this.sendPostMessage('Host_VersionRestore', {
|
||||||
|
Status: 'Pre_Restore',
|
||||||
|
})
|
||||||
|
|
||||||
|
this.versionToRestore = eventState.version
|
||||||
|
|
||||||
|
// Prevent files_versions own restore as we'd need to wait for Collabora to be ready
|
||||||
|
eventState.preventDefault = true
|
||||||
|
},
|
||||||
|
async handlePreRestoreAck() {
|
||||||
|
const restoreUrl = getRootUrl() + '/remote.php/dav/versions/' + getCurrentUser().uid
|
||||||
|
+ '/versions/' + this.fileid + '/' + this.versionToRestore.fileVersion
|
||||||
|
try {
|
||||||
|
await axios({
|
||||||
|
method: 'MOVE',
|
||||||
|
url: restoreUrl,
|
||||||
|
headers: {
|
||||||
|
Destination: generateRemoteUrl('dav') + '/versions/' + getCurrentUser().uid + '/restore/target',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
emit('files_versions:restore:restored', this.versionToRestore)
|
||||||
|
} catch (e) {
|
||||||
|
showError(t('richdocuments', 'Failed to revert the document to older version'))
|
||||||
|
}
|
||||||
|
this.versionToRestore = null
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,8 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateUrl, generateRemoteUrl, getRootUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
import moment from '@nextcloud/moment'
|
|
||||||
import { getCurrentDirectory } from '../helpers/filesApp.js'
|
import { getCurrentDirectory } from '../helpers/filesApp.js'
|
||||||
|
|
||||||
const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1'
|
const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1'
|
||||||
|
|
@ -80,10 +79,6 @@ export default {
|
||||||
this.getFileList().hideMask && this.getFileList().hideMask()
|
this.getFileList().hideMask && this.getFileList().hideMask()
|
||||||
this.getFileList().setPageTitle && this.getFileList().setPageTitle(this.fileName)
|
this.getFileList().setPageTitle && this.getFileList().setPageTitle(this.fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPublic) {
|
|
||||||
this.addVersionSidebarEvents()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|
@ -94,9 +89,6 @@ export default {
|
||||||
this.updateFileInfo(undefined, Date.now())
|
this.updateFileInfo(undefined, Date.now())
|
||||||
|
|
||||||
this.fileModel = null
|
this.fileModel = null
|
||||||
if (!isPublic) {
|
|
||||||
this.removeVersionSidebarEvents()
|
|
||||||
}
|
|
||||||
$('#richdocuments-header').remove()
|
$('#richdocuments-header').remove()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -131,9 +123,9 @@ export default {
|
||||||
console.error('[FilesAppIntegration] Sharing is not supported')
|
console.error('[FilesAppIntegration] Sharing is not supported')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (OCA.Files.Sidebar) {
|
|
||||||
OCA.Files.Sidebar.open(this.filePath + '/' + this.fileName)
|
OCA?.Files?.Sidebar?.open(this.filePath + '/' + this.fileName)
|
||||||
}
|
OCA?.Files?.Sidebar?.setActiveTab('sharing')
|
||||||
},
|
},
|
||||||
|
|
||||||
rename(newName) {
|
rename(newName) {
|
||||||
|
|
@ -427,115 +419,17 @@ export default {
|
||||||
avatardiv.append(popover)
|
avatardiv.append(popover)
|
||||||
},
|
},
|
||||||
|
|
||||||
addVersionSidebarEvents() {
|
|
||||||
$(document.querySelector('#content')).on('click.revisions', '.app-sidebar .preview-container', this.showVersionPreview.bind(this))
|
|
||||||
$(document.querySelector('#content')).on('click.revisions', '.app-sidebar .downloadVersion', this.showVersionPreview.bind(this))
|
|
||||||
$(document.querySelector('#content')).on('mousedown.revisions', '.app-sidebar .revertVersion', this.restoreVersion.bind(this))
|
|
||||||
$(document.querySelector('#content')).on('click.revisionsTab', '.app-sidebar [data-tabid=versionsTabView]', this.addCurrentVersion.bind(this))
|
|
||||||
},
|
|
||||||
|
|
||||||
removeVersionSidebarEvents() {
|
|
||||||
$(document.querySelector('#content')).off('click.revisions')
|
|
||||||
$(document.querySelector('#content')).off('click.revisions')
|
|
||||||
$(document.querySelector('#content')).off('mousedown.revisions')
|
|
||||||
$(document.querySelector('#content')).off('click.revisionsTab')
|
|
||||||
},
|
|
||||||
|
|
||||||
addCurrentVersion() {
|
|
||||||
$('#lastSavedVersion').remove()
|
|
||||||
$('#currentVersion').remove()
|
|
||||||
if (this.getFileModel()) {
|
|
||||||
const preview = OC.MimeType.getIconUrl(this.getFileModel().get('mimetype'))
|
|
||||||
const mtime = this.getFileModel().get('mtime')
|
|
||||||
$('.tab.versionsTabView').prepend('<ul id="lastSavedVersion"><li data-revision="0"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n'
|
|
||||||
+ '<div><a class="downloadVersion">' + t('richdocuments', 'Last saved version') + ' (<span class="versiondate has-tooltip live-relative-timestamp" data-timestamp="' + mtime + '"></span>)</div></div></li></ul>')
|
|
||||||
$('.tab.versionsTabView').prepend('<ul id="currentVersion"><li data-revision="" class="active"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n'
|
|
||||||
+ '<div><a class="downloadVersion">' + t('richdocuments', 'Current version (unsaved changes)') + '</a></div></div></li></ul>')
|
|
||||||
$('.live-relative-timestamp').each(function() {
|
|
||||||
$(this).text(moment(parseInt($(this).attr('data-timestamp'), 10)).fromNow())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showRevHistory() {
|
showRevHistory() {
|
||||||
if (this.handlers.showRevHistory && this.handlers.showRevHistory(this)) {
|
if (this.handlers.showRevHistory && this.handlers.showRevHistory(this)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getFileList()) {
|
if (isPublic || !this.getFileList()) {
|
||||||
this.getFileList()
|
console.error('[FilesAppIntegration] Versions are not supported')
|
||||||
.showDetailsView(this.fileName, 'versionsTabView')
|
return
|
||||||
this.addCurrentVersion()
|
|
||||||
}
|
}
|
||||||
},
|
OCA?.Files?.Sidebar?.open(this.filePath + '/' + this.fileName)
|
||||||
|
OCA?.Files?.Sidebar?.setActiveTab('version_vue')
|
||||||
showVersionPreview(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
let element = e.currentTarget.parentElement.parentElement
|
|
||||||
if ($(e.currentTarget).hasClass('downloadVersion')) {
|
|
||||||
element = e.currentTarget.parentElement.parentElement.parentElement.parentElement
|
|
||||||
}
|
|
||||||
const version = element.dataset.revision
|
|
||||||
const fileId = this.fileId
|
|
||||||
const title = this.fileName
|
|
||||||
console.debug('[FilesAppIntegration] showVersionPreview', version, fileId, title)
|
|
||||||
this.sendPostMessage('Action_loadRevViewer', { fileId, title, version })
|
|
||||||
$(element.parentElement.parentElement).find('li').removeClass('active')
|
|
||||||
$(element).addClass('active')
|
|
||||||
},
|
|
||||||
|
|
||||||
restoreVersion(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
this.sendPostMessage('Host_VersionRestore', { Status: 'Pre_Restore' })
|
|
||||||
|
|
||||||
const version = e.currentTarget.parentElement.parentElement.dataset.revision
|
|
||||||
|
|
||||||
this._restoreVersionCallback = () => {
|
|
||||||
this._restoreDAV(version)
|
|
||||||
this._restoreVersionCallback = null
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
restoreVersionExecute() {
|
|
||||||
if (this._restoreVersionCallback !== null) {
|
|
||||||
this._restoreVersionCallback()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
restoreVersionAbort() {
|
|
||||||
this._restoreVersionCallback = null
|
|
||||||
},
|
|
||||||
|
|
||||||
_restoreSuccess(response) {
|
|
||||||
if (response.status === 'error') {
|
|
||||||
OC.Notification.showTemporary(t('richdocuments', 'Failed to revert the document to older version'))
|
|
||||||
}
|
|
||||||
// Reload the document frame to get the new file
|
|
||||||
// TODO: ideally we should have a post messsage that can be sent to collabora to just reload the file once the restore is finished
|
|
||||||
document.getElementById('richdocumentsframe').src = document.getElementById('richdocumentsframe').src
|
|
||||||
OC.Apps.hideAppSidebar()
|
|
||||||
},
|
|
||||||
|
|
||||||
_restoreError() {
|
|
||||||
OC.Notification.showTemporary(t('richdocuments', 'Failed to revert the document to older version'))
|
|
||||||
},
|
|
||||||
|
|
||||||
_restoreDAV(version) {
|
|
||||||
const restoreUrl = getRootUrl() + '/remote.php/dav/versions/' + getCurrentUser().uid
|
|
||||||
+ '/versions/' + this.fileId + '/' + version
|
|
||||||
$.ajax({
|
|
||||||
type: 'MOVE',
|
|
||||||
url: restoreUrl,
|
|
||||||
headers: {
|
|
||||||
Destination: generateRemoteUrl('dav') + '/versions/' + getCurrentUser().uid + '/restore/target',
|
|
||||||
},
|
|
||||||
success: this._restoreSuccess.bind(this),
|
|
||||||
error: this._restoreError.bind(this),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,78 +21,91 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<transition name="fade" appear>
|
<div class="office-viewer">
|
||||||
<div class="office-viewer">
|
<div v-if="showLoadingIndicator"
|
||||||
<div v-if="showLoadingIndicator"
|
class="office-viewer__loading-overlay"
|
||||||
class="office-viewer__loading-overlay"
|
:class="{ debug: debug }">
|
||||||
:class="{ debug: debug }">
|
<NcEmptyContent v-if="!error" :title="t('richdocuments', 'Loading {filename} …', { filename: basename }, 1, {escape: false})">
|
||||||
<NcEmptyContent v-if="!error" :title="t('richdocuments', 'Loading {filename} …', { filename: basename }, 1, {escape: false})">
|
<template #icon>
|
||||||
<template #icon>
|
<NcLoadingIcon />
|
||||||
<NcLoadingIcon />
|
</template>
|
||||||
</template>
|
<template #action>
|
||||||
<template #action>
|
<NcButton @click="close">
|
||||||
<NcButton @click="close">
|
{{ t('richdocuments', 'Cancel') }}
|
||||||
{{ t('richdocuments', 'Cancel') }}
|
</NcButton>
|
||||||
</NcButton>
|
</template>
|
||||||
</template>
|
</NcEmptyContent>
|
||||||
</NcEmptyContent>
|
<NcEmptyContent v-else :title="t('richdocuments', 'Document loading failed')" :description="errorMessage">
|
||||||
<NcEmptyContent v-else :title="t('richdocuments', 'Document loading failed')" :description="errorMessage">
|
<template #icon>
|
||||||
<template #icon>
|
<AlertOctagonOutline />
|
||||||
<AlertOctagonOutline />
|
</template>
|
||||||
</template>
|
<template #action>
|
||||||
<template #action>
|
<NcButton @click="close">
|
||||||
<NcButton @click="close">
|
{{ t('richdocuments', 'Close') }}
|
||||||
{{ t('richdocuments', 'Close') }}
|
</NcButton>
|
||||||
</NcButton>
|
</template>
|
||||||
</template>
|
</NcEmptyContent>
|
||||||
</NcEmptyContent>
|
|
||||||
</div>
|
|
||||||
<div v-show="!useNativeHeader && showIframe" class="office-viewer__header">
|
|
||||||
<div class="avatars">
|
|
||||||
<NcAvatar v-for="view in avatarViews"
|
|
||||||
:key="view.ViewId"
|
|
||||||
:user="view.UserId"
|
|
||||||
:display-name="view.UserName"
|
|
||||||
:show-user-status="false"
|
|
||||||
:show-user-status-compact="false"
|
|
||||||
:style="viewColor(view)" />
|
|
||||||
</div>
|
|
||||||
<NcActions>
|
|
||||||
<NcActionButton icon="office-viewer__header__icon-menu-sidebar" @click="share" />
|
|
||||||
</NcActions>
|
|
||||||
</div>
|
|
||||||
<iframe id="collaboraframe"
|
|
||||||
ref="documentFrame"
|
|
||||||
data-cy="documentframe"
|
|
||||||
class="office-viewer__iframe"
|
|
||||||
:style="{visibility: showIframe ? 'visible' : 'hidden' }"
|
|
||||||
:src="src" />
|
|
||||||
|
|
||||||
<ZoteroHint :show.sync="showZotero" @submit="reload" />
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
<form ref="form"
|
||||||
|
:target="iframeId"
|
||||||
|
:action="formData.action"
|
||||||
|
method="post">
|
||||||
|
<input name="access_token" :value="formData.accessToken" type="hidden">
|
||||||
|
<input name="access_token_ttl" :value="formData.accessTokenTTL" type="hidden">
|
||||||
|
<input name="ui_defaults" :value="formData.uiDefaults" type="hidden">
|
||||||
|
<input name="css_variables" :value="formData.cssVariables" type="hidden">
|
||||||
|
<input name="theme" :value="formData.theme" type="hidden">
|
||||||
|
<input name="buy_product" value="https://nextcloud.com/pricing" type="hidden">
|
||||||
|
</form>
|
||||||
|
<iframe :id="iframeId"
|
||||||
|
ref="documentFrame"
|
||||||
|
:name="iframeId"
|
||||||
|
data-cy="documentframe"
|
||||||
|
scrolling="no"
|
||||||
|
allowfullscreen
|
||||||
|
class="office-viewer__iframe"
|
||||||
|
:style="{visibility: showIframe ? 'visible' : 'hidden' }"
|
||||||
|
:src="iframeSrc" />
|
||||||
|
|
||||||
|
<ZoteroHint :show.sync="showZotero" @submit="reload" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { NcAvatar, NcButton, NcActions, NcActionButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
|
import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
|
||||||
import AlertOctagonOutline from 'vue-material-design-icons/AlertOctagonOutline.vue'
|
import AlertOctagonOutline from 'vue-material-design-icons/AlertOctagonOutline.vue'
|
||||||
import { loadState } from '@nextcloud/initial-state'
|
import { loadState } from '@nextcloud/initial-state'
|
||||||
|
|
||||||
import ZoteroHint from '../components/Modal/ZoteroHint.vue'
|
import ZoteroHint from '../components/Modal/ZoteroHint.vue'
|
||||||
import { basename, dirname } from 'path'
|
import { basename, dirname } from 'path'
|
||||||
import { getDocumentUrlForFile, getDocumentUrlForPublicFile } from '../helpers/url.js'
|
import { getRandomId } from '../helpers/index.js'
|
||||||
|
import {
|
||||||
|
getNextcloudUrl,
|
||||||
|
getWopiUrl,
|
||||||
|
} from '../helpers/url.js'
|
||||||
import PostMessageService from '../services/postMessage.tsx'
|
import PostMessageService from '../services/postMessage.tsx'
|
||||||
import FilesAppIntegration from './FilesAppIntegration.js'
|
import FilesAppIntegration from './FilesAppIntegration.js'
|
||||||
import { LOADING_ERROR, checkCollaboraConfiguration, checkProxyStatus } from '../services/collabora.js'
|
import { LOADING_ERROR, checkCollaboraConfiguration, checkProxyStatus } from '../services/collabora.js'
|
||||||
import { enableScrollLock, disableScrollLock } from '../helpers/safariFixer.js'
|
import { enableScrollLock, disableScrollLock } from '../helpers/safariFixer.js'
|
||||||
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { generateOcsUrl } from '@nextcloud/router'
|
import {
|
||||||
|
generateUrl,
|
||||||
|
imagePath,
|
||||||
|
} from '@nextcloud/router'
|
||||||
|
import { getCapabilities } from '@nextcloud/capabilities'
|
||||||
|
import {
|
||||||
|
generateCSSVarTokens,
|
||||||
|
getCollaboraTheme,
|
||||||
|
getUIDefaults,
|
||||||
|
} from '../helpers/coolParameters.js'
|
||||||
|
import Config from '../services/config.tsx'
|
||||||
|
import openLocal from '../mixins/openLocal.js'
|
||||||
|
import pickLink from '../mixins/pickLink.js'
|
||||||
|
import saveAs from '../mixins/saveAs.js'
|
||||||
|
import uiMention from '../mixins/uiMention.js'
|
||||||
|
import version from '../mixins/version.js'
|
||||||
|
|
||||||
const FRAME_DOCUMENT = 'FRAME_DOCUMENT'
|
const FRAME_DOCUMENT = 'FRAME_DOCUMENT'
|
||||||
const PostMessages = new PostMessageService({
|
|
||||||
FRAME_DOCUMENT: () => document.getElementById('collaboraframe').contentWindow,
|
|
||||||
})
|
|
||||||
|
|
||||||
const LOADING_STATE = {
|
const LOADING_STATE = {
|
||||||
LOADING: 0,
|
LOADING: 0,
|
||||||
|
|
@ -105,14 +118,14 @@ export default {
|
||||||
name: 'Office',
|
name: 'Office',
|
||||||
components: {
|
components: {
|
||||||
AlertOctagonOutline,
|
AlertOctagonOutline,
|
||||||
NcAvatar,
|
|
||||||
NcActions,
|
|
||||||
NcActionButton,
|
|
||||||
NcButton,
|
NcButton,
|
||||||
NcEmptyContent,
|
NcEmptyContent,
|
||||||
NcLoadingIcon,
|
NcLoadingIcon,
|
||||||
ZoteroHint,
|
ZoteroHint,
|
||||||
},
|
},
|
||||||
|
mixins: [
|
||||||
|
openLocal, pickLink, saveAs, uiMention, version,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
filename: {
|
filename: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -127,36 +140,35 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => false,
|
default: () => false,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
src: null,
|
postMessage: null,
|
||||||
|
iframeId: 'collaboraframe_' + getRandomId(),
|
||||||
|
iframeSrc: null,
|
||||||
loading: LOADING_STATE.LOADING,
|
loading: LOADING_STATE.LOADING,
|
||||||
loadingTimeout: null,
|
loadingTimeout: null,
|
||||||
error: null,
|
error: null,
|
||||||
views: [],
|
views: [],
|
||||||
|
|
||||||
showZotero: false,
|
|
||||||
showLinkPicker: false,
|
showLinkPicker: false,
|
||||||
|
showZotero: false,
|
||||||
|
|
||||||
|
formData: {
|
||||||
|
action: null,
|
||||||
|
accessToken: null,
|
||||||
|
accessTokenTTL: null,
|
||||||
|
uiDefaults: getUIDefaults(),
|
||||||
|
cssVariables: generateCSSVarTokens(),
|
||||||
|
theme: getCollaboraTheme(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
basename() {
|
|
||||||
return basename(this.filename)
|
|
||||||
},
|
|
||||||
useNativeHeader() {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
avatarViews() {
|
|
||||||
return this.views
|
|
||||||
},
|
|
||||||
viewColor() {
|
|
||||||
return view => ({
|
|
||||||
'border-color': '#' + ('000000' + Number(view.Color).toString(16)).slice(-6),
|
|
||||||
'border-width': '2px',
|
|
||||||
'border-style': 'solid',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
showIframe() {
|
showIframe() {
|
||||||
return this.loading >= LOADING_STATE.FRAME_READY
|
return this.loading >= LOADING_STATE.FRAME_READY
|
||||||
},
|
},
|
||||||
|
|
@ -176,8 +188,17 @@ export default {
|
||||||
debug() {
|
debug() {
|
||||||
return !!window.TESTING
|
return !!window.TESTING
|
||||||
},
|
},
|
||||||
|
isPublic() {
|
||||||
|
return document.getElementById('isPublic')?.value === '1'
|
||||||
|
},
|
||||||
|
shareToken() {
|
||||||
|
return document.getElementById('sharingToken')?.value
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.postMessage = new PostMessageService({
|
||||||
|
FRAME_DOCUMENT: () => document.getElementById(this.iframeId).contentWindow,
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
await checkCollaboraConfiguration()
|
await checkCollaboraConfiguration()
|
||||||
await checkProxyStatus()
|
await checkProxyStatus()
|
||||||
|
|
@ -187,99 +208,77 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileList = OCA?.Files?.App?.getCurrentFileList?.()
|
if (this.fileid) {
|
||||||
FilesAppIntegration.init({
|
const fileList = OCA?.Files?.App?.getCurrentFileList?.()
|
||||||
fileName: basename(this.filename),
|
FilesAppIntegration.init({
|
||||||
fileId: this.fileid,
|
fileName: basename(this.filename),
|
||||||
filePath: dirname(this.filename),
|
fileId: this.fileid,
|
||||||
fileList,
|
filePath: dirname(this.filename),
|
||||||
fileModel: fileList?.getModelForFile(basename(this.filename)),
|
fileList,
|
||||||
sendPostMessage: (msgId, values) => {
|
fileModel: fileList?.getModelForFile(basename(this.filename)),
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values)
|
sendPostMessage: (msgId, values) => {
|
||||||
},
|
this.postMessage.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values)
|
||||||
})
|
},
|
||||||
PostMessages.registerPostMessageHandler(this.postMessageHandler)
|
})
|
||||||
|
}
|
||||||
|
this.postMessage.registerPostMessageHandler(this.postMessageHandler)
|
||||||
|
|
||||||
this.load()
|
this.load()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
PostMessages.unregisterPostMessageHandler(this.postMessageHandler)
|
this.postMessage.unregisterPostMessageHandler(this.postMessageHandler)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async load() {
|
async load() {
|
||||||
|
const fileid = this.fileid ?? basename(dirname(this.source))
|
||||||
|
const version = this.fileid ? 0 : basename(this.source)
|
||||||
|
|
||||||
enableScrollLock()
|
enableScrollLock()
|
||||||
const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1'
|
|
||||||
this.src = getDocumentUrlForFile(this.filename, this.fileid) + '&path=' + encodeURIComponent(this.filename)
|
// Generate WOPI token
|
||||||
if (isPublic) {
|
const { data } = await axios.post(generateUrl('/apps/richdocuments/token'), {
|
||||||
this.src = getDocumentUrlForPublicFile(this.filename, this.fileid)
|
fileId: fileid, shareToken: this.shareToken,
|
||||||
}
|
})
|
||||||
|
Config.update('urlsrc', data.urlSrc)
|
||||||
|
|
||||||
|
// Generate form and submit to the iframe
|
||||||
|
const action = getWopiUrl({
|
||||||
|
fileId: fileid + '_' + loadState('richdocuments', 'instanceId', 'instanceid') + (version > 0 ? '_' + version : ''),
|
||||||
|
title: this.filename,
|
||||||
|
readOnly: version > 0,
|
||||||
|
revisionHistory: !this.isPublic,
|
||||||
|
closeButton: !Config.get('hideCloseButton'),
|
||||||
|
})
|
||||||
|
this.$set(this.formData, 'action', action)
|
||||||
|
this.$set(this.formData, 'accessToken', data.token)
|
||||||
|
this.$nextTick(() => this.$refs.form.submit())
|
||||||
|
|
||||||
this.loading = LOADING_STATE.LOADING
|
this.loading = LOADING_STATE.LOADING
|
||||||
this.loadingTimeout = setTimeout(() => {
|
this.loadingTimeout = setTimeout(() => {
|
||||||
console.error('FAILED')
|
console.error('Document loading failed due to timeout: Please check for failing network requests')
|
||||||
this.loading = LOADING_STATE.FAILED
|
this.loading = LOADING_STATE.FAILED
|
||||||
this.error = t('richdocuments', 'Failed to load {productName} - please try again later', { productName: loadState('richdocuments', 'productName', 'Nextcloud Office') })
|
this.error = t('richdocuments', 'Failed to load {productName} - please try again later', { productName: loadState('richdocuments', 'productName', 'Nextcloud Office') })
|
||||||
}, (OC.getCapabilities().richdocuments.config.timeout * 1000 || 15000))
|
}, (getCapabilities().richdocuments.config.timeout * 1000 || 15000))
|
||||||
|
},
|
||||||
|
sendPostMessage(msgId, values = {}) {
|
||||||
|
this.postMessage.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values)
|
||||||
},
|
},
|
||||||
documentReady() {
|
documentReady() {
|
||||||
this.loading = LOADING_STATE.DOCUMENT_READY
|
this.loading = LOADING_STATE.DOCUMENT_READY
|
||||||
clearTimeout(this.loadingTimeout)
|
clearTimeout(this.loadingTimeout)
|
||||||
|
this.sendPostMessage('Host_PostmessageReady')
|
||||||
|
this.sendPostMessage('Insert_Button', {
|
||||||
|
id: 'Open_Local_Editor',
|
||||||
|
imgurl: window.location.protocol + '//' + getNextcloudUrl() + imagePath('richdocuments', 'launch.svg'),
|
||||||
|
mobile: false,
|
||||||
|
label: t('richdocuments', 'Open in local editor'),
|
||||||
|
hint: t('richdocuments', 'Open in local editor'),
|
||||||
|
insertBefore: 'print',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async share() {
|
async share() {
|
||||||
FilesAppIntegration.share()
|
FilesAppIntegration.share()
|
||||||
},
|
},
|
||||||
async pickLink() {
|
|
||||||
try {
|
|
||||||
if (this.showLinkPicker) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.showLinkPicker = true
|
|
||||||
const link = await getLinkWithPicker(null, true)
|
|
||||||
try {
|
|
||||||
const url = new URL(link)
|
|
||||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'Action_InsertLink', { url: link })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('error when parsing the link picker result')
|
|
||||||
}
|
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'Action_Paste', { Mimetype: 'text/plain', Data: link })
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Link picker promise rejected :', e)
|
|
||||||
} finally {
|
|
||||||
this.showLinkPicker = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async resolveLink(url) {
|
|
||||||
try {
|
|
||||||
const result = await axios.get(generateOcsUrl('references/resolve', 2), {
|
|
||||||
params: {
|
|
||||||
reference: url,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const resolvedLink = result.data.ocs.data.references[url]
|
|
||||||
const title = resolvedLink?.openGraphObject?.name
|
|
||||||
const thumbnailUrl = resolvedLink?.openGraphObject?.thumb
|
|
||||||
if (thumbnailUrl) {
|
|
||||||
try {
|
|
||||||
const imageResponse = await axios.get(thumbnailUrl, { responseType: 'blob' })
|
|
||||||
if (imageResponse?.status === 200 && imageResponse?.data) {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.addEventListener('loadend', (e) => {
|
|
||||||
const b64Image = e.target.result
|
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'Action_GetLinkPreview_Resp', { url, title, image: b64Image })
|
|
||||||
})
|
|
||||||
reader.readAsDataURL(imageResponse.data)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading the reference image', e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'Action_GetLinkPreview_Resp', { url, title, image: null })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error resolving a reference', e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close() {
|
close() {
|
||||||
FilesAppIntegration.close()
|
FilesAppIntegration.close()
|
||||||
disableScrollLock()
|
disableScrollLock()
|
||||||
|
|
@ -288,19 +287,14 @@ export default {
|
||||||
reload() {
|
reload() {
|
||||||
this.loading = LOADING_STATE.LOADING
|
this.loading = LOADING_STATE.LOADING
|
||||||
this.load()
|
this.load()
|
||||||
this.$refs.documentFrame.contentWindow.location.replace(this.src)
|
this.$refs.documentFrame.contentWindow.location.replace(this.iframeSrc)
|
||||||
},
|
},
|
||||||
postMessageHandler({ parsed, data }) {
|
postMessageHandler({ parsed }) {
|
||||||
if (data === 'NC_ShowNamePicker') {
|
const { msgId, args, deprecated } = parsed
|
||||||
this.documentReady()
|
console.debug('[viewer] Received post message', msgId, args, deprecated)
|
||||||
return
|
if (deprecated) {
|
||||||
} else if (data === 'loading') {
|
|
||||||
this.loading = LOADING_STATE.LOADING
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.debug('[viewer] Received post message', parsed)
|
|
||||||
const { msgId, args, deprecated } = parsed
|
|
||||||
if (deprecated) { return }
|
|
||||||
|
|
||||||
switch (msgId) {
|
switch (msgId) {
|
||||||
case 'App_LoadingStatus':
|
case 'App_LoadingStatus':
|
||||||
|
|
@ -309,8 +303,7 @@ export default {
|
||||||
this.loading = LOADING_STATE.FRAME_READY
|
this.loading = LOADING_STATE.FRAME_READY
|
||||||
this.$emit('update:loaded', true)
|
this.$emit('update:loaded', true)
|
||||||
FilesAppIntegration.initAfterReady()
|
FilesAppIntegration.initAfterReady()
|
||||||
}
|
} else if (args.Status === 'Document_Loaded') {
|
||||||
if (args.Status === 'Document_Loaded') {
|
|
||||||
this.documentReady()
|
this.documentReady()
|
||||||
} else if (args.Status === 'Failed') {
|
} else if (args.Status === 'Failed') {
|
||||||
this.loading = LOADING_STATE.FAILED
|
this.loading = LOADING_STATE.FAILED
|
||||||
|
|
@ -325,14 +318,16 @@ export default {
|
||||||
this.loading = LOADING_STATE.FAILED
|
this.loading = LOADING_STATE.FAILED
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'loading':
|
case 'UI_Close':
|
||||||
break
|
|
||||||
case 'close':
|
|
||||||
this.close()
|
this.close()
|
||||||
break
|
break
|
||||||
case 'Get_Views_Resp':
|
case 'Get_Views_Resp':
|
||||||
case 'Views_List':
|
case 'Views_List':
|
||||||
this.views = args
|
this.views = args
|
||||||
|
this.unlockAndOpenLocally()
|
||||||
|
break
|
||||||
|
case 'UI_SaveAs':
|
||||||
|
this.saveAs(args.format)
|
||||||
break
|
break
|
||||||
case 'Action_Save_Resp':
|
case 'Action_Save_Resp':
|
||||||
if (args.fileName !== this.filename) {
|
if (args.fileName !== this.filename) {
|
||||||
|
|
@ -341,9 +336,15 @@ export default {
|
||||||
break
|
break
|
||||||
case 'UI_InsertGraphic':
|
case 'UI_InsertGraphic':
|
||||||
FilesAppIntegration.insertGraphic((filename, url) => {
|
FilesAppIntegration.insertGraphic((filename, url) => {
|
||||||
PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'postAsset', { FileName: filename, Url: url })
|
this.postMessage.sendWOPIPostMessage(FRAME_DOCUMENT, 'Action_InsertGraphic', {
|
||||||
|
filename,
|
||||||
|
url,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case 'UI_Mention':
|
||||||
|
this.uiMention(parsed.args.text)
|
||||||
|
break
|
||||||
case 'UI_CreateFile':
|
case 'UI_CreateFile':
|
||||||
FilesAppIntegration.createNewFile(args.DocumentType)
|
FilesAppIntegration.createNewFile(args.DocumentType)
|
||||||
break
|
break
|
||||||
|
|
@ -351,12 +352,11 @@ export default {
|
||||||
FilesAppIntegration.rename(args.NewName)
|
FilesAppIntegration.rename(args.NewName)
|
||||||
break
|
break
|
||||||
case 'UI_FileVersions':
|
case 'UI_FileVersions':
|
||||||
case 'rev-history':
|
|
||||||
FilesAppIntegration.showRevHistory()
|
FilesAppIntegration.showRevHistory()
|
||||||
break
|
break
|
||||||
case 'App_VersionRestore':
|
case 'App_VersionRestore':
|
||||||
if (args.Status === 'Pre_Restore_Ack') {
|
if (args.Status === 'Pre_Restore_Ack') {
|
||||||
FilesAppIntegration.restoreVersionExecute()
|
this.handlePreRestoreAck()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'UI_Share':
|
case 'UI_Share':
|
||||||
|
|
@ -371,28 +371,31 @@ export default {
|
||||||
case 'Action_GetLinkPreview':
|
case 'Action_GetLinkPreview':
|
||||||
this.resolveLink(args.url)
|
this.resolveLink(args.url)
|
||||||
break
|
break
|
||||||
|
case 'Clicked_Button':
|
||||||
|
this.buttonClicked(args)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async buttonClicked(args) {
|
||||||
|
if (args?.Id === 'Open_Local_Editor') {
|
||||||
|
this.showOpenLocalConfirmation()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.office-viewer {
|
.office-viewer {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--color-main-background);
|
background-color: var(--color-main-background);
|
||||||
transition: opacity .25s;
|
|
||||||
|
|
||||||
&__loading-overlay {
|
&__loading-overlay:not(.viewer__file--hidden) {
|
||||||
border-top: 3px solid var(--color-primary-element);
|
border-top: 3px solid var(--color-primary-element);
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -414,48 +417,16 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
|
||||||
position: absolute;
|
|
||||||
right: 44px;
|
|
||||||
top: 3px;
|
|
||||||
z-index: 99999;
|
|
||||||
display: flex;
|
|
||||||
background-color: #fff;
|
|
||||||
|
|
||||||
.avatars {
|
|
||||||
display: flex;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
::v-deep .avatardiv {
|
|
||||||
margin-left: -15px;
|
|
||||||
box-shadow: 0 0 3px var(--color-box-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon-menu-sidebar {
|
|
||||||
background-image: var(--icon-menu-sidebar-000) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__iframe {
|
&__iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .fade-enter-active,
|
|
||||||
::v-deep .fade-leave-active {
|
|
||||||
transition: opacity .25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .fade-enter,
|
|
||||||
::v-deep .fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.viewer .office-viewer {
|
.viewer .office-viewer:not(.viewer__file--hidden) {
|
||||||
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
top: -50px;
|
top: -50px;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ if (OCA.Viewer) {
|
||||||
mimes: supportedMimes,
|
mimes: supportedMimes,
|
||||||
component: Office,
|
component: Office,
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
|
canCompare: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue