diff --git a/.travis.yml b/.travis.yml index 03c675f29..ccabfd5c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,6 +104,7 @@ branches: before_install: - sudo apt-get -qq update - sudo apt-get install -y libxml2-utils + - composer install - wget https://raw.githubusercontent.com/$SERVER/before_install.sh - . ./before_install.sh "$APP_NAME" "$SERVER_BRANCH" "$DB" - cd ../core || cd ../server diff --git a/appinfo/app.php b/appinfo/app.php index 4a06b1faa..4ea25942c 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -13,6 +13,10 @@ namespace OCA\Bookmarks\AppInfo; +if ((@include_once __DIR__ . '/../vendor/autoload.php')===false) { + throw new \Exception('Cannot include autoload. Did you run install dependencies using composer?'); +} + $navigationEntry = function () { return [ 'id' => 'bookmarks', diff --git a/appinfo/application.php b/appinfo/application.php index 1253eb8f1..5bc3d91a1 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -14,7 +14,9 @@ namespace OCA\Bookmarks\AppInfo; use OCA\Bookmarks\Controller\Lib\Bookmarks; +use OCA\Bookmarks\Controller\Lib\ImageService; use \OCP\AppFramework\App; +use OCP\AppFramework\Utility\ITimeFactory; use \OCP\IContainer; use \OCA\Bookmarks\Controller\WebViewController; use OCA\Bookmarks\Controller\Rest\TagsController; @@ -70,6 +72,7 @@ public function __construct(array $urlParams = array()) { /** @var IContainer $c */ $user = $c->query('ServerContainer')->getUserSession()->getUser(); $uid = is_null($user) ? null : $user->getUID(); + return new InternalBookmarkController( $c->query('AppName'), $c->query('Request'), @@ -77,7 +80,9 @@ public function __construct(array $urlParams = array()) { $c->query('ServerContainer')->getDatabaseConnection(), $c->query('ServerContainer')->getL10NFactory()->get('bookmarks'), $c->query('ServerContainer')->query(Bookmarks::class), - $c->query('ServerContainer')->getUserManager() + $c->query('ServerContainer')->getUserManager(), + $c->query('ServerContainer')->query(ImageService::class), + $c->query('ServerContainer')->query(ITimeFactory::class) ); }); @@ -117,6 +122,7 @@ public function __construct(array $urlParams = array()) { $c->query('ServerContainer')->getUserManager() ); }); + } } diff --git a/appinfo/database.xml b/appinfo/database.xml index 004cd33e3..860be21d8 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -73,6 +73,13 @@ true 4 + + image + text + + false + 4096 + diff --git a/appinfo/routes.php b/appinfo/routes.php index 985c28dce..0825cefbc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -33,9 +33,13 @@ array('name' => 'internal_bookmark#edit_bookmark', 'url' => '/bookmark/{id}', 'verb' => 'PUT'), array('name' => 'internal_bookmark#get_single_bookmark', 'url' => '/bookmark/{id}', 'verb' => 'GET'), array('name' => 'internal_bookmark#delete_bookmark', 'url' => '/bookmark/{id}', 'verb' => 'DELETE'), + array('name' => 'internal_bookmark#get_bookmark_image', 'url' => '/bookmark/{id}/image', 'verb' => 'GET'), array('name' => 'internal_tags#full_tags', 'url' => '/tag', 'verb' => 'GET'), array('name' => 'internal_tags#rename_tag', 'url' => '/tag', 'verb' => 'POST'), array('name' => 'internal_tags#delete_tag', 'url' => '/tag', 'verb' => 'DELETE'), + array('name' => 'internal_tags#rename_tag', 'url' => '/tag/{old_name}', 'verb' => 'POST'), + array('name' => 'internal_tags#rename_tag', 'url' => '/tag/{old_name}', 'verb' => 'PUT'), + array('name' => 'internal_tags#delete_tag', 'url' => '/tag/{old_name}', 'verb' => 'DELETE'), // Public REST API array('name' => 'bookmark#get_bookmarks', 'url' => '/public/rest/v2/bookmark', 'verb' => 'GET'), array('name' => 'bookmark#new_bookmark', 'url' => '/public/rest/v2/bookmark', 'verb' => 'POST'), diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..aa7fb8f26 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "marcelklehr/link-preview": "^2" + } +} diff --git a/controller/lib/bookmarks.php b/controller/lib/bookmarks.php index 4c4776bf1..f8d5d5e05 100644 --- a/controller/lib/bookmarks.php +++ b/controller/lib/bookmarks.php @@ -24,10 +24,7 @@ namespace OCA\Bookmarks\Controller\Lib; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\ClientException; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; @@ -45,9 +42,9 @@ class Bookmarks { /** @var IL10N */ private $l; - - /** @var IClientService */ - private $httpClientService; + + /** @var LinkExplorer */ + private $linkExplorer; /** @var EventDispatcherInterface */ private $eventDispatcher; @@ -59,14 +56,14 @@ public function __construct( IDBConnection $db, IConfig $config, IL10N $l, - IClientService $httpClientService, + LinkExplorer $linkExplorer, EventDispatcherInterface $eventDispatcher, ILogger $logger ) { $this->db = $db; $this->config = $config; $this->l = $l; - $this->httpClientService = $httpClientService; + $this->linkExplorer = $linkExplorer; $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; } @@ -177,7 +174,7 @@ public function findBookmarks( } $tableAttributes = array('id', 'url', 'title', 'user_id', 'description', - 'public', 'added', 'lastmodified', 'clickcount',); + 'public', 'added', 'lastmodified', 'clickcount', 'image'); $returnTags = true; @@ -471,14 +468,54 @@ public function editBookmark($userid, $id, $url, $title, $tags = [], $descriptio * @param array $tags Simple array of tags to qualify the bookmark (different tags are taken from values) * @param string $description A longer description about the bookmark * @param boolean $isPublic True if the bookmark is publishable to not registered users + * @param string $image URL to a visual representation of the bookmarked site * @return int The id of the bookmark created */ - public function addBookmark($userid, $url, $title, $tags = array(), $description = '', $isPublic = false) { + public function addBookmark($userid, $url, $title, $tags = array(), $description = '', $isPublic = false, $image = null) { $public = $isPublic ? 1 : 0; - $urlWithoutPrefix = trim(substr($url, strpos($url, "://") + 3)); // Removes everything from the url before the "://" pattern (included) - if($urlWithoutPrefix === '') { - throw new \InvalidArgumentException('Bookmark URL is missing'); + + // do some meta tag inspection of the link... + + // allow only http(s) and (s)ftp + $protocols = '/^(https?|s?ftp)\:\/\//i'; + try { + if (preg_match($protocols, $url)) { + $data = $this->getURLMetadata($url); + } else { + // if no allowed protocol is given, evaluate https and https + foreach(['https://', 'http://'] as $protocol) { + $testUrl = $protocol . $url; + $data = $this->getURLMetadata($testUrl); + if(isset($data['title'])) { + break; + } + } + } + } catch (\Exception $e) { + // only because the server cannot reach a certain URL it does not + // mean the user's browser cannot. + \OC::$server->getLogger()->logException($e, ['app' => 'bookmarks']); + } + if (isset($data['url'])) { + $url = $data['url']; + } + if ((!isset($title) || trim($title) === '')) { + $title = isset($data['title'])? $data['title'] : $url; + } + if (isset($data['description']) && (!isset($description) || trim($description) === '')) { + $description = $data['description']; } + if (isset($data['image']) && !isset($image)) { + $image = $data['image']; + } + + // Check if it is a valid URL (after adding http(s) prefix) + $urlData = parse_url($url); + if(!$this->isProperURL($urlData)) { + throw new \InvalidArgumentException('Invalid URL supplied'); + } + + $urlWithoutPrefix = trim(substr($url, strpos($url, "://") + 3)); // Removes everything from the url before the "://" pattern (included) $decodedUrlNoPrefix = htmlspecialchars_decode($urlWithoutPrefix); $decodedUrl = htmlspecialchars_decode($url); @@ -512,6 +549,10 @@ public function addBookmark($userid, $url, $title, $tags = array(), $description if (trim($description) != '') { // Do we replace the old description $qb->set('description', $qb->createParameter('description')); } + + if (isset($image)) { // Do we replace the old description + $qb->set('image', $qb->createParameter('image')); + } $qb ->where($qb->expr()->like('url', $qb->createParameter('compareUrl'))) // Find url in the db independantly from its protocol @@ -522,6 +563,7 @@ public function addBookmark($userid, $url, $title, $tags = array(), $description 'compareUrl' => '%' . $this->db->escapeLikeParameter($decodedUrlNoPrefix), 'title' => $title, 'description' => $description, + 'image' => $image, ]); $qb->execute(); @@ -542,7 +584,8 @@ public function addBookmark($userid, $url, $title, $tags = array(), $description 'public' => $qb->createParameter('public'), 'added' => $qb->createFunction('UNIX_TIMESTAMP()'), 'lastmodified' => $qb->createFunction('UNIX_TIMESTAMP()'), - 'description' => $qb->createParameter('description') + 'description' => $qb->createParameter('description'), + 'image' => $qb->createParameter('image'), )) ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id'))); $qb->setParameters(array( @@ -550,7 +593,8 @@ public function addBookmark($userid, $url, $title, $tags = array(), $description 'url' => $decodedUrl, 'title' => htmlspecialchars_decode($title), // XXX: Should the title update above also decode it first? 'public' => $public, - 'description' => $description + 'description' => $description, + 'image' => $image, )); $qb->execute(); @@ -613,7 +657,6 @@ private function addTags($bookmarkID, $tags) { * @return null * */ public function importFile($user, $file) { - libxml_use_internal_errors(true); $dom = new \domDocument(); $dom->loadHTMLFile($file, \LIBXML_PARSEHUGE); @@ -658,75 +701,12 @@ public function importFile($user, $file) { /** * @brief Load Url and receive Metadata (Title) * @param string $url Url to load and analyze - * @param bool $tryHarder modifies cURL options for another atttempt if the - * first request did not succeed (e.g. cURL error 18) * @return array Metadata for url; * @throws \Exception|ClientException */ - public function getURLMetadata($url, $tryHarder = false) { - $metadata = ['url' => $url]; - $page = $contentType = ''; - - try { - $client = $this->httpClientService->newClient(); - $options = []; - if($tryHarder) { - $curlOptions = [ 'curl' => - [ CURLOPT_HTTPHEADER => ['Expect:'] ] - ]; - if(version_compare(ClientInterface::VERSION, '6') === -1) { - $options = ['config' => $curlOptions]; - } else { - $options = $curlOptions; - } - } - $request = $client->get($url, $options); - $page = $request->getBody(); - $contentType = $request->getHeader('Content-Type'); - } catch (ClientException $e) { - $errorCode = $e->getCode(); - if (!($errorCode >= 401 && $errorCode <= 403)) { - // whitelist Unauthorized, Forbidden and Paid pages - throw $e; - } - } catch (\GuzzleHttp\Exception\RequestException $e) { - if($tryHarder) { - throw $e; - } - return $this->getURLMetadata($url, true); - } catch (\Exception $e) { - throw $e; - } - - //Check for encoding of site. - //If not UTF-8 convert it. - $encoding = array(); - preg_match('#.+?/.+?;\\s?charset\\s?=\\s?(.+)#i', $contentType, $encoding); - if(empty($encoding)) { - preg_match('/charset="?(.*?)["|;]/i', $page, $encoding); - } - - if (isset($encoding[1])) { - $decodeFrom = strtoupper($encoding[1]); - } else { - $decodeFrom = 'UTF-8'; - } - - if ($page) { - - if ($decodeFrom != 'UTF-8') { - $page = iconv($decodeFrom, "UTF-8", $page); - } - - preg_match("/(.*)<\/title>/si", $page, $match); - - if (isset($match[1])) { - $metadata['title'] = html_entity_decode($match[1]); - } - } - - return $metadata; - } + public function getURLMetadata($url) { + return $this->linkExplorer->get($url); + } /** * @brief Separate Url String at comma character @@ -743,4 +723,16 @@ public function analyzeTagRequest($line) { return $filterTag; } + /** + * Checks whether parse_url was able to return proper URL data + * + * @param bool|array $urlData result of parse_url + * @return bool + */ + public function isProperURL($urlData) { + if ($urlData === false || !isset($urlData['scheme']) || !isset($urlData['host'])) { + return false; + } + return true; + } } diff --git a/controller/lib/imageservice.php b/controller/lib/imageservice.php new file mode 100644 index 000000000..98b309903 --- /dev/null +++ b/controller/lib/imageservice.php @@ -0,0 +1,112 @@ +<?php +/** + * @author Marcel Klehr + * @copyright 2016 Marcel Klehr mklehr@gmx.net + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Bookmarks\Controller\Lib; + +use OCP\ICache; +use OCP\ICacheFactory; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\ClientException; + +class ImageService { + + // Cache for one month + const CACHE_TTL = 4 * 7 * 24 * 60 * 60; + + /** @var ICache */ + private $cache; + + /** + * @param ICacheFactory $cacheFactory + */ + public function __construct(ICacheFactory $cacheFactory) { + $this->cache = $cacheFactory->create('bookmarks.images'); + } + + private function buildKey($url) { + return base64_encode($url); + } + + /** + * @param string $url + * @return string|null image data + */ + public function getImage($url) { + $key = $this->buildKey($url); + // Try cache first + if ($image = $this->cache->get($key)) { + $image = json_decode($image, true); + return [ + 'contentType' => $image['contentType'], + 'data' => base64_decode($image['data']) + ]; + } + + // Fetch image from remote server + $image = $this->fetchImage($url); + + if (is_null($image)) { + return null; + } + + // Store in cache for next time + $json = json_encode([ + 'contentType' => $image['contentType'], + 'data' => base64_encode($image['data']) + ]); + $this->cache->set($key, $json, self::CACHE_TTL); + + return $image; + } + + /** + * @param string $url + * @return string|null fetched image data + */ + private function fetchImage($url) { + $body = $contentType = ''; + try { + $client = new \GuzzleHTTP\Client(); + $request = $client->get($url); + $body = $request->getBody(); + $contentType = $request->getHeader('Content-Type'); + } catch (\GuzzleHttp\Exception\RequestException $e) { + return null; + } catch (\Exception $e) { + throw $e; + } + + // Some HTPP Error occured :/ + if (200 != $request->getStatusCode()) { + return null; + } + + // It's not actually an image, doh. + if (!$contentType || stripos($contentType, 'image') !== 0) { + return null; + } + + return [ + 'contentType' => $contentType, + 'data' => $body + ]; + } +} + diff --git a/controller/lib/linkexplorer.php b/controller/lib/linkexplorer.php new file mode 100644 index 000000000..0d1470ac1 --- /dev/null +++ b/controller/lib/linkexplorer.php @@ -0,0 +1,39 @@ +<?php +namespace OCA\Bookmarks\Controller\Lib; + +use Marcelklehr\LinkPreview\Client as LinkPreview; +use Marcelklehr\LinkPreview\Exceptions\ConnectionErrorException; + +class LinkExplorer { + + /** + * @brief Load Url and receive Metadata (Title) + * @param string $url Url to load and analyze + * @return array Metadata for url; + */ + public function get($url) { + $data = ['url' => $url]; + + // Use LinkPreview to get the meta data + $previewClient = new LinkPreview($url); + $previewClient->getParser('general')->setMinimumImageDimension(0,0); + try { + libxml_use_internal_errors(false); + $preview = $previewClient->getPreview('general'); + } catch (\Marcelklehr\LinkPreview\Exceptions\ConnectionErrorException $e) { + \OCP\Util::writeLog('bookmarks', $e, \OCP\Util::WARN); + return $data; + } + + $data = $preview->toArray(); + if (!isset($data)) { + return ['url' => $url]; + } + + $data['url'] = (string) $previewClient->getUrl(); + $data['image'] = $data['cover']; + + return $data; + } + +} diff --git a/controller/rest/bookmarkcontroller.php b/controller/rest/bookmarkcontroller.php index d7959ac3e..e43e515e4 100644 --- a/controller/rest/bookmarkcontroller.php +++ b/controller/rest/bookmarkcontroller.php @@ -95,6 +95,7 @@ public function getSingleBookmark($id, $user = null) { * @param string conjunction * @param string sortby * @param array search + * @param int limit * @return JSONResponse * * @NoAdminRequired @@ -110,7 +111,8 @@ public function getBookmarks( $tags = null, $conjunction = "or", $sortby = "", - $search = array() + $search = array(), + $limit = 10 ) { if ($user === null) { $user = $this->userId; @@ -146,8 +148,7 @@ public function getBookmarks( $tagsOnly = false; } - $limit = 10; - $offset = $page * 10; + $offset = $page * $limit; if ($page == -1) { $limit = -1; $offset = 0; @@ -162,7 +163,7 @@ public function getBookmarks( } $attributesToSelect = array('url', 'title', 'id', 'user_id', 'description', 'public', - 'added', 'lastmodified', 'clickcount', 'tags'); + 'added', 'lastmodified', 'clickcount', 'tags', 'image'); $bookmarks = $this->bookmarks->findBookmarks($user, $offset, $sqlSortColumn, $filterTag, $tagsOnly, $limit, $publicOnly, $attributesToSelect, $conjunction); @@ -183,44 +184,16 @@ public function getBookmarks( */ public function newBookmark($url = "", $item = array(), $title = "", $is_public = false, $description = "") { $title = trim($title); - if ($title === '') { - $title = $url; - // allow only http(s) and (s)ftp - $protocols = '/^(https?|s?ftp)\:\/\//i'; - try { - if (preg_match($protocols, $url)) { - $data = $this->bookmarks->getURLMetadata($url); - $title = isset($data['title']) ? $data['title'] : $title; - } else { - // if no allowed protocol is given, evaluate https and https - foreach(['https://', 'http://'] as $protocol) { - $testUrl = $protocol . $url; - $data = $this->bookmarks->getURLMetadata($testUrl); - if(isset($data['title'])) { - $title = $data['title']; - $url = $testUrl; - break; - } - } - } - } catch (\Exception $e) { - // only because the server cannot reach a certain URL it does not - // mean the user's browser cannot. - \OC::$server->getLogger()->logException($e, ['app' => 'bookmarks']); - } - } - - // Check if it is a valid URL (after adding http(s) prefix) - $urlData = parse_url($url); - if(!$this->isProperURL($urlData)) { - return new JSONResponse(array('status' => 'error'), Http::STATUS_BAD_REQUEST); - } - + $image = null; $tags = isset($item['tags']) ? $item['tags'] : array(); - $id = $this->bookmarks->addBookmark($this->userId, $url, $title, $tags, $description, $is_public); - $bm = $this->bookmarks->findUniqueBookmark($id, $this->userId); - return new JSONResponse(array('item' => $bm, 'status' => 'success')); + try { + $id = $this->bookmarks->addBookmark($this->userId, $url, $title, $tags, $description, $is_public, $image); + } catch(\InvalidArgumentException $e) { + return new JSONResponse(array('status' => 'error', 'data' => [$e->getMessage()]), Http::STATUS_BAD_REQUEST); + } + $bm = $this->bookmarks->findUniqueBookmark($id, $this->userId); + return new JSONResponse(array('item' => $bm, 'status' => 'success')); } /** @@ -251,15 +224,16 @@ public function legacyEditBookmark($id = null, $url = "", $item = array(), $titl * @param array $item * @param string $title * @param bool $is_public Description - * @param null $record_id + * @param int $record_id * @param string $description + * @param array $tags * @return JSONResponse * * @NoAdminRequired * @NoCSRFRequired * @CORS */ - public function editBookmark($id = null, $url = null, $item = null, $title = null, $is_public = null, $record_id = null, $description = null) { + public function editBookmark($id = null, $url = null, $item = null, $title = null, $is_public = null, $record_id = null, $description = null, $tags = null) { if ($record_id !== null) { $id = $record_id; @@ -274,6 +248,10 @@ public function editBookmark($id = null, $url = null, $item = null, $title = nul ]; if (is_array($item) && isset($item['tags']) && is_array($item['tags'])) { $newProps['tags'] = $item['tags']; + }elseif (is_array($tags)) { + $newProps['tags'] = $tags; + }else{ + $newProps['tags'] = []; } foreach ($newProps as $prop => $value) { if(!is_null($value)) { @@ -283,14 +261,10 @@ public function editBookmark($id = null, $url = null, $item = null, $title = nul // Check if url and id are valid $urlData = parse_url($bookmark['url']); - if(!$this->isProperURL($urlData) || !is_numeric($bookmark['id'])) { + if(!$this->bookmarks->isProperURL($urlData) || !is_numeric($bookmark['id'])) { return new JSONResponse(array(), Http::STATUS_BAD_REQUEST); } - if ($bookmark['tags'] === false) { - $bookmark['tags'] = []; - } - $id = $this->bookmarks->editBookmark($this->userId, $bookmark['id'], $bookmark['url'], $bookmark['title'], $bookmark['tags'], $bookmark['description'], $bookmark['public']); $bm = $this->bookmarks->findUniqueBookmark($id, $this->userId); @@ -337,27 +311,15 @@ public function deleteBookmark($id = -1) { public function clickBookmark($url = "") { $url = urldecode($url); $urlData = parse_url($url); - if(!$this->isProperURL($urlData)) { + if(!$this->bookmarks->isProperURL($urlData)) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } - // prepare url for query - $url = $this->db->escapeLikeParameter(htmlspecialchars_decode($url)); - - $config = \OC::$server->getConfig(); - $escape = ''; - if( - strpos($config->getSystemValue('dbtype'),'sqlite') !== false - && version_compare($config->getSystemValue('version'), 12, '<') - ) { - // sqlite requires the ESCAPE declaration, which is added per default as of Nc 12 - $escape = ' ESCAPE \'\\\''; - } $qb = $this->db->getQueryBuilder(); $qb->update('bookmarks') ->set('clickcount', $qb->createFunction('`clickcount` +1')) ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($this->userId))) - ->andWhere($qb->expr()->like('url', $qb->createNamedParameter($url)) . $escape) + ->andWhere($qb->expr()->eq('url', $qb->createNamedParameter(htmlspecialchars_decode($url)))) ->execute(); return new JSONResponse(['status' => 'success'], Http::STATUS_OK); @@ -430,18 +392,4 @@ public function exportBookmark() { return new ExportResponse($file); } - - /** - * Checks whether parse_url was able to return proper URL data - * - * @param bool|array $urlData result of parse_url - * @return bool - */ - protected function isProperURL($urlData) { - if ($urlData === false || !isset($urlData['scheme']) || !isset($urlData['host'])) { - return false; - } - return true; - } - } diff --git a/controller/rest/internalbookmarkcontroller.php b/controller/rest/internalbookmarkcontroller.php index f7da78dbe..00434ca8b 100644 --- a/controller/rest/internalbookmarkcontroller.php +++ b/controller/rest/internalbookmarkcontroller.php @@ -14,18 +14,45 @@ use OCP\IL10N; use \OCP\IRequest; use \OCP\AppFramework\ApiController; +use \OCP\AppFramework\Http\DataDisplayResponse; +use \OCP\AppFramework\Http\NotFoundResponse; use \OCP\AppFramework\Http\JSONResponse; use \OCP\AppFramework\Http; use \OC\User\Manager; use \OCA\Bookmarks\Controller\Lib\Bookmarks; +use \OCA\Bookmarks\Controller\Lib\ImageService; +use DateInterval; +use DateTime; +use OCP\AppFramework\Utility\ITimeFactory; class InternalBookmarkController extends ApiController { + const IMAGES_CACHE_TTL = 7 * 24 * 60 * 60; + private $publicController; + + private $userId; + private $libBookmarks; + private $imageService; + private $timeFactory; - public function __construct($appName, IRequest $request, $userId, IDBConnection $db, IL10N $l10n, Bookmarks $bookmarks, Manager $userManager) { + public function __construct( + $appName, + IRequest $request, + $userId, + IDBConnection $db, + IL10N $l10n, + Bookmarks $bookmarks, + Manager $userManager, + ImageService $imageService, + ITimeFactory $timeFactory + ) { parent::__construct($appName, $request); - $this->publicController = new BookmarkController($appName, $request, $userId, $db, $l10n, $bookmarks, $userManager); + $this->publicController = new BookmarkController($appName, $request, $userId, $db, $l10n, $bookmarks, $userManager); + $this->userId = $userId; + $this->libBookmarks = $bookmarks; + $this->imageService = $imageService; + $this->timeFactory = $timeFactory; } /** @@ -38,6 +65,7 @@ public function __construct($appName, IRequest $request, $userId, IDBConnection * @param string conjunction * @param string sortby * @param array search + * @param int limit * @return JSONResponse * * @NoAdminRequired @@ -51,9 +79,10 @@ public function getBookmarks( $tags = array(), $conjunction = "or", $sortby = "", - $search = array() + $search = array(), + $limit = 10 ) { - return $this->publicController->getBookmarks($type, $tag, $page, $sort, $user, $tags, $conjunction, $sortby, $search); + return $this->publicController->getBookmarks($type, $tag, $page, $sort, $user, $tags, $conjunction, $sortby, $search, $limit); } /** @@ -105,12 +134,13 @@ public function legacyEditBookmark($id = null, $url = "", $item = array(), $titl * @param bool $is_public Description * @param null $record_id * @param string $description + * @param array $tags * @return JSONResponse * * @NoAdminRequired */ - public function editBookmark($id = null, $url = "", $item = array(), $title = "", $is_public = false, $record_id = null, $description = "") { - return $this->publicController->editBookmark($id, $url, $item, $title, $is_public, $record_id, $description); + public function editBookmark($id = null, $url = "", $item = array(), $title = "", $is_public = false, $record_id = null, $description = "", $tags = []) { + return $this->publicController->editBookmark($id, $url, $item, $title, $is_public, $record_id, $description, $tags); } /** @@ -160,4 +190,37 @@ public function importBookmark() { public function exportBookmark() { return $this->publicController->exportBookmark(); } + + /** + * + * @param int $id The id of the bookmark whose image shoudl be returned + * @return \OCP\AppFramework\Http\Reponse + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function getBookmarkImage($id) { + $bookmark = $this->libBookmarks->findUniqueBookmark($id, $this->userId); + if (!isset($bookmark) || !isset($bookmark['image']) || $bookmark['image'] === '') { + return new NotFoundResponse(); + } + + $image = $this->imageService->getImage($bookmark['image']); + if (!isset($image)) { + return new NotFoundResponse(); + } + + $response = new DataDisplayResponse($image['data']); + $response->addHeader('Content-Type', $image['contentType']); + + $response->cacheFor(self::IMAGES_CACHE_TTL); + + $expires = new DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new DateInterval('PT' . self::IMAGES_CACHE_TTL . 'S')); + $response->addHeader('Expires', $expires->format(DateTime::RFC1123)); + $response->addHeader('Pragma', 'cache'); + + return $response; + } } diff --git a/controller/rest/internaltagscontroller.php b/controller/rest/internaltagscontroller.php index 56663b3d8..a0bef37dc 100644 --- a/controller/rest/internaltagscontroller.php +++ b/controller/rest/internaltagscontroller.php @@ -28,19 +28,21 @@ public function deleteTag($old_name = "") { /** * @param string $old_name * @param string $new_name + * @param string $name * @return JSONResponse * * @NoAdminRequired */ - public function renameTag($old_name = "", $new_name = "") { - return $this->publicController->renameTag($old_name, $new_name); + public function renameTag($old_name = "", $new_name = "", $name = '') { + return $this->publicController->renameTag($old_name, $new_name, $name); } /** + * @param bool $count whether to add the count of bookmarks per tag * @NoAdminRequired */ - public function fullTags() { - return $this->publicController->fullTags(); + public function fullTags($count) { + return $this->publicController->fullTags($count); } } diff --git a/controller/rest/tagscontroller.php b/controller/rest/tagscontroller.php index 98b579d10..2a007ad99 100644 --- a/controller/rest/tagscontroller.php +++ b/controller/rest/tagscontroller.php @@ -42,13 +42,17 @@ public function deleteTag($old_name = "") { /** * @param string $old_name * @param string $new_name + * @param string $name * @return JSONResponse * * @NoAdminRequired * @NoCSRFRequired * @CORS */ - public function renameTag($old_name = "", $new_name = "") { + public function renameTag($old_name = "", $new_name = "", $name = '') { + if ($new_name === '') { + $new_name = $name; + } if ($old_name == "" || $new_name == "") { return new JSONResponse(array(), Http::STATUS_BAD_REQUEST); @@ -59,11 +63,12 @@ public function renameTag($old_name = "", $new_name = "") { } /** + * @param bool $count whether to add the count of bookmarks per tag * @NoAdminRequired * @NoCSRFRequired * @CORS */ - public function fullTags() { + public function fullTags($count=FALSE) { header("Cache-Control: no-cache, must-revalidate"); header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); @@ -71,7 +76,11 @@ public function fullTags() { $qtags = $this->bookmarks->findTags($this->userId, array(), 0, 400); $tags = array(); foreach ($qtags as $tag) { - $tags[] = $tag['tag']; + if ($count === TRUE) { + $tags[] = ['name' => $tag['tag'], 'count' => $tag['nbr']]; + } else { + $tags[] = $tag['tag']; + } } return new JSONResponse($tags); diff --git a/controller/webviewcontroller.php b/controller/webviewcontroller.php index 37ca40cea..724c4fd7e 100644 --- a/controller/webviewcontroller.php +++ b/controller/webviewcontroller.php @@ -57,8 +57,7 @@ public function __construct($appName, IRequest $request, $userId, IURLGenerator * @NoCSRFRequired */ public function index() { - $bookmarkleturl = $this->urlgenerator->getAbsoluteURL('index.php/apps/bookmarks/bookmarklet'); - $params = array('user' => $this->userId, 'bookmarkleturl' => $bookmarkleturl); + $params = array('user' => $this->userId); $policy = new ContentSecurityPolicy(); $policy->addAllowedFrameDomain("'self'"); diff --git a/css/3rdparty/jquery.tagit.css b/css/3rdparty/jquery.tagit.css deleted file mode 100644 index ae814986c..000000000 --- a/css/3rdparty/jquery.tagit.css +++ /dev/null @@ -1,111 +0,0 @@ -ul.tagit { - padding: 1px 5px; - overflow: auto; - margin-left: inherit; /* usually we don't want the regular ul margins. */ - margin-right: inherit; - width: 91.5%; -} -ul.tagit li { - display: block; - float: left; - margin: 2px 5px 2px 0; -} -ul.tagit li.tagit-choice { - padding: .2em 18px .2em .5em; - position: relative; - line-height: inherit; -} -ul.tagit li.tagit-new { - padding: .25em 4px .25em 0; -} - -ul.tagit li.tagit-choice a.tagit-label { - cursor: pointer; - text-decoration: none; -} -ul.tagit li.tagit-choice .close { - cursor: pointer; - position: absolute; - right: .1em; - top: 50%; - margin-top: -8px; -} - -/* used for some custom themes that don't need image icons */ -ul.tagit li.tagit-choice .close .text-icon { - display: none; -} - -ul.tagit li.tagit-choice input { - display: block; - float: left; - margin: 2px 5px 2px 0; -} -ul.tagit input[type="text"] { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - border: none; - margin: 0; - padding: 0; - width: inherit; - background-color: inherit; - outline: none; -} - - -/***** ZENDESK ***/ - -/* Optional scoped theme for tag-it which mimics the zendesk widget. */ - - -ul.tagit { - border-style: solid; - border-width: 1px; - border-color: #C6C6C6; - background: inherit; -} -ul.tagit li.tagit-choice { - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-border-radius: 6px; - border: 1px solid #CAD8F3; - - background: none; - background-color: #DEE7F8; - - color: #555; - font-weight: normal; -} -ul.tagit li.tagit-choice a.close { - text-decoration: none; -} -ul.tagit li.tagit-choice .close { - right: .4em; -} -ul.tagit li.tagit-choice .ui-icon { - display: none; -} -ul.tagit li.tagit-choice .close .text-icon { - display: inline; - font-family: arial, sans-serif; - font-size: 16px; - line-height: 16px; - color: #777; -} -ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove { - background-color: #bbcef1; - border-color: #6d95e0; -} -ul.tagit li.tagit-choice a.tagLabel:hover, -ul.tagit li.tagit-choice a.close .text-icon:hover { - color: #222; -} -ul.tagit input[type="text"] { - color: #333333; - background: none; -} - - - diff --git a/css/bookmarklet.css b/css/bookmarklet.css new file mode 100644 index 000000000..2844606f2 --- /dev/null +++ b/css/bookmarklet.css @@ -0,0 +1,28 @@ +#bookmarklet_form { + padding: .2cm; +} +#bookmarklet_form #add_form_loading { + margin: .5cm; + float: right; + visibility: hidden; +} +#bookmarklet_form .addBm { + position: relative; +} +#bookmarklet_form h1 { + clear: both; + margin: .5cm 0; + font-size: .8cm; +} +#bookmarklet_form .bookmark-exists { + font-weight: bold; + border: 1px #ebebeb solid; + border-radius: 5px; + margin: .5cm 0; + padding: .2cm; +} +#bookmarklet_form input, +#bookmarklet_form textarea { + resize: vertical; + width: 100%; +} diff --git a/css/bookmarks.css b/css/bookmarks.css index 7e553dc6b..710f56d60 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,438 +1,517 @@ -input.disabled, input.disabled:hover, input.disabled:focus { - cursor: not-allowed; - background-color: #ddd; - color: gray; +#content > div { + height: 100%; + overflow: auto; } -#settingsbtn { background: transparent; color:#666; - border-bottom: 1px solid #ddd; border-top: 1px solid #fff; } -#settingsbtn:hover { background:#dbdbdb !important; color:#999; - border-bottom: 1px solid #CCCCCC; border-top: 1px solid #D4D4D4; } -#settingsbtn img { width: 18px; height: 18px; margin: 10px; } - -.bookmarks_headline { - font-size: large; - font-weight: bold; - margin-left: 2em; - padding: 2.5ex 0.5ex; +#app-navigation { + height: 100%; + position: relative; } -.bookmarks_menu { - margin-left: 1.5em; - padding: 0.5ex; +#app-navigation > ul { + height: auto; } -.bookmarks_list, #firstrun { - overflow: auto; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 100%; +.navigation a > span { + display: inline-block; + vertical-align: text-bottom; + margin-right: 12px; } -#add_form { - padding-bottom: 2em; +#app-navigation h3 { + padding-top: .7cm; + padding-left: 12px; + border-top: 1px solid #ededed; } - -#add_form_loading { - visibility: hidden; - float: right; - margin: 8px; +#app-navigation h3 [class^=icon-] { + display: inline-block; + vertical-align: sub; } -#add_form input { - margin: 0.3em; +#app-content { + transition: transform .5s ease; + transform: none; } -#add_url { - width: 98%; + +.mobile-nav { + padding: .3cm; + display: none; } -.bookmarks_addBml { - text-decoration: underline; + +.mobile-nav .toggle-menu { + display: inline-block; } -#bookmark_add_submit { - background-color: transparent; - border: none; - position: absolute; - right: 0; - top: 0; - opacity: .5; - padding: 17px 14px; + +@media only screen and (max-width: 768px) { + + .mobile-nav-open #app-content { + transform: translate(250px, 0); + } + + .mobile-nav { + display: block; + } + } -.bookmarks_label { - width: 7em; - display: inline-block; - text-align: right; +.add-bookmark input { + margin: 0; + width: 100%; + padding: 12px 40px 12px 12px; + box-sizing: border-box; + background-color: transparent; + border: none; + border-bottom: 1px solid #eee; + border-radius: 0; } -.bookmarks_input { - width: 8em; +.add-bookmark button { + position: absolute; + right: 0; + top: 0; + min-height: 34px; + padding: 10px 20px; + background-color: transparent; + border: none; + opacity: .5; + font-weight: bold; } -.bookmark_actions { - position: absolute; - right: 1em; - top: 12px; - display: none; +.bulk-actions { + display: none; + position: fixed; + top: 45px; + left: 250px; + right: 0; + box-shadow: 2px 1px 4px 0px #666; + padding: .3cm .5cm; + background: white; + z-index: 3; } -.bookmark_actions span { margin: 0 0.4em; } -.bookmark_actions img { opacity: 0.3;} -.bookmark_actions img:hover { opacity: 1; cursor: pointer; } -.bookmark_edit img {opacity:0.3;} -.bookmark_edit img:hover {cursor: pointer; opacity: 1;} -.bookmark_single { - position: relative; - padding: 0.2em 1em; - min-height: 3em; - border-bottom: 1px solid #DDD; - -webkit-transition:background-color 500ms; -moz-transition:background-color 500ms; -o-transition:background-color 500ms; transition:background-color 500ms; +.bulk-actions + * { + transition: margin-top .5s ease; } -.bookmark_single:hover { - background-color:#f8f8f8; +.bulk-actions.active { + display: block; } -.bookmark_single:hover .bookmark_actions { - display: block; +.bulk-actions.active + * { + margin-top: 2.5cm; } -.bookmark_title { - font-weight: bold; - display: block; - margin-right: 0.8em; - margin-top:0.7em; +@media only screen and (max-width: 768px) { + + .bulk-actions { + position: relative; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + .bulk-actions.active + * { + margin-top: 0 !important; + } } -.bookmark_title a { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - display: inline-block; - max-width: 74%; +.bulk-actions > button { + vertical-align: top; } -.bookmark_single:hover .bookmark_title .bookmark_edit_btn { - display:inline-block; +.bulk-actions .tags { + display: inline-block; + width: 60%; + margin: 3px 3px 3px 0; + position: relative; } -.bm_view_list .bookmark_tags { - position: absolute; - top: 0.5em; - right: 6em; - text-align: right; +.bulk-actions .tags-selection { + display: block; + width: 100%; } -#app-navigation > ul > li { - display: block !important; +.bulk-actions .selection-tools { + float: right; } -.bookmark_tags { - left:0%; - position: relative; - width: 75%; +.bulk-actions .selection-tools .close { + display: inline-block; + cursor: pointer; + margin: 3px 3px 3px 0; + padding: 6px 12px; + width: auto; + min-height: 34px; + cursor: pointer; + box-sizing: border-box; } -.bookmark_tag { - display: inline-block; - color: white; - margin: 0.2em 0.2em; - padding: 0 0.4em; - background-color: #1D2D44; - border-radius: 0.4em; - opacity: 0.2; + +.bulk-actions .selection-tools .close span { + display: inline-block; + vertical-align: text-top; } -.bookmark_tag:hover { opacity: 0.5; } -.loading_meta { - display: none; - margin-left: 5px; +button span, +.bulk-actions .selection-tools .close span { + cursor: pointer; } -.addBm { - background: none repeat scroll 0 0 #F8F8F8; - border-radius: 0.5em 0.5em 0.5em 0.5em; - color: #555555; - margin: 1em; - padding: 0.5em 1em; - text-shadow: 0 1px 0 #FFFFFF; - width: 500px; +@media only screen and (max-width: 925px) { + .bulk-actions .tags { + width: 80%; + } + .bulk-actions .selection-tools { + float: none; + text-align: right; + margin-bottom: .5cm; + } + .bulk-actions.active + * { + margin-top: 3.5cm; + } +} +.bookmarks-empty { + width: 5cm; + margin: 5cm auto; } -.bm_desc { - width:90%; + +.bookmarks { + display: flex; + flex-wrap: wrap; + overflow: hidden; + padding: 0 .6cm .6cm 0; } -.addBm h1 { - font-weight: bold; - border-bottom: 1px solid #BABABA; + +.bookmark-card { + position: relative; + box-sizing: border-box; /*necessary for hover effect goodness to work*/ + flex-grow: 1; + width: 6cm; + height: 6cm; + margin: .6cm 0 0 .6cm; + padding: .5cm; + border: 1px solid #ddd; + overflow: hidden; + background-color: rgb(210, 210, 210); + background-size: cover; + background-position: center; + transition: background-color .3s ease; + background-blend-mode: overlay; } +.bookmark-card, +.bookmark-card * { + cursor: pointer; +} -.addBm fieldset > ul > li { - margin-top: 1em; +.bookmark-card:hover { + background-color: rgb(180, 180, 180); } -.addBm label { - display:block; - width:100%; + +.bookmark-card.active, +.bookmark-card.active:hover { + transform: none; + border: 1px solid #0082c9; + box-shadow: inset 0 0 10px #0082c9; } -.addBm .is_public_label { - display: inline; +.bookmark-card .panel { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: .5cm .5cm .1cm .6cm; + background: rgba(255, 255, 255, .8); + box-shadow: #aaa 0 0 5px; } -.addBm fieldset input[type="text"], .addBm textarea { - width:100%; + +.bookmark-card:hover .panel { + background: rgba(255, 255, 255, .90); } -.addBm textarea{ - min-width:250px; + +.bookmark-card .selectbox { + position: absolute; + left: .1cm; + top: .5cm; } -.addBm .close_btn -{ - height: 18px; - margin: -20px 0 0; - padding: 1px; - float:right; - width: 19px; + +.bookmark-card h1 { + font-size: .5cm; + padding-right: .5cm; } -.addBm .submit { - float: right; + +.bookmark-card h2 { + margin-top: .1cm; + font-size: .4cm; + opacity: .4; } -.addBm ul.tagit { background:white; } +.bookmark-card h2 :link span { + display: inline-block; + vertical-align: text-bottom; + margin-right: 3px; +} -.addBm input.ui-autocomplete-input, #tag_filter input.ui-autocomplete-input, .bookmark_form_tags input.ui-autocomplete-input{ - box-shadow:none; +.bookmark-card h2 :link:hover { + text-decoration: underline; +} +.bookmark-card .tags { + max-height: 2.5cm; + overflow: hidden; + position: absolute; + bottom: .5cm; } -ul.tagit li.tagit-new { - padding-left: 0em; +.bookmark-card .tag-nav-item { + border-radius: .2cm; + background: #ebebeb; + display: inline-block; + padding: 0 .2cm; + margin: .1cm; } -#tag_select_label{ - display: block; - text-align: center; - margin-top: 10px; - cursor: default; +.bookmark-card .tag-nav-item:hover { + background: #e0e0e0; } -li.tag_list { - padding-left: 3px; - padding-right: 3px; - overflow: hidden; +.bookmark-detail { + position: fixed; + top: 45px; + right: 0; + z-index: 2; + width: 500px; + height: 100%; + box-sizing: border-box; + padding: 1cm; + background: white; + border-left: 1px solid #ebebeb; + color: #333; } -li.tag_list a { - display: inline !important; - padding: 5px 11px 5px 11px !important; - border: 1px solid transparent; +@media only screen and (max-width: 768px) { + + .bookmark-detail { + width: auto; + left: 0; + } + } -li.tag_list li:hover > a { - background: none repeat scroll 0 0 #dee7f8 !important; - border: 1px solid #cad8f3; - border-radius: 6px; +.bookmark-detail h1 { + font-size: .7cm; + line-height: 1.3; + padding-right: 2.3cm; + cursor: text; } -.tag_list em , .share_list em{ - float:right; - display:block; +.bookmark-detail h2 { + margin-top: .3cm; + opacity: .4; +} +.bookmark-detail h2 .edit { + float: right; +} +.bookmark-detail h3 { + margin-top: 1cm; + margin-bottom: .3cm; + border-bottom: 1px solid #ebebeb; } -.tags_actions { - margin-left: 0.5em; - display: inline; - line-height: 44px; +.bookmark-detail h2 :link:hover { + text-decoration: underline; } -.tags_actions > span { - display:none; - float:right; +.bookmark-detail h2 :link span { + display: inline-block; + vertical-align: middle; + margin-right: 3px; } -li:hover > .tags_actions > span { - display:inline; + +.bookmark-detail > .close { + position: absolute; + top: .1cm; + right: .1cm; + width: 1cm; + height: 1cm; + cursor: pointer; } -li:hover > .tags_actions > em { display : none; } -#tag_filter li, #tag_filter li:hover { - background: none repeat scroll 0 0 #F8F8F8; +.bookmark-detail .status { + position: absolute; + top: .4cm; + left: 1cm; + font-size: .4cm; + opacity: .4; } -#tag_filter .tagit { - display: inline-block; - margin: 0.3em; +.bookmark-detail .status [class^="icon-"] { + display: inline-block; } -#tag_filter ul.tagit > li.tagit-new { - padding:0; +.bookmark-detail .status .message { + display: none; } -#tag_filter ul.tagit li.tagit-choice { - background: none repeat scroll 0 0 #DEE7F8; - padding: 0.2em 18px 0.2em 0.5em; - width: auto; +.bookmark-detail .status.saving .message.message-saving { + display: inline; + position: relative; + left: .2cm; } - -#tag_filter a { - display: inline; - float: none; - margin: 0; - padding: 0; +.bookmark-detail .status.saved .message.message-saved { + display: inline; + animation: fadeout ease 5s both 1; } - -#tag_filter ul li:hover > a { - background-color: transparent; +@keyframes fadeout { + 0% { + opacity: 1; + display: inline; + } + 99% { + opacity: 0; + display: inline; + } + 100% { + opacity: 0; + display: none; + } } -#tag_filter ul.tagit li.tagit-choice .close{ - margin-top: -8px; - cursor: pointer; - margin-top: -8px; - top: 50%; - padding-left: 0px !important; +.bookmark-detail .actions { + position: absolute; + right: 1cm; } -#tag_filter ul.tagit li.tagit-choice .close span{ - text-align: right; +.bookmark-detail .actions button { + height: 1cm; + width: 1cm; } -ul.tagit li.tagit-choice .close .text-icon { - display: block !important; +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice, +.bookmark-detail .tag-nav-item { + border-radius: .2cm; + border: none; + background: #ebebeb; + display: inline-block; + padding: .1cm .2cm; + margin: .1cm; } -.bookmark_desc{ - padding-left:0.25em; - padding-bottom:0.25em; - white-space:nowrap; - text-overflow:ellipsis; - overflow:hidden; - display:block; - font-weight:500; - /* z-index: -10; */ -} -.bookmark_date, .bookmark_submit { - font-size:small; - position: absolute; - right: 6em; - color:gray; - /* margin-top:2.5em; */ - margin-right: 0.2em; - top:0.8em; +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, +.bookmark-detail .tag-nav-item:hover { + background: #e0e0e0; } -/* -.bm_view_img .bookmark_single:hover .bookmark_url{ - display:block; +.bookmark-detail .description { + margin-top: .5cm; + font-size: .4cm; + /*for editing*/ + width: auto; + min-height: 300px; + cursor: text; } -.bm_view_img .bookmark_single .bookmark_url{ - display:block; -} -*/ -.bookmark_single:hover .bookmark_url { display: inline; } -.bookmark_single:hover .bookmark_url a[href]{ color:gray; } -.bookmark_url { display: none; } -.bookmark_form_title { - float: left; - width:25%; +.bookmark-detail .description.empty { + opacity: .4; } -.bookmark_form_url { - float:left; - width:25%; +.tags-management { + height: 80% !important; + height: calc(100% - 80px - 100px) !important; } -.bookmark_form_tags { - width:30%; - float:left; -} +@media only screen and (max-width: 768px) { + .tags-management { + width: auto; + float: none; + border-bottom: 2px solid #bbb; + } -.bookmark_form_desc { - width:50%; - padding-top:100px; } -.bookmark_form_desc textarea { - margin-top:-3.4em; +.tags-management .tag-man-item { + cursor: pointer; } -.bookmark_form_title input, .bookmark_form_url input { - width: 89%; -} -.bookmark_form_desc textarea { - width: 94.5%; +.tags-management .tag-man-item.active { + background: #f3f3f3; } -.bookmark_single .bookmark_edit_btn { - visibility:hidden; - width: 16px; +.tags-management .tag-man-item > a { + padding-left: .2cm; } -.bookmark_single:hover .bookmark_edit_btn { - visibility: visible; +.tags-management .tag-man-item .actions { + float: right; } -.bookmark_single:focus .bookmark_edit_btn { - visibility: visible; - opacity:1; +.tags-management .tag-man-item .actions ul { + display: block !important; } -.bookmark_single_form { - padding: 20px 8px; - border-bottom: 1px solid #DDD; +.tags-management .tag-man-item .actions .action { + display: inline-block; + width: auto !important; } -.bookmark_single_form .tagit{ - /* width: 80%; */ - box-shadow: 0 1px 1px #FFFFFF, 0 1px 0 #BBBBBB inset; - /* margin: 3px; */ - /* height:6.4em; */ +.tags-management .tag-man-item .action button { + border: none; + background-color: transparent; + opacity: .45; + padding: 3px 12px; } -.bookmark_form_submit { - margin-left: 4px; + +.tags-management .tag-man-item .action button:hover { + opacity: 1; } -.bookmark_form_submit .reset { - float: right; + +.tags-management .tag-man-item input[type="text"] { + min-height: .7cm; + padding: 0 .1cm; + margin: 0; + width: 70%; } -.bm_view_img .bookmark_actions { - bottom: 0.7em; - display: block; - position: absolute; - right: 1em; - top: auto; +/* make checkbox look like an icon */ + +.menu-item-checkbox span:first-child { + padding: 5px 5px; } -#bm_setting_panel { - background-color: #eee; - padding: 1em; - height: 100%; - /* display:none; */ +/* display 'remove from filter' when selected, and 'add to ...' when unselected*/ + +.tags-management .tag-man-item.active .menu-filter-add { + display: none; } -/* .open #bm_setting_panel { display:block; } */ -#bm_setting_panel legend{ - margin-top: 0.5em; +.tags-management .tag-man-item .menu-filter-remove { + display: none; } -#bookmarklet_hint { - margin-bottom: 6px; +.tags-management .tag-man-item.active .menu-filter-remove { + display: block; } -#bm_import { - opacity: 0; - position: absolute; - width: 90px; + +#app-navigation .settings h3 { + padding-top: 0; + padding-left: 0; + border-top: none; } -#bm_import:hover ~ button#bm_import_submit { - background-color: rgba(250, 250, 250, 0.9); - color: #333; +.settings .button { + display: inline-block; } -#import_bookmark { - margin-top: 10px; +.settings .import-form { + display: inline-block; } -#import_bookmark .personalblock { - margin-top: 1em; +.settings .import { + display: none; } -#app-navigation a.bookmarklet { - margin-top: 5px; - padding: 0 5px 2px; + +.settings .upload { + display: none; } diff --git a/css/select2.css b/css/select2.css new file mode 100644 index 000000000..54d8e0e4c --- /dev/null +++ b/css/select2.css @@ -0,0 +1,655 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; +} + +.select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; +} + +.select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.select2-container .select2-selection--single .select2-selection__clear { + position: relative; +} + +.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; +} + +.select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; +} + +.select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.select2-container .select2-search--inline { + float: left; +} + +.select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; +} + +.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; +} + +.select2-results { + display: block; +} + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; +} + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; +} + +.select2-results__option[aria-selected] { + cursor: pointer; +} + +.select2-container--open .select2-dropdown { + left: 0; +} + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-search--dropdown { + display: block; + padding: 4px; +} + +.select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; +} + +.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.select2-search--dropdown.select2-search--hide { + display: none; +} + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); +} + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; +} + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; +} + +.select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; +} + +.select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; +} + +.select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; +} + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; +} + +.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; +} + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; +} + +.select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; +} + +.select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; +} + +.select2-container--default .select2-selection--multiple .select2-selection__placeholder { + color: #999; + margin-top: 5px; + float: left; +} + +.select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; +} + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; +} + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; +} + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, +.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, +.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; +} + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; +} + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; +} + +.select2-container--default .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; +} + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; +} + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; +} + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; +} + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); +} + +.select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; +} + +.select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; +} + +.select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; +} + +.select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; +} + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; +} + +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; +} + +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); +} + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); +} + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; +} + +.select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; +} + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; + margin-left: 5px; + margin-right: auto; +} + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; +} + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; +} + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; +} + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; +} + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; +} + +.select2-container--classic .select2-dropdown--below { + border-top: none; +} + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; +} + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; +} + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; +} + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; +} + +/* Nextcloud styling */ + +.select2-selection__choice__remove, +.select2-selection__clear { + font-size: .5cm; + vertical-align: bottom; +} + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #0082c9; +} +.select2-container--default .select2-selection--multiple { + border-color: #dbdbdb; +} +.select2-container--default .select2-selection--multiple:hover { + border-color: #0082c9; +} diff --git a/js/3rdparty/js_tpl.js b/js/3rdparty/js_tpl.js deleted file mode 100644 index 62d98ba9d..000000000 --- a/js/3rdparty/js_tpl.js +++ /dev/null @@ -1,33 +0,0 @@ - -// Simple JavaScript Templating -// John Resig - http://ejohn.org/ - MIT Licensed -(function () { - var cache = {}; - - this.tmpl = function tmpl(str, data) { - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] || - tmpl(document.getElementById(str).innerHTML) : - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<&").join("\t") - .replace(/((^|&>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)&>/g, "',$1,'") - .split("\t").join("');") - .split("&>").join("p.push('") - .split("\r").join("\\'") - + "');}return p.join('');"); - - // Provide some basic currying to the user - return data ? fn(data) : fn; - }; -})(); diff --git a/js/3rdparty/tag-it.js b/js/3rdparty/tag-it.js deleted file mode 100644 index f16d021f6..000000000 --- a/js/3rdparty/tag-it.js +++ /dev/null @@ -1,365 +0,0 @@ -/* - * jQuery UI Tag-it! - * - * @version v2.0 (06/2011) - * - * Copyright 2011, Levy Carneiro Jr. - * Released under the MIT license. - * http://aehlke.github.com/tag-it/LICENSE - * - * Homepage: - * http://aehlke.github.com/tag-it/ - * - * Authors: - * Levy Carneiro Jr. - * Martin Rehfeld - * Tobias Schmidt - * Skylar Challand - * Alex Ehlke - * - * Maintainer: - * Alex Ehlke - Twitter: @aehlke - * - * Dependencies: - * jQuery v1.4+ - * jQuery UI v1.8+ - */ -(function ($) { - - $.widget('ui.tagit', { - options: { - itemName: 'item', - fieldName: 'tags', - availableTags: [], - tagSource: null, - removeConfirmation: false, - caseSensitive: true, - placeholderText: null, - // When enabled, quotes are not neccesary - // for inputting multi-word tags. - allowSpaces: false, - // The below options are for using a single field instead of several - // for our form values. - // - // When enabled, will use a single hidden field for the form, - // rather than one per tag. It will delimit tags in the field - // with singleFieldDelimiter. - // - // The easiest way to use singleField is to just instantiate tag-it - // on an INPUT element, in which case singleField is automatically - // set to true, and singleFieldNode is set to that element. This - // way, you don't need to fiddle with these options. - singleField: false, - singleFieldDelimiter: ',', - // Set this to an input DOM node to use an existing form field. - // Any text in it will be erased on init. But it will be - // populated with the text of tags as they are created, - // delimited by singleFieldDelimiter. - // - // If this is not set, we create an input node for it, - // with the name given in settings.fieldName, - // ignoring settings.itemName. - singleFieldNode: null, - // Optionally set a tabindex attribute on the input that gets - // created for tag-it. - tabIndex: null, - // Event callbacks. - onTagAdded: null, - onTagRemoved: null, - onTagClicked: null - }, - _create: function () { - // for handling static scoping inside callbacks - var that = this; - - // There are 2 kinds of DOM nodes this widget can be instantiated on: - // 1. UL, OL, or some element containing either of these. - // 2. INPUT, in which case 'singleField' is overridden to true, - // a UL is created and the INPUT is hidden. - if (this.element.is('input')) { - this.tagList = $('<ul></ul>').insertAfter(this.element); - this.options.singleField = true; - this.options.singleFieldNode = this.element; - this.element.css('display', 'none'); - } else { - this.tagList = this.element.find('ul, ol').andSelf().last(); - } - - this._tagInput = $('<input type="text">').addClass('ui-widget-content'); - if (this.options.tabIndex) { - this._tagInput.attr('tabindex', this.options.tabIndex); - } - if (this.options.placeholderText) { - this._tagInput.attr('placeholder', this.options.placeholderText); - } - this.options.tagSource = this.options.tagSource || function (search, showChoices) { - var filter = search.term.toLowerCase(); - var choices = $.grep(that.options.availableTags, function (element) { - // Only match autocomplete options that begin with the search term. - // (Case insensitive.) - return (element.toLowerCase().indexOf(filter) === 0); - }); - showChoices(that._subtractArray(choices, that.assignedTags())); - }; - - this.tagList - .addClass('tagit') - .addClass('ui-widget ui-widget-content ui-corner-all') - // Create the input field. - .append($('<li class="tagit-new"></li>').append(this._tagInput)) - .click(function (e) { - var target = $(e.target); - if (target.hasClass('tagit-label')) { - that._trigger('onTagClicked', e, target.closest('.tagit-choice')); - } else { - // Sets the focus() to the input field, if the user - // clicks anywhere inside the UL. This is needed - // because the input field needs to be of a small size. - that._tagInput.focus(); - } - }); - - // Add existing tags from the list, if any. - this.tagList.children('li').each(function () { - if (!$(this).hasClass('tagit-new')) { - that.createTag($(this).html(), $(this).attr('class')); - $(this).remove(); - } - }); - - // Single field support. - if (this.options.singleField) { - if (this.options.singleFieldNode) { - // Add existing tags from the input field. - var node = $(this.options.singleFieldNode); - var tags = node.val().split(this.options.singleFieldDelimiter); - node.val(''); - $.each(tags, function (index, tag) { - that.createTag(tag); - }); - } else { - // Create our single field input after our list. - this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '">'); - } - } - - // Events. - this._tagInput - .keydown(function (event) { - // Backspace is not detected within a keypress, so it must use keydown. - if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') { - var tag = that._lastTag(); - if (!that.options.removeConfirmation || tag.hasClass('remove')) { - // When backspace is pressed, the last tag is deleted. - that.removeTag(tag); - } else if (that.options.removeConfirmation) { - tag.addClass('remove ui-state-highlight'); - } - } else if (that.options.removeConfirmation) { - that._lastTag().removeClass('remove ui-state-highlight'); - } - - // Comma/Space/Enter are all valid delimiters for new tags, - // except when there is an open quote or if setting allowSpaces = true. - // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught. - if ( - event.which == $.ui.keyCode.COMMA || - event.which == $.ui.keyCode.ENTER || - ( - event.which == $.ui.keyCode.TAB && - that._tagInput.val() !== '' - ) || - ( - event.which == $.ui.keyCode.SPACE && - that.options.allowSpaces !== true && - ( - $.trim(that._tagInput.val()).replace(/^s*/, '').charAt(0) != '"' || - ( - $.trim(that._tagInput.val()).charAt(0) == '"' && - $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' && - $.trim(that._tagInput.val()).length - 1 !== 0 - ) - ) - ) - ) { - event.preventDefault(); - that.createTag(that._cleanedInput()); - - // The autocomplete doesn't close automatically when TAB is pressed. - // So let's ensure that it closes. - that._tagInput.autocomplete('close'); - } - }).blur(function (e) { - //If autocomplete is enabled and suggestion was clicked, don't add it - if (that.options.tagSource && that._tagInput.data('autocomplete-open')) { - that._cleanedInput(); - } else { - that.createTag(that._cleanedInput()); - } - }); - - - // Autocomplete. - if (this.options.availableTags || this.options.tagSource) { - this._tagInput.autocomplete({ - source: this.options.tagSource, - open: function () { - that._tagInput.data('autocomplete-open', true) - }, - close: function () { - that._tagInput.data('autocomplete-open', false) - }, - select: function (event, ui) { - that.createTag(ui.item.value); - // Preventing the tag input to be updated with the chosen value. - return false; - } - }); - } - }, - _cleanedInput: function () { - // Returns the contents of the tag input, cleaned and ready to be passed to createTag - return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1')); - }, - _lastTag: function () { - return this.tagList.children('.tagit-choice:last'); - }, - assignedTags: function () { - // Returns an array of tag string values - var that = this; - var tags = []; - if (this.options.singleField) { - tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); - if (tags[0] === '') { - tags = []; - } - } else { - this.tagList.children('.tagit-choice').each(function () { - tags.push(that.tagLabel(this)); - }); - } - return tags; - }, - _updateSingleTagsField: function (tags) { - // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter - $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)); - }, - _subtractArray: function (a1, a2) { - var result = []; - for (var i = 0; i < a1.length; i++) { - if ($.inArray(a1[i], a2) == -1) { - result.push(a1[i]); - } - } - return result; - }, - tagLabel: function (tag) { - // Returns the tag's string label. - if (this.options.singleField) { - return $(tag).children('.tagit-label').text(); - } else { - return $(tag).children('input').val(); - } - }, - _isNew: function (value) { - var that = this; - var isNew = true; - this.tagList.children('.tagit-choice').each(function (i) { - if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) { - isNew = false; - return; - } - }); - return isNew; - }, - _formatStr: function (str) { - if (this.options.caseSensitive) { - return str; - } - return $.trim(str.toLowerCase()); - }, - createTag: function (value, additionalClass) { - that = this; - // Automatically trims the value of leading and trailing whitespace. - value = $.trim(value); - - if (!this._isNew(value) || value === '') { - return false; - } - - var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value); - - // Create tag. - var tag = $('<li></li>') - .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') - .addClass(additionalClass) - .append(label); - - // Button for removing the tag. - var removeTagIcon = $('<span></span>') - .addClass('ui-icon ui-icon-close'); - var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X - .addClass('close') - .append(removeTagIcon) - .click(function (e) { - // Removes a tag when the little 'x' is clicked. - that.removeTag(tag); - }); - tag.append(removeTag); - - // Unless options.singleField is set, each tag has a hidden input field inline. - if (this.options.singleField) { - var tags = this.assignedTags(); - tags.push(value); - this._updateSingleTagsField(tags); - } else { - var escapedValue = label.html(); - tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]">'); - } - - this._trigger('onTagAdded', null, tag); - - // Cleaning the input. - this._tagInput.val(''); - - // insert tag - this._tagInput.parent().after(tag); - }, - removeTag: function (tag, animate) { - if (typeof animate === 'undefined') { - animate = true; - } - - tag = $(tag); - - this._trigger('onTagRemoved', null, tag); - - if (this.options.singleField) { - var tags = this.assignedTags(); - var removedTagLabel = this.tagLabel(tag); - tags = $.grep(tags, function (el) { - return el != removedTagLabel; - }); - this._updateSingleTagsField(tags); - } - // Animate the removal. - if (animate) { - tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function () { - tag.remove(); - }).dequeue(); - } else { - tag.remove(); - } - this._trigger('onTagFinishRemoved', null, tag); - }, - removeAll: function () { - // Removes all tags. Takes an optional `animate` argument. - var that = this; - this.tagList.children('.tagit-choice').each(function (index, tag) { - that.removeTag(tag, false); - }); - } - - }); - -})(jQuery); diff --git a/js/apps/Bookmarklet.js b/js/apps/Bookmarklet.js new file mode 100644 index 000000000..969e741fc --- /dev/null +++ b/js/apps/Bookmarklet.js @@ -0,0 +1,19 @@ +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import BookmarkletView from '../views/Bookmarklet'; + +const Marionette = Backbone.Marionette; + +export default Marionette.Application.extend({ + region: '#bookmarklet_form', + onBeforeStart: function() { + var that = this; + this.tags = new Tags; + this.tags.fetch({ + data: {count: true}, + }); + }, + onStart: function() { + this.showView(new BookmarkletView({app: this})); + }, +}); diff --git a/js/apps/Main.js b/js/apps/Main.js new file mode 100644 index 000000000..2abbf0745 --- /dev/null +++ b/js/apps/Main.js @@ -0,0 +1,56 @@ +import Backbone from 'backbone'; +import Bookmarks from '../models/Bookmarks'; +import Tag from '../models/Tag'; +import Tags from '../models/Tags'; +import Router from './MainRouter'; +import AppView from '../views/App'; + +const Marionette = Backbone.Marionette; + +export default Marionette.Application.extend({ + region: '#content', + onBeforeStart: function() { + var that = this; + this.bookmarks = new Bookmarks; + this.tags = new Tags; + this.tags.fetch({ + data: {count: true}, + success: function() { + // we sadly cannot listen ot 'sync', which would fire after fetching, so we have to listen to these and add some timeout + that.listenTo(that.tags, 'sync add remove', that.onTagChanged); + } + }); + this.listenTo(this.bookmarks, 'sync', this.onBookmarkTagsChanged); + + this.router = new Router({app: this}); + }, + onStart: function() { + this.showView(new AppView({app: this})); + Backbone.history.start(); + }, + onTagChanged: function(tag) { + var that = this; + if (!(tag instanceof Tag)) return; // we can also receive 'sync' events from the collection, which we don't want here + if (this.bookmarkChanged) return this.bookmarkChanged = false; // set to true by onBookmarkTagsChanged + this.tagChanged = true; + + // we need to wait 'till the tag change has been acknowledged by the server + setTimeout(function() { + that.bookmarks + .filter(function(bm) { + return bm.get('tags').some(function(t) { + return t === tag.get('name') || t === tag.previous('name'); + }); + }) + .forEach(function(bm) { + bm.fetch(); + }); + }, 100); + }, + onBookmarkTagsChanged: function() { + var that = this; + if (this.tagChanged === true) return this.tagChanged = false; + this.bokmarkChanged = true; + that.tags.fetch({data: {count: true}}); // we listen to 'sync', so we can fetch immediately + } +}); diff --git a/js/apps/MainRouter.js b/js/apps/MainRouter.js new file mode 100644 index 000000000..53cea684c --- /dev/null +++ b/js/apps/MainRouter.js @@ -0,0 +1,49 @@ +import Backbone from 'backbone'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.AppRouter.extend({ + controller: { + index: function() { + setTimeout(function(){ + Backbone.history.navigate('all', {trigger: true}); + }, 1); + }, + all: function() { + this.app.bookmarks.setFetchQuery({}) + this.app.bookmarks.fetchPage(); + Radio.channel('nav').trigger('navigate', 'all'); + }, + favorites: function() { + Radio.channel('nav').trigger('navigate', 'favorites'); + }, + shared: function() { + this.app.bookmarks.setFetchQuery({}); + this.app.bookmarks.fetchPage(); + Radio.channel('nav').trigger('navigate', 'shared'); + }, + tags: function(tagString) { + var tags = tagString? tagString.split(',').map(decodeURIComponent) : []; + this.app.bookmarks.setFetchQuery({tags: tags, conjunction: 'and'}); + this.app.bookmarks.fetchPage(); + Radio.channel('nav').trigger('navigate', 'tags', tags); + }, + search: function(query) { + this.app.bookmarks.setFetchQuery({search: decodeURIComponent(query).split(' '), conjunction: 'and'}); + this.app.bookmarks.fetchPage(); + Radio.channel('nav').trigger('navigate', 'search', query); + } + }, + appRoutes: { + '': 'index', + 'all': 'all', + 'favorites': 'favorites', + 'shared': 'shared', + 'tags(/*tags)': 'tags', + 'search/:query': 'search' + }, + initialize: function(options) { + this.controller.app = options.app; + } +}); diff --git a/js/bookmarklet.js b/js/bookmarklet.js index bd5895748..55a183834 100644 --- a/js/bookmarklet.js +++ b/js/bookmarklet.js @@ -1,73 +1,13 @@ -var ajaxCallCount = 0; - -function increaseAjaxCallCount() { - ajaxCallCount++; - if (ajaxCallCount - 1 === 0) { - updateLoadingAnimation(); - } -} - -function decreaseAjaxCallCount() { - if (ajaxCallCount > 0) { - ajaxCallCount--; - updateLoadingAnimation(); - } -} - -function updateLoadingAnimation() { - if (ajaxCallCount === 0) { - $("#add_form_loading").css("visibility", "hidden"); - } else { - $("#add_form_loading").css("visibility", "visible"); - } -} - -$(document).ready(function () { - $(".submit").click(function () { - increaseAjaxCallCount(); - - var endpoint = 'bookmark'; - var method = 'POST'; - var id = ''; - if($('#bookmarkID').length > 0) { - endpoint += '/'+ $('#bookmarkID').val(); - method = 'PUT'; - id = '&record_id=' + $('#bookmarkID').val(); - } - - var tags = ''; - $('.tagit-choice .tagit-label').each(function() { - tags += '&item[tags][]='+$(this).text(); - }); - var dataString = 'url=' + $("input#url").val() + '&description=' + - $("textarea#description").val() + '&title=' + $("input#title").val() + tags + id; - $.ajax({ - type: method, - url: endpoint, - data: dataString, - complete: function () { - decreaseAjaxCallCount(); - }, - success: function (data) { - if (data.status === 'success') { - OC.dialogs.message("Bookmark added.", "Success", undefined, [], undefined, true) - _.delay(function() { - window.close(); - }, 1e3); - } else { - OC.dialogs.alert(t("bookmarks", "Some Error happened."), - t("bookmarks", "Error"), null, true); - } - } - }); - return false; - }); - - $.get('tag', function (data) { - $('.tags').tagit({ - allowSpaces: true, - availableTags: data, - placeholderText: t('bookmark', 'Tags') - }); - }); +import _ from 'underscore'; +import Backbone from 'backbone'; +import Marionette from 'backbone.marionette'; +import select2 from 'select2'; +import App from './apps/Bookmarklet'; +import fixBackboneSync from './utils/FixBackboneSync'; + +// init + +var app = new App(); +$(function() { + app.start(); }); diff --git a/js/bookmarklet_old.js b/js/bookmarklet_old.js new file mode 100644 index 000000000..c3b54e154 --- /dev/null +++ b/js/bookmarklet_old.js @@ -0,0 +1,73 @@ +var ajaxCallCount = 0; + +function increaseAjaxCallCount() { + ajaxCallCount++; + if (ajaxCallCount - 1 === 0) { + updateLoadingAnimation(); + } +} + +function decreaseAjaxCallCount() { + if (ajaxCallCount > 0) { + ajaxCallCount--; + updateLoadingAnimation(); + } +} + +function updateLoadingAnimation() { + if (ajaxCallCount === 0) { + $('#add_form_loading').css('visibility', 'hidden'); + } else { + $('#add_form_loading').css('visibility', 'visible'); + } +} + +$(document).ready(function () { + $('.submit').click(function () { + increaseAjaxCallCount(); + + var endpoint = 'bookmark'; + var method = 'POST'; + var id = ''; + if($('#bookmarkID').length > 0) { + endpoint += '/'+ $('#bookmarkID').val(); + method = 'PUT'; + id = '&record_id=' + $('#bookmarkID').val(); + } + + var tags = ''; + $('.tagit-choice .tagit-label').each(function() { + tags += '&item[tags][]='+$(this).text(); + }); + var dataString = 'url=' + $('input#url').val() + '&description=' + + $('textarea#description').val() + '&title=' + $('input#title').val() + tags + id; + $.ajax({ + type: method, + url: endpoint, + data: dataString, + complete: function () { + decreaseAjaxCallCount(); + }, + success: function (data) { + if (data.status === 'success') { + OC.dialogs.message('Bookmark added.', 'Success', undefined, [], undefined, true); + _.delay(function() { + window.close(); + }, 1e3); + } else { + OC.dialogs.alert(t('bookmarks', 'Some Error happened.'), + t('bookmarks', 'Error'), null, true); + } + } + }); + return false; + }); + + $.get('tag', function (data) { + $('.tags').tagit({ + allowSpaces: true, + availableTags: data, + placeholderText: t('bookmark', 'Tags') + }); + }); +}); diff --git a/js/bookmarks.js b/js/bookmarks.js deleted file mode 100644 index 5bbff687c..000000000 --- a/js/bookmarks.js +++ /dev/null @@ -1,429 +0,0 @@ -var bookmarksPage = 0; -var bookmarksLoading = false; -var dialog; -var bookmarksSorting = 'bookmarks_sorting_recent'; -var fullTags = []; -var ajaxCallCount = 0; - -$(document).ready(function () { - getTags(); - watchUrlField(); - $('#bm_import').change(attachSettingEvent); - $('#add_url').on('keydown keyup change click', watchUrlField); - $('#app-settings').on('click keydown', toggleSettings); - $('#bm_export').click(exportBm); - $('#emptycontent-setting').click(function () { - if (!$('#app-settings').hasClass('open')) { - $('#app-settings').click(); - } - }); - $('.bookmarks_list').scroll(updateOnBottom).empty(); - $('#tag_filter input').tagit({ - allowSpaces: true, - availableTags: fullTags, - onTagFinishRemoved: filterTagsChanged, - placeholderText: t('bookmarks', 'Filter by tag') - }).tagit('option', 'onTagAdded', filterTagsChanged); - getBookmarks(); -}); - -function getTags() { - jQuery.ajax({ - url: 'tag', - success: function (result) { - fullTags = result; - }, - async: false - }); -} - -var formatString = (function () { - var replacer = function (context) { - return function (s, name) { - return context[name]; - }; - }; - - return function (input, context) { - return input.replace(/\{(\w+)\}/g, replacer(context)); - }; -})(); - -function increaseAjaxCallCount() { - ajaxCallCount++; - if (ajaxCallCount - 1 === 0) { - updateLoadingAnimation(); - } -} - -function decreaseAjaxCallCount() { - if (ajaxCallCount > 0) { - ajaxCallCount--; - updateLoadingAnimation(); - } -} - -function updateLoadingAnimation() { - if (ajaxCallCount === 0) { - $('#bookmark_add_submit').removeClass('icon-loading-small'); - $('#bookmark_add_submit').addClass('icon-add'); - } else { - $('#bookmark_add_submit').removeClass('icon-add'); - $('#bookmark_add_submit').addClass('icon-loading-small'); - } -} - -function watchClickInSetting(e) { - if ($('#app-settings').find($(e.target)).length === 0) { - toggleSettings(); - } -} - -function checkURL(url) { - if (url.substring(0, 3) === "htt") { - return url; - } - return "http://" + url; -} - -function toggleSettings() { - if ($('#app-settings').hasClass('open')) { //Close - $('#app-settings').switchClass("open", ""); - $('body').unbind('click', watchClickInSetting); - } - else { - $('#app-settings').switchClass("", "open"); - $('body').bind('click', watchClickInSetting); - } -} -function addFilterTag(event) { - event.preventDefault(); - $('#tag_filter input').tagit('createTag', $(this).text()); -} - -function updateTagsList(tag) { - var html = tmpl("tag_tmpl", tag); - $('.tag_list').append(html); -} - -function filterTagsChanged() -{ - $('#bookmarkFilterTag').val($('#tag_filter input').val()); - $('.bookmarks_list').empty(); - bookmarksPage = 0; - getBookmarks(); -} -function getBookmarks() { - if (bookmarksLoading) { - //have patience :) - return; - } - increaseAjaxCallCount(); - bookmarksLoading = true; - //Update Rel Tags if first page - if (bookmarksPage === 0) { - - $.ajax({ - type: 'GET', - url: 'bookmark', - data: {type: 'rel_tags', tag: $('#bookmarkFilterTag').val(), page: bookmarksPage, sort: bookmarksSorting}, - success: function (tags) { - $('.tag_list').empty(); - for (var i in tags.data) { - updateTagsList(tags.data[i]); - } - $('.tag_list .tag_edit').click(renameTag); - $('.tag_list .tag_delete').click(deleteTag); - $('.tag_list a.tag').click(addFilterTag); - - - } - }); - } - $.ajax({ - type: 'GET', - url: 'bookmark', - data: {type: 'bookmark', tag: $('#bookmarkFilterTag').val(), page: bookmarksPage, conjunction: 'and', sort: bookmarksSorting}, - complete: function () { - decreaseAjaxCallCount(); - }, - success: function (bookmarks) { - if (bookmarks.data.length) { - bookmarksPage += 1; - } - $('.bookmark_link').unbind('click', recordClick); - $('.bookmark_delete').unbind('click', delBookmark); - $('.bookmark_edit').unbind('click', editBookmark); - - for (var i in bookmarks.data) { - updateBookmarksList(bookmarks.data[i]); - } - checkEmpty(); - - $('.bookmark_link').click(recordClick); - $('.bookmark_delete').click(delBookmark); - $('.bookmark_edit').click(editBookmark); - - bookmarksLoading = false; - if (bookmarks.data.length) { - updateOnBottom(); - } - } - }); -} - -function watchUrlField() { - var form = $('#add_form'); - var el = $('#add_url'); - var button = $('#bookmark_add_submit'); - form.unbind('submit'); - if (!acceptUrl(el.val())) { - form.bind('submit', function (e) { - e.preventDefault(); - }); - button.addClass('disabled'); - } - else { - button.removeClass('disabled'); - form.bind('submit', addBookmark); - } -} - -function acceptUrl(url) { - return url.replace(/^\s+/g, '').replace(/\s+$/g, '') !== ''; -} - -function addBookmark(event) { - event.preventDefault(); - var url = $('#add_url').val(); - //If trim is empty - if (!acceptUrl(url)) { - return; - } - - $('#add_url').val(''); - var bookmark = {url: url, description: '', title: '', from_own: 0, added_date: new Date()}; - increaseAjaxCallCount(); - $.ajax({ - type: 'POST', - url: 'bookmark', - data: bookmark, - complete: function () { - decreaseAjaxCallCount(); - }, - success: function (data) { - if (data.status === 'success') { - // First remove old BM if exists - $('.bookmark_single').filterAttr('data-id', data.item.id).remove(); - - var bookmark = $.extend({}, bookmark, data.item); - updateBookmarksList(bookmark, 'prepend'); - checkEmpty(); - watchUrlField(); - } - }, - error: function () { - OC.Notification.showTemporary(t('bookmarks', 'Could not add bookmark.')); - } - }); -} - -function delBookmark() { - var record = $(this).parent().parent(); - OC.dialogs.confirm(t('bookmarks', 'Are you sure you want to remove this bookmark?'), - t('bookmarks', 'Warning'), function (answer) { - if (answer) { - $.ajax({ - type: 'DELETE', - url: 'bookmark/' + record.data('id'), - success: function (data) { - if (data.status === 'success') { - record.remove(); - checkEmpty(); - } - } - }); - } - }); -} - -function checkEmpty() { - if ($('.bookmarks_list').children().length === 0) { - $("#emptycontent").show(); - $("#bm_export").addClass('disabled'); - $('.bookmarks_list').hide(); - } else { - $("#emptycontent").hide(); - $("#bm_export").removeClass('disabled'); - $('.bookmarks_list').show(); - } -} -function editBookmark() { - if ($('.bookmark_single_form').length) { - $('.bookmark_single_form .reset').click(); - } - var record = $(this).parent().parent(); - var bookmark = record.data('record'); - var html = tmpl("item_form_tmpl", bookmark); - - record.after(html); - record.hide(); - var rec_form = record.next().find('form'); - rec_form.find('.bookmark_form_tags ul').tagit({ - allowSpaces: true, - availableTags: fullTags, - placeholderText: t('bookmarks', 'Tags') - }); - - rec_form.find('.reset').bind('click', cancelBookmark); - rec_form.bind('submit', function (event) { - event.preventDefault(); - var form_values = $(this).serialize(); - if(form_values.indexOf('item%5Btags%5D') === -1) { - // if not tag is selected, the input field does not exist, so - // we need to manually insert an empty array - form_values += "&item%5Btags%5D%5B%5D="; - } - $.ajax({ - type: 'PUT', - url: $(this).attr('action') + "/" + this.elements['record_id'].value, - data: form_values, - success: function (data) { - if (data.status === 'success') { - //@TODO : do better reaction than reloading the page - filterTagsChanged(); - } else { // On failure - //@TODO : show error message? - } - } - }); - }); -} - -function cancelBookmark(event) { - event.preventDefault(); - var rec_form = $(this).closest('form').parent(); - rec_form.prev().show(); - rec_form.remove(); -} - -function updateBookmarksList(bookmark, position) { - position = typeof position !== 'undefined' ? position : 'append'; - bookmark = $.extend({title: '', description: '', added_date: new Date('now'), tags: []}, bookmark); - var tags = bookmark.tags; - var taglist = ''; - for (var i = 0, len = tags.length; i < len; ++i) { - if (tags[i] !== '') - taglist = taglist + '<a class="bookmark_tag" href="#">' + escapeHTML(tags[i]) + '</a> '; - } - if (!hasProtocol(bookmark.url)) { - bookmark.url = 'http://' + bookmark.url; - } - - if (bookmark.added) { - bookmark.added_date.setTime(parseInt(bookmark.added) * 1000); - } - - if (!bookmark.title) - bookmark.title = ''; - - var html = tmpl("item_tmpl", bookmark); - if (position === "prepend") { - $('.bookmarks_list').prepend(html); - } else { - $('.bookmarks_list').append(html); - } - var line = $('.bookmark_single[data-id="' + bookmark.id + '"]'); - line.data('record', bookmark); - if (taglist !== '') { - line.append('<p class="bookmark_tags">' + taglist + '</p>'); - } - line.find('a.bookmark_tag').bind('click', addFilterTag); - line.find('.bookmark_link').click(recordClick); - line.find('.bookmark_delete').click(delBookmark); - line.find('.bookmark_edit').click(editBookmark); - -} - -function updateOnBottom() { - //check wether user is on bottom of the page - var top = $('.bookmarks_list>:last-child').position().top; - var height = $('.bookmarks_list').height(); - // use a bit of margin to begin loading before we are really at the - // bottom - if (top < height * 1.2) { - getBookmarks(); - } -} - -function recordClick() { - $.ajax({ - type: 'POST', - url: 'bookmark/click', - data: 'url=' + encodeURIComponent($(this).attr('href')) - }); -} - -function hasProtocol(url) { - var regexp = /(ftp|http|https|sftp)/; - return regexp.test(url); -} - -function renameTag() { - if ($('input[name="tag_new_name"]').length) - return; // Do nothing if a tag is currenlty edited - var tagElement = $(this).closest('li'); - tagElement.append('<form><input name="tag_new_name" type="text"></form>'); - var form = tagElement.find('form'); - //tag_el.find('.tags_actions').hide(); - var tagName = tagElement.find('.tag').hide().text(); - tagElement.find('input').val(tagName).focus().bind('blur', function () { - form.trigger('submit'); - }); - form.bind('submit', submitTagName); -} - -function submitTagName(event) { - event.preventDefault(); - var tagElement = $(this).closest('li'); - var newTagName = tagElement.find('input').val(); - var oldTagName = tagElement.find('.tag').show().text(); - //tag_el.find('.tag_edit').show(); - //tag_el.find('.tags_actions').show(); - tagElement.find('input').unbind('blur'); - tagElement.find('form').unbind('submit').remove(); - - if (newTagName !== oldTagName && newTagName !== '') { - //submit - $.ajax({ - type: 'POST', - url: 'tag', - data: {old_name: oldTagName, new_name: newTagName}, - success: function (bookmarks) { - if (bookmarks.status === 'success') { - filterTagsChanged(); - } - } - }); - } -} - -function deleteTag() { - var tag_el = $(this).closest('li'); - var old_tag_name = tag_el.find('.tag').show().text(); - OC.dialogs.confirm(t('bookmarks', 'Are you sure you want to remove this tag from every entry?'), - t('bookmarks', 'Warning'), function (answer) { - if (answer) { - $.ajax({ - type: 'DELETE', - url: 'tag', - data: {old_name: old_tag_name}, - success: function (bookmarks) { - if (bookmarks.status === 'success') { - filterTagsChanged(); - } - } - }); - } - }); -} diff --git a/js/main.js b/js/main.js new file mode 100644 index 000000000..9923874ca --- /dev/null +++ b/js/main.js @@ -0,0 +1,15 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Marionette from 'backbone.marionette'; +import select2 from 'select2'; +import Tag from './models/Tag'; +import Tags from './models/Tags'; +import App from './apps/Main'; +import fixBackboneSync from './utils/FixBackboneSync'; + +// init + +var app = new App(); +$(function() { + app.start(); +}); diff --git a/js/models/Bookmark.js b/js/models/Bookmark.js new file mode 100644 index 000000000..c579e554a --- /dev/null +++ b/js/models/Bookmark.js @@ -0,0 +1,16 @@ +import Backbone from 'backbone'; +import $ from 'jquery' + +export default Backbone.Model.extend({ + urlRoot: 'bookmark', + clickLink: function() { + const url = encodeURIComponent(this.get('url')) + $.ajax({ + method: 'POST', + url: 'bookmark/click?url='+url, + headers: { + 'requesttoken': oc_requesttoken + } + }) + }, +}); diff --git a/js/models/Bookmarks.js b/js/models/Bookmarks.js new file mode 100644 index 000000000..68b602014 --- /dev/null +++ b/js/models/Bookmarks.js @@ -0,0 +1,53 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Bookmark from './Bookmark'; + +const BATCH_SIZE = 30; + +export default Backbone.Collection.extend({ + model: Bookmark, + url: 'bookmark', + parse: function(json) { + return json.data; + }, + initialize: function() { + this.loadingState = new Backbone.Model({ + page: 0, + query: {}, + fetching: false, + reachedEnd: false + }) + }, + setFetchQuery: function(data) { + this.loadingState.set({ + page: 0, + query: data, + fetching: false, + reachedEnd: false + }) + }, + fetchPage: function() { + var that = this; + if (this.loadingState.get('fetching') || this.loadingState.get('reachedEnd')) { + return; + } + const nextPage = this.loadingState.get('page'); + this.loadingState.set({page: nextPage+1, fetching: true}); + + // Show spinner after 1.5s + const spinnerTimeout = setTimeout(() => this.reset(), 1500); + + return this.fetch({ + data: _.extend({}, this.loadingState.get('query'), {page: nextPage, limit: BATCH_SIZE}), + reset: nextPage === 0, + remove: false, + success: function(collections, response) { + clearTimeout(spinnerTimeout); + that.loadingState.set({ + fetching: false, + reachedEnd: response.data.length < BATCH_SIZE + }); + } + }); + } +}); diff --git a/js/models/Tag.js b/js/models/Tag.js new file mode 100644 index 000000000..78999f55e --- /dev/null +++ b/js/models/Tag.js @@ -0,0 +1,6 @@ +import Backbone from 'backbone'; + +export default Backbone.Model.extend({ + idAttribute: 'name', + urlRoot: 'tag' +}); diff --git a/js/models/Tags.js b/js/models/Tags.js new file mode 100644 index 000000000..626c7c7f9 --- /dev/null +++ b/js/models/Tags.js @@ -0,0 +1,13 @@ +import Backbone from 'backbone'; +import Tag from './Tag'; + +var Tags = Backbone.Collection.extend({ + model: Tag, + comparator: function(t) {return -t.get('count');}, + url: 'tag', + parse: function(json) { + return json; + } +}); + +export default Tags; diff --git a/js/settings.js b/js/settings.js deleted file mode 100644 index 226c84fe5..000000000 --- a/js/settings.js +++ /dev/null @@ -1,63 +0,0 @@ -function attachSettingEvent(event) { - event.preventDefault(); - fileUpload($(this).closest('form'), $('#upload')); -} - -function exportBm() { - window.location = $(this).attr('href'); -} - -function fileUpload(form, resultDiv) { - - var uploadEventHandler = function () { - var data = {}; - try { - data = $.parseJSON(iframe.contents().text()); - } catch (e) { - } - if (!data) { - resultDiv.text(t('bookmark', 'Import error')); - return; - } - if (data.status == 'error') { - var list = $("<ul></ul>").addClass('setting_error_list'); - console.log(data); - $.each(data.data, function (index, item) { - list.append($("<li></li>").text(item)); - }); - resultDiv.html(list); - } else { - resultDiv.text(t('bookmark', 'Import completed successfully.')); - getBookmarks(); - } - }; - - // Create the iframe... - var iframe; - if ($('#upload_iframe').length === 1) - iframe = $('#upload_iframe'); - else { - iframe = $('<iframe></iframe>').attr({ - id: 'upload_iframe', - name: 'upload_iframe', - width: '0', - height: '0', - border: '0', - style: 'display:none' - }).bind('load', uploadEventHandler); - form.append(iframe); - } - - // Set properties of form... - form.attr({ - target: 'upload_iframe', - method: 'post', - enctype: 'multipart/form-data', - encoding: 'multipart/form-data' - }); - - // Submit the form... - form.submit(); - - resultDiv.text(t('bookmark', 'Uploading...')); -} \ No newline at end of file diff --git a/js/templates/AddBookmark.html b/js/templates/AddBookmark.html new file mode 100644 index 000000000..5ee872c5d --- /dev/null +++ b/js/templates/AddBookmark.html @@ -0,0 +1,7 @@ +<li class="link"> + <a href="#" class="icon-add"><%- t('bookmarks', 'Add a Bookmark') %></a> +</li> +<li class="form" style="display: none"> + <input type="text" value="" placeholder="<%- t('bookmarks', 'Address') %>"/> + <button title="<%- t('bookmarks', 'Add this to my bookmarks')" class="icon-add"></button> +</li> diff --git a/js/templates/App.html b/js/templates/App.html new file mode 100644 index 000000000..80a81e33d --- /dev/null +++ b/js/templates/App.html @@ -0,0 +1,9 @@ +<div id="app-navigation"> + <div id="add-bookmark-slot"></div> + <div id="navigation-slot"></div> + <h3><span class="icon-tag"></span> <%- t('bookmarks', 'Tags') %></h3> + <div id="favorite-tags-slot"></div> + <div id="settings-slot"></div> +</div> +<div id="app-content"> +</div> diff --git a/js/templates/BookmarkCard.html b/js/templates/BookmarkCard.html new file mode 100644 index 000000000..7f80ffc32 --- /dev/null +++ b/js/templates/BookmarkCard.html @@ -0,0 +1,7 @@ +<div class="panel"> + <input type="checkbox" name="selector" class="checkbox"/> + <label class="selectbox" for="selector" title="<%- t('bookmarks', 'Select') %>"></label> + <h1><%- title %></h1> + <h2><a target="_blank" href="<%- url %>"><span class="icon-external"></span><%- new URL(url).host %></a></h2> +</div> +<div class="tags"></div> diff --git a/js/templates/BookmarkDetail.html b/js/templates/BookmarkDetail.html new file mode 100644 index 000000000..43984bf32 --- /dev/null +++ b/js/templates/BookmarkDetail.html @@ -0,0 +1,18 @@ +<div class="close icon-close" title="<%- t('bookmarks', 'Close') %>"></div> +<div class="status"> + <span class="message message-saving"><span class="icon-loading"></span></span> + <span class="message message-saved"><span class="icon-checkmark"></span> <%- t('bookmarks', 'Saved') %></span> +</div> +<div class="actions"> + <button class="delete icon-delete"></button> +</div> +<h1 data-attribute="title"><%- title %></h1> +<h3>Link</h3> +<h2 data-attribute="url"> + <a target="_blank" href="<%- url %>"><span class="icon-external"></span><%- new URL(url).host %></a> + <a href="#" class="edit"><span class="icon-rename"></span></a> +</h2> +<h3><%- t('bookmarks', 'Tags') %></h3> +<div class="tags"></div> +<h3><%- t('bookmarks', 'Description') %></h3> +<div class="description <%- !description.trim()? 'empty' : '' %>" data-attribute="description"><%- description.trim()? description : t('bookmarks', 'Add a description...') %></div> diff --git a/js/templates/BulkActions.html b/js/templates/BulkActions.html new file mode 100644 index 000000000..ce7a3b9d5 --- /dev/null +++ b/js/templates/BulkActions.html @@ -0,0 +1,10 @@ +<div class="selection-tools"> + <button class="primary select-all"><span class="icon-checkmark-white"></span><span> <%- t('bookmarks', 'Select all visible') %></span></button> + <div class="close" title="<%- t('bookmarks', 'Cancel') %>"><span class="icon-close"></span></div> +</div> +<button class="delete"> + <span class="icon-delete"></span> + <span><%- t('bookmarks', 'Delete') %></span> +</button> +<div class="tags"> +</div> diff --git a/js/templates/Content.html b/js/templates/Content.html new file mode 100644 index 000000000..33eef09c6 --- /dev/null +++ b/js/templates/Content.html @@ -0,0 +1,4 @@ +<div id="mobile-nav-slot"></div> +<div id="bulk-actions-slot"></div> +<div id="view-bookmarks-slot"></div> +<div id="bookmark-detail-slot"></div> diff --git a/js/templates/EmptyBookmarks.html b/js/templates/EmptyBookmarks.html new file mode 100644 index 000000000..fe91e76c5 --- /dev/null +++ b/js/templates/EmptyBookmarks.html @@ -0,0 +1,2 @@ +<h2><%- t('bookmarks', 'No bookmarks here.') %></h2> +<p><%- t('bookmarks', 'There are no bookmarks available for this query. Try changing your filter or add some using the menu entry on the left.') %></p> diff --git a/js/templates/LoadingBookmarks.html b/js/templates/LoadingBookmarks.html new file mode 100644 index 000000000..0f455bfc0 --- /dev/null +++ b/js/templates/LoadingBookmarks.html @@ -0,0 +1 @@ +<div class="icon-loading"></div> diff --git a/js/templates/MobileNav.html b/js/templates/MobileNav.html new file mode 100644 index 000000000..9822086f4 --- /dev/null +++ b/js/templates/MobileNav.html @@ -0,0 +1 @@ +<a href="#" class="icon-menu toggle-menu" title="<%- t('bookmarks', 'Open main menu') %>"></a> diff --git a/js/templates/Navigation.html b/js/templates/Navigation.html new file mode 100644 index 000000000..c31bfe632 --- /dev/null +++ b/js/templates/Navigation.html @@ -0,0 +1,11 @@ +<li data-id="all" class="all"> + <a href="#" class="icon-home"><%- t('bookmarks', 'All bookmarks') %></a> +</li> +<!-- +<li data-id="favorites" class="favorites"> + <a href="#"><span class="icon-favorite"></span><%- t('bookmarks', 'Favorites') %></a> +</li> +<li data-id="shared" class="shared"> + <a href="#"><span class="icon-share"></span><%- t('bookmarks', 'Shared') %></a> +</li> +--> diff --git a/js/templates/Settings.html b/js/templates/Settings.html new file mode 100644 index 000000000..f3dccc801 --- /dev/null +++ b/js/templates/Settings.html @@ -0,0 +1,17 @@ +<div id="app-settings-header"> + <button class="settings-button"><%- t('bookmarks', 'Settings') %></button> +</div> +<div id="app-settings-content"> + <h3><%- t('bookmarks', 'Bookmarklet') %></h3> + <p><%- t('bookmarks', 'Drag this to your browser bookmarks and click it, when you want to bookmark a webpage quickly:') %></p> + <a class="button bookmarklet" href=""><%- t('bookmarks', 'Add to {instanceName} ', {instanceName: oc_defaults.name}) %></a> + <h3><%- t('bookmarks', 'Import & Export') %></h3> + <form class="import-form" action="bookmark/import" method="post" target="upload_iframe" enctype="multipart/form-data" encoding="multipart/form-data"> + <input type="file" class="import" name="bm_import" size="5" /> + <input type="hidden" name="requesttoken" value="<%- oc_requesttoken %>" /> + <button class="import-facade"><span class="icon-upload"></span> <%- t('bookmarks', 'Import') %></button> + </form> + <iframe class="upload" name="upload_iframe" id="upload_iframe"></iframe> + <button class="export"><span class="icon-download"></span> <%- t('bookmarks', 'Export') %></button> + <div class="import-status"></div> +</div> diff --git a/js/templates/TagsManagementTag_default.html b/js/templates/TagsManagementTag_default.html new file mode 100644 index 000000000..2f1e5e0c6 --- /dev/null +++ b/js/templates/TagsManagementTag_default.html @@ -0,0 +1,39 @@ +<a href="#"> + <span><%- name %></span> +</a> +<div class="app-navigation-entry-utils"> + <ul> + <li class="app-navigation-entry-utils-counter"><%- count > 999 ? "999+" :count %></li> + <li class="app-navigation-entry-utils-menu-button"> + <button></button> + </li> + </ul> +</div> +<div class="app-navigation-entry-menu"> + <ul> + <li> + <button class="menu-item-checkbox menu-filter-add"> + <span><input type="checkbox" name="select" class="checkbox" /><label for="select"></label></span> + <span><%- t('bookmarks', 'Add to filter') %></span> + </button> + </li> + <li> + <button class="menu-item-checkbox menu-filter-remove"> + <span><input type="checkbox" name="select" checked class="checkbox" /><label for="select"></label></span> + <span><%- t('bookmarks', 'Remove from filter') %></span> + </button> + </li> + <li> + <button class="menu-edit"> + <span class="icon-rename"></span> + <span><%- t('bookmarks', 'Rename') %></span> + </button> + </li> + <li> + <button class="menu-delete"> + <span class="icon-delete"></span> + <span><%- t('bookmarks', 'Delete') %></span> + </button> + </li> + </ul> +</div> diff --git a/js/templates/TagsManagementTag_editing.html b/js/templates/TagsManagementTag_editing.html new file mode 100644 index 000000000..28ededa3f --- /dev/null +++ b/js/templates/TagsManagementTag_editing.html @@ -0,0 +1,13 @@ +<a href="#"> + <input type="text" value="<%- name %>"> + <div class="actions"> + <ul> + <li class="action"> + <button class="submit icon-checkmark"></button> + </li> + <li class="action"> + <button class="cancel icon-close"></button> + </li> + </ul> + </div> +</a> diff --git a/js/templates/TagsNavigationTag.html b/js/templates/TagsNavigationTag.html new file mode 100644 index 000000000..c0f2bbc75 --- /dev/null +++ b/js/templates/TagsNavigationTag.html @@ -0,0 +1 @@ +<a href="#"><%- name %></a> diff --git a/js/utils/FixBackboneSync.js b/js/utils/FixBackboneSync.js new file mode 100644 index 000000000..5dc587b33 --- /dev/null +++ b/js/utils/FixBackboneSync.js @@ -0,0 +1,14 @@ +import Backbone from 'backbone'; + +var _sync = Backbone.sync; +Backbone.sync = function(method, model, options) { + var overrideOptions = { + headers: { + 'requesttoken': oc_requesttoken + } + }; + if (method === 'update' && model instanceof Tag) { + overrideOptions.url = model.urlRoot+'/'+model.previous('name'); + } + _sync(method, model, _.extend({}, options, overrideOptions)); +}; diff --git a/js/views/AddBookmark.js b/js/views/AddBookmark.js new file mode 100644 index 000000000..30c4e662a --- /dev/null +++ b/js/views/AddBookmark.js @@ -0,0 +1,77 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Bookmark from '../models/Bookmark'; +import templateString from '../templates/AddBookmark.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + template: _.template(templateString), + className: 'add-bookmark', + tagName: 'ul', + events: { + 'click @ui.link': 'activate', + 'click @ui.button': 'submit', + 'keydown @ui.input': 'onKeydown', + 'blur @ui.input': 'deactivate' + }, + ui: { + 'link': '.link a', + 'linkEntry': '.link', + 'formEntry': '.form', + 'input': 'input', + 'button': 'button' + }, + activate: function() { + this.getUI('linkEntry').hide(); + this.getUI('formEntry').show(); + this.getUI('input').focus(); + }, + deactivate: function() { + this.getUI('linkEntry').show(); + this.getUI('formEntry').hide(); + this.getUI('input').val(''); + }, + onKeydown: function(e) { + if (e.which != 13) return; + // Enter + this.submit(); + }, + submit: function(e) { + var $input = this.getUI('input'); + if (this.pending || $input.val() === '') return; + var url = $input.val(); + var bm = new Bookmark({url: url}); + this.setPending(true); + var that = this; + bm.save(null,{ + success: function() { + // needed in order for the route to be revaluated when it's already active + Backbone.history.navigate('dummyroute'); + Backbone.history.navigate('all', {trigger: true}); + that.setPending(false); + that.deactivate(); + }, + error: function() { + that.setPending(false); + that.getUI('button').removeClass('icon-add'); + that.getUI('button').addClass('icon-error-color'); + } + }); + }, + setPending: function(pending) { + if (pending) { + this.getUI('button').removeClass('icon-add'); + this.getUI('button').removeClass('icon-error-color'); + this.getUI('button').addClass('icon-loading-small'); + this.getUI('button').prop('disabled', true); + }else { + this.getUI('button').removeClass('icon-error-color'); + this.getUI('button').addClass('icon-add'); + this.getUI('button').removeClass('icon-loading-small'); + this.getUI('button').prop('disabled', false); + } + this.pending = pending; + } +}); diff --git a/js/views/App.js b/js/views/App.js new file mode 100644 index 000000000..8a40b46ca --- /dev/null +++ b/js/views/App.js @@ -0,0 +1,53 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import SearchController from './SearchController'; +import AddBookmarkView from './AddBookmark'; +import NavigationView from './Navigation'; +import TagsManagementView from './TagsManagement'; +import ContentView from './Content'; +import SettingsView from './Settings'; +import templateString from '../templates/App.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + template: _.template(templateString), + regions: { + 'addBookmarks': { + el: '#add-bookmark-slot', + replaceElement: true + }, + 'navigation': { + el: '#navigation-slot', + replaceElement: true + }, + 'content': { + el: '#app-content', + replaceElement: true + }, + 'tags': { + el: '#favorite-tags-slot', + replaceElement: true + }, + 'settings': { + el: '#settings-slot', + replaceElement: true + } + }, + initialize: function(options) { + this.app = options.app; + this.searchController = new SearchController; + + $(window.document).click(function(e) { + Radio.channel('documentClicked').trigger('click', e); + }); + }, + onRender: function() { + this.showChildView('addBookmarks', new AddBookmarkView()); + this.showChildView('navigation', new NavigationView); + this.showChildView('content', new ContentView({app: this.app})); + this.showChildView('tags', new TagsManagementView({collection: this.app.tags})); + this.showChildView('settings', new SettingsView()); + } +}); diff --git a/js/views/BookmarkCard.js b/js/views/BookmarkCard.js new file mode 100644 index 000000000..178649958 --- /dev/null +++ b/js/views/BookmarkCard.js @@ -0,0 +1,85 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagsNavigationView from './TagsNavigation'; +import templateString from '../templates/BookmarkCard.html'; +import colorPalettes from 'nice-color-palettes'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +const COLORS = colorPalettes.reduce((p1, p2) => p1.concat(p2), []) +const simpleHash = (str) => { + var hash = 0; + for (var i = 0; i<str.length; i++){ + hash = str.charCodeAt(i) + (hash << 6) + (hash << 16) - hash; + } + return hash +} + +export default Marionette.View.extend({ + template: _.template(templateString), + className: 'bookmark-card', + ui: { + 'link': 'h2 > a', + 'checkbox': '.selectbox', + }, + regions: { + 'tags': '.tags' + }, + events: { + 'click': 'open', + 'click @ui.link': 'clickLink', + 'click @ui.checkbox': 'select', + }, + initialize: function(opts) { + this.app = opts.app; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'select', this.onSelect); + this.listenTo(this.model, 'unselect', this.onUnselect); + this.listenTo(this.app.tags, 'sync', this.render); + this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions); + }, + onRender: function() { + var that = this; + if (this.model.get('image')) { + this.$el.css('background-image', 'url(bookmark/'+this.model.get('id')+'/image)'); + } else { + this.$el.css('background-color', COLORS[simpleHash(this.model.get('url')) & 63] + '66') + } + var tags = new Tags(this.model.get('tags').map(function(id) { + return that.app.tags.findWhere({name: id}); + })); + this.showChildView('tags', new TagsNavigationView({collection: tags})); + this.$('.checkbox').prop('checked', this.$el.hasClass('active')); + }, + clickLink: function() { + this.model.clickLink(); + }, + open: function(e) { + if (e && (e.target === this.getUI('checkbox')[0] || e.target === this.getUI('link')[0])) { + return; + } + if (this.$el.closest('.selection-active').length) { + this.select(e); + return + } + Radio.channel('details').trigger('show', this.model); + }, + select: function(e) { + e.stopPropagation(); + if (this.$el.hasClass('active')) { + this.model.trigger('unselect', this.model); + }else{ + this.model.trigger('select', this.model); + } + }, + onSelect: function() { + this.$el.addClass('active'); + this.render(); + }, + onUnselect: function() { + this.$el.removeClass('active'); + this.render(); + } +}); diff --git a/js/views/BookmarkDetail.js b/js/views/BookmarkDetail.js new file mode 100644 index 000000000..66ad7a699 --- /dev/null +++ b/js/views/BookmarkDetail.js @@ -0,0 +1,122 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagsNavigationView from './TagsNavigation'; +import TagsSelectionView from './TagsSelection'; +import templateString from '../templates/BookmarkDetail.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + template: _.template(templateString), + className: 'bookmark-detail', + regions: { + 'tags': { + el: '.tags' + } + }, + ui: { + 'link': 'h2 > a', + 'close': '> .close', + 'edit': '.edit', + 'delete': '.delete', + 'status': '.status' + }, + events: { + 'click @ui.link': 'clickLink', + 'click @ui.close': 'close', + 'click @ui.delete': 'delete', + 'click h1': 'edit', + 'click h2 .edit': 'edit', + 'click .description': 'edit', + 'click .submit': 'submit', + 'click .cancel': 'cancel' + }, + initialize: function(opts) { + this.app = opts.app; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'destroy', this.onDestroy); + this.listenTo(this.app.tags, 'sync', this.render); + }, + onRender: function() { + var that = this; + this.tags = new Tags(this.model.get('tags').map(function(id) { + return that.app.tags.findWhere({name: id}); + })); + this.listenTo(this.tags, 'add remove', this.submitTags); + this.showChildView('tags', new TagsSelectionView({collection: this.app.tags, selected: this.tags, app: this.app })); + + if (this.savingState === 'saving') { + this.savingState = 'saved'; + } + if (this.savingState === 'saved') { + this.getUI('status').addClass('saved'); + } + }, + clickLink: function() { + this.model.clickLink(); + }, + close: function() { + Radio.channel('details').trigger('close'); + }, + edit: function(e) { + var that = this; + e.preventDefault(); + + var $el = $(e.target).closest('[data-attribute]'); + + if ($el.prop('contenteditable') === true) { + return; + } + + switch($el.data('attribute')) { + case 'url': + $el.text(this.model.get('url')); + // fallthrough + case 'title': + $el.on('keydown', function(e) { + // enter + if (e.which === 13) { + that.submit($el); + } + }); + break; + case 'description': + if ($el.hasClass('empty')) { + $el.text('') + } + break; + } + $el.prop('contenteditable', true); + $el.one('blur', function() { + that.submit($el); + }); + $el.focus(); + }, + submitTags: function() { + this.model.set({ + 'tags': this.tags.pluck('name'), + }); + this.model.save({wait: true}); + this.savingState = 'saving'; + this.getUI('status').removeClass('saved').addClass('saving'); + }, + submit: function($el) { + if (this.savingState === 'saving') { + return; + } + this.savingState = 'saving'; + this.model.set({ + [$el.data('attribute')]: $el.text() + }); + this.model.save({wait: true}); + this.getUI('status').removeClass('saved').addClass('saving'); + }, + delete: function() { + this.model.destroy() + }, + onDestroy: function() { + this.close(); + } +}); diff --git a/js/views/Bookmarklet.js b/js/views/Bookmarklet.js new file mode 100644 index 000000000..803caa999 --- /dev/null +++ b/js/views/Bookmarklet.js @@ -0,0 +1,54 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Bookmark from '../models/Bookmark'; +import Tags from '../models/Tags'; +import TagsSelectionView from './TagsSelection'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + template: false, + el: '#bookmarklet_form', + regions: { + 'tags': { + el: '#tags', + replaceElement: true + }, + }, + events: { + 'click .submit': 'submit' + }, + initialize: function(options) { + this.app = options.app; + + $(window.document).click(function(e) { + Radio.channel('documentClicked').trigger('click', e); + }); + + this.app.tags.once('reset sync add remove', () => { + this.selected = new Tags( + this.$('#tags li') + .map((e) => $(e).text()) + .map((tagName) => this.app.tags.findWhere({name: tagName})) + ); + this.showChildView('tags', new TagsSelectionView({app: this.app, selected: this.selected})); + }); + }, + submit: function(e) { + e.preventDefault(); + this.$('#add_form_loading').css('visibility', 'visible'); + var bm = new Bookmark({ + title: this.$('.title').val(), + url: this.$('.url_input').val(), + description: this.$('.desc').val(), + tags: this.selected.pluck('name') + }); + bm.once('sync', () => setTimeout(() => window.close(), 1e3)); + bm.save({ + wait: true, + error: () => OC.dialogs.alert(t('bookmarks', 'An error occurred while trying to save the bookmark.'), + t('bookmarks', 'Error'), null, true) + }); + } +}); diff --git a/js/views/Bookmarks.js b/js/views/Bookmarks.js new file mode 100644 index 000000000..e527de2ee --- /dev/null +++ b/js/views/Bookmarks.js @@ -0,0 +1,21 @@ +import Backbone from 'backbone'; +import BookmarkCardView from './BookmarkCard'; +import EmptyBookmarksView from './EmptyBookmarks'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.CollectionView.extend({ + className: 'bookmarks', + initialize: function(opts) { + this.app = opts.app; + }, + childViewOptions: function() { + return {app: this.app}; + }, + childView: function() {return BookmarkCardView;}, + emptyViewOptions: function() { + return {app: this.app}; + }, + emptyView: function() {return EmptyBookmarksView;} +}); diff --git a/js/views/BulkActions.js b/js/views/BulkActions.js new file mode 100644 index 000000000..8acaa926d --- /dev/null +++ b/js/views/BulkActions.js @@ -0,0 +1,95 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagsSelectionView from './TagsSelection'; +import templateString from '../templates/BulkActions.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'bulk-actions', + template: _.template(templateString), + regions: { + 'tags': { + el: '.tags' + } + }, + events: { + 'click .delete': 'delete', + 'click .select-all': 'selectAll', + 'click .selection-tools .close': 'abort' + }, + initialize: function(opts) { + this.app = opts.app; + this.all = this.app.bookmarks; + this.selected = opts.selected; + this.tags = new Tags; + this.listenTo(this.tags, 'remove', this.onTagRemoved); + this.listenTo(this.tags, 'add', this.onTagAdded); + this.listenTo(this.selected, 'remove', this.onReduceSelection); + this.listenTo(this.selected, 'add', this.onExtendSelection); + }, + onRender: function() { + this.showChildView('tags', new TagsSelectionView({collection: this.app.tags, selected: this.tags, app: this.app })); + }, + updateTags: function() { + var that = this; + this.triggeredByAlgo = true; + this.tags.reset( + _.intersection.apply(_, this.selected.pluck('tags')) + .map(function(name) { + return that.app.tags.get(name); + }) + ); + this.triggeredByAlgo = false; + }, + onReduceSelection: function() { + if (this.selected.length == 0) { + this.$el.removeClass('active'); + } + this.updateTags(); + }, + onExtendSelection: function() { + if (this.selected.length == 1) { + this.$el.addClass('active'); + } + this.updateTags(); + }, + delete: function() { + this.selected.slice().forEach(function(model) { + model.trigger('unselect', model); + model.destroy({ + error: function() { + Backbone.history.navigate('all', {trigger: true}); + } + }); + }); + }, + onTagAdded: function(tag) { + if (this.triggeredByAlgo) return; + this.selected.forEach(function(model) { + var tags = model.get('tags'); + model.set('tags', _.union(tags, [tag.get('name')])); + model.save(); + }); + }, + onTagRemoved: function(tag) { + if (this.triggeredByAlgo) return; + this.selected.forEach(function(model) { + var tags = model.get('tags'); + model.set('tags', _.without(tags, tag.get('name'))); + model.save(); + }); + }, + selectAll: function() { + this.all.forEach(function(model) { + model.trigger('select', model); + }); + }, + abort: function() { + this.selected.models.slice().forEach(function(model) { + model.trigger('unselect', model); + }); + } +}); diff --git a/js/views/Content.js b/js/views/Content.js new file mode 100644 index 000000000..b7dbad581 --- /dev/null +++ b/js/views/Content.js @@ -0,0 +1,78 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Bookmarks from '../models/Bookmarks'; +import MobileNavView from './MobileNav'; +import BulkActionsView from './BulkActions'; +import BookmarksView from './Bookmarks'; +import BookmarkDetailView from './BookmarkDetail'; +import templateString from '../templates/Content.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + template: _.template(templateString), + id: 'app-content', + regions: { + 'mobileNav': { + el: '#mobile-nav-slot', + replaceElement: true + }, + 'bulkActions': { + el: '#bulk-actions-slot', + replaceElement: true + }, + 'viewBookmarks': { + el: '#view-bookmarks-slot', + replaceElement: true + }, + 'bookmarkDetail': { + el: '#bookmark-detail-slot', + replaceElement: true + } + }, + events: { + 'scroll': 'infiniteScroll' + }, + initialize: function(options) { + this.app = options.app; + this.bookmarks = this.app.bookmarks; + this.selected = new Bookmarks; + this.listenTo(this.bookmarks.loadingState, 'change:fetching', this.infiniteScroll); + this.listenTo(this.bookmarks, 'select', this.onSelect); + this.listenTo(this.bookmarks, 'unselect', this.onUnselect); + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate); + this.listenTo(Radio.channel('details'), 'show', this.onShowDetails); + this.listenTo(Radio.channel('details'), 'close', this.onCloseDetails); + }, + onRender: function() { + this.showChildView('mobileNav', new MobileNavView()); + this.showChildView('bulkActions', new BulkActionsView({selected: this.selected, app: this.app})); + this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks, app: this.app})); + }, + infiniteScroll: function(e) { + if (this.$el.prop('scrollHeight') < this.$el.prop('scrollTop') + this.$el.height() + 500) { + this.bookmarks.fetchPage() + } + }, + onSelect: function(model) { + if (this.selected.length == 0) { + this.$el.addClass('selection-active'); + Radio.channel('details').trigger('close') + } + this.selected.add(model); + }, + onUnselect: function(model) { + if (this.selected.length == 1) { + this.$el.removeClass('selection-active'); + } + this.selected.remove(model); + }, + onShowDetails: function(model) { + var view = new BookmarkDetailView({model: model, app: this.app}); + this.showChildView('bookmarkDetail', view); + }, + onCloseDetails: function() { + this.detachChildView('bookmarkDetail'); + } +}); diff --git a/js/views/EmptyBookmarks.js b/js/views/EmptyBookmarks.js new file mode 100644 index 000000000..a040e7d84 --- /dev/null +++ b/js/views/EmptyBookmarks.js @@ -0,0 +1,23 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateStringEmpty from '../templates/EmptyBookmarks.html'; +import templateStringLoading from '../templates/LoadingBookmarks.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + getTemplate: function() { + if (this.app.bookmarks.loadingState.get('fetching')) { + return _.template(templateStringLoading); + + } else { + return _.template(templateStringEmpty); + } + }, + className: 'bookmarks-empty', + initialize: function(options) { + this.app = options.app; + this.listenTo(this.app.bookmarks.loadingState, 'change:fetching', this.render); + } +}); diff --git a/js/views/MobileNav.js b/js/views/MobileNav.js new file mode 100644 index 000000000..aa167c5f9 --- /dev/null +++ b/js/views/MobileNav.js @@ -0,0 +1,18 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/MobileNav.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'mobile-nav', + template: _.template(templateString), + events: { + 'click .toggle-menu': 'toggleMenu' + }, + toggleMenu: function(e) { + e.preventDefault(); + $('body').toggleClass('mobile-nav-open'); + } +}); diff --git a/js/views/Navigation.js b/js/views/Navigation.js new file mode 100644 index 000000000..a943c4d7c --- /dev/null +++ b/js/views/Navigation.js @@ -0,0 +1,28 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/Navigation.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'navigation', + tagName: 'ul', + template: _.template(templateString), + events: { + 'click .all': 'onClick', + 'click .favorites': 'onClick', + 'click .shared': 'onClick' + }, + initialize: function() { + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this); + }, + onClick: function(e) { + e.preventDefault(); + Backbone.history.navigate(e.target.parentNode.dataset.id, {trigger: true}); + }, + onNavigate: function(category) { + $('.active', this.$el).removeClass('active'); + if (category && this.$('[data-id='+category+']').length) this.$('[data-id='+category+']').addClass('active'); + } +}); diff --git a/js/views/SearchController.js b/js/views/SearchController.js new file mode 100644 index 000000000..53ad678cb --- /dev/null +++ b/js/views/SearchController.js @@ -0,0 +1,36 @@ +import Backbone from 'backbone'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + el: '#searchbox', + initialize: function() { + var that = this; + // register a dummy search plugin + OC.Plugins.register('OCA.Search', { attach: function(search) { + search.setFilter('bookmarks', function(query) { + that.submit(query); + }); + } + }); + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this); + }, + events: { + 'keydown': 'onKeydown' + }, + onRender: function() { + this.$el.show(); + }, + onNavigate: function(route, query) { + if (route === 'search/:query') this.$el.val(decodeURIComponent(query)); + }, + submit: function(query) { + if (query !== '') { + query = encodeURIComponent(query); + Backbone.history.navigate('search/'+query, {trigger: true}); + }else { + Backbone.history.navigate('all', {trigger: true}); + } + } +}); diff --git a/js/views/Settings.js b/js/views/Settings.js new file mode 100644 index 000000000..4096f2bfd --- /dev/null +++ b/js/views/Settings.js @@ -0,0 +1,106 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/Settings.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'settings', + id: 'app-settings', + template: _.template(templateString), + ui: { + 'content': '#app-settings-content', + 'bookmarklet': '.bookmarklet', + 'import': '.import', + 'form': '.import-form', + 'iframe': '.upload', + 'status': '.import-status' + }, + events: { + 'click .settings-button': 'open', + 'click @ui.bookmarklet': 'bookmarkletClick', + 'click .import-facade': 'importTrigger', + 'change @ui.import': 'importSubmit', + 'load @ui.iframe': 'importResult', + 'click .export': 'exportTrigger' + }, + onRender: function() { + const bookmarkletUrl = window.location.origin + oc_webroot + '/index.php/apps/bookmarks/bookmarklet'; + const bookmarkletSrc = `javascript:(function(){var a=window,b=document,c=encodeURIComponent,e=c(document.title),d=a.open('${bookmarkletUrl}?output=popup&url='+c(b.location)+'&title='+e,'bkmk_popup','left='+((a.screenX||a.screenLeft)+10)+',top='+((a.screenY||a.screenTop)+10)+',height=400px,width=550px,resizable=1,alwaysRaised=1');a.setTimeout(function(){d.focus()},300);})();`; + this.getUI('bookmarklet').prop('href', bookmarkletSrc); + }, + open: function(e) { + e.preventDefault(); + this.getUI('content').slideToggle(); + }, + bookmarkletClick: function(e) { + e.preventDefault(); + }, + importTrigger: function(e) { + e.preventDefault(); + this.getUI('import').click(); + }, + importSubmit: function(e) { + var that = this; + e.preventDefault(); + if (typeof(window.fetch) !== 'undefined') { + // If we have fetch() do a little hapiness dance and go! + var data = new FormData(); + data.append('bm_import', this.getUI('import')[0].files[0]); + fetch(this.getUI('form').attr('action'), { + method: 'POST', + headers: { + requesttoken: oc_requesttoken + }, + body: data, + mode: 'same-origin', + credentials: 'same-origin' + }) + .then(function(res) { + if (!res.ok) { + if (res.status === 413) { + return {status: 'error', data: ['Selected file is too large']}; + } + return {status: 'error', data: [res.statusText]}; + } + return res.json(); + }) + .then(function(json) { + that.importResult(JSON.stringify(json)); + }) + .catch(function(e) { + that.importResult(JSON.stringify({status: 'error', data: [e.message]})); + }); + } else { + // If we don't have fetch() ask grandpa iframe to send it + this.getUI('iframe').load(function() { + that.importResult(that.getUI('iframe').contents().text()); + }); + this.getUI('form').submit(); + } + this.getUI('status').text(t('bookmark', 'Uploading...')); + }, + importResult: function (data) { + try { + data = $.parseJSON(data); + } catch (e) { + this.getUI('status').text(t('bookmark', 'Import error')); + return; + } + if (data.status == 'error') { + var list = $('<ul></ul>').addClass('setting_error_list'); + console.log(data); + $.each(data.data, function (index, item) { + list.append($('<li></li>').text(item)); + }); + this.getUI('status').html(list); + return; + } + this.getUI('status').text(t('bookmark', 'Import completed successfully.')); + Backbone.history.navigate('', {trigger: true}); // reload app + }, + exportTrigger: function() { + window.location = 'bookmark/export?requesttoken='+encodeURIComponent(oc_requesttoken); + } +}); diff --git a/js/views/TagsManagement.js b/js/views/TagsManagement.js new file mode 100644 index 000000000..15da35a55 --- /dev/null +++ b/js/views/TagsManagement.js @@ -0,0 +1,64 @@ +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagView from './TagsManagementTag'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.CollectionView.extend({ + childView: TagView, + tagName: 'ul', + className: 'tags-management', + initialize: function(options) { + this.selected = new Tags; + this.selected.comparator = 'name'; + + this.listenTo(this.collection, 'select', this.onSelect); + this.listenTo(this.collection, 'unselect', this.onUnselect); + this.listenTo(this.collection, 'add', this.onAdd); + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate); + }, + onNavigate: function(category, tags) { + // reset selection (needs slice, since we pull the models out from under the loop otherwise) + this.selected.slice().forEach(function(t) { + t.trigger('unselect', t, true); + }); + if (category !== 'tags') { + this.lastRouteTags = []; // for the below hack + return; + } + + var that = this; + // select all tags passed by router + tags.forEach(function(tagName) { + var tag = that.collection.findWhere({name: tagName}); + if (!tag) return; + + tag.trigger('select', tag, true); + }); + + // hack! + // this is for when the route is triggered before the tags are loaded + this.lastRouteTags = tags; + }, + onAdd: function(model) { + if (~this.lastRouteTags.indexOf(model.get('name'))) { + // wait for the tag view to render, so it can receive the event + setTimeout(function() { + model.trigger('select', model, true); + }, 1); + return; + } + }, + onSelect: function(model, silentRoute) { + this.selected.add(model); + if (!silentRoute) this.triggerRoute(); + }, + onUnselect: function(model, silentRoute) { + this.selected.remove(model); + if (!silentRoute) this.triggerRoute(); + }, + triggerRoute: function() { + Backbone.history.navigate('tags/'+this.selected.pluck('name').map(encodeURIComponent).join(','), {trigger: true}); + } +}); diff --git a/js/views/TagsManagementTag.js b/js/views/TagsManagementTag.js new file mode 100644 index 000000000..de20fb4eb --- /dev/null +++ b/js/views/TagsManagementTag.js @@ -0,0 +1,94 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateStringDefault from '../templates/TagsManagementTag_default.html'; +import templateStringEditing from '../templates/TagsManagementTag_editing.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'tag-man-item', + tagName: 'li', + getTemplate: function() { + if (this.editing) { + return this.templateEditing; + } + return this.templateDefault; + }, + templateDefault: _.template(templateStringDefault), + templateEditing: _.template(templateStringEditing), + ui: { + 'actionsMenu': '.app-navigation-entry-menu', + 'actionsToggle': '.app-navigation-entry-utils-menu-button' + }, + events: { + 'click': 'selectSimple', + 'click @ui.actionsToggle': 'toggleActions', + 'click .menu-filter-add': 'actionSelect', + 'click .menu-filter-remove': 'actionUnselect', + 'click .menu-delete': 'actionDelete', + 'click .menu-edit': 'actionEdit', + 'click .action .submit': 'actionSubmit', + 'click .action .cancel': 'actionCancel' + }, + initialize: function() { + this.listenTo(this.model, 'select', this.onSelect); + this.listenTo(this.model, 'unselect', this.onUnselect); + this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions); + }, + onRender: function() { + if (this.selected) { + this.$el.addClass('active'); + }else{ + this.$el.removeClass('active'); + } + if (this.editing) { + this.$('input').focus(); + } + }, + onSelect: function() { + this.selected = true; + this.render(); + }, + onUnselect: function() { + this.selected = false; + this.render(); + }, + selectSimple: function(e) { + e.preventDefault(); + if (e && !~[ this.el, this.$('a')[0], this.$('span')[0] ].indexOf(e.target)) { + return; + } + if (this.editing) return; + Backbone.history.navigate('tags/'+encodeURIComponent(this.model.get('name')), {trigger: true}); + }, + toggleActions: function() { + this.getUI('actionsMenu').toggleClass('open'); + }, + closeActions: function(e) { + if (this.editing || $.contains(this.getUI('actionsToggle')[0], e.target)) return; + this.getUI('actionsMenu').removeClass('open'); + }, + actionSelect: function(e) { + this.model.trigger('select', this.model); + }, + actionUnselect: function(e) { + this.model.trigger('unselect', this.model); + }, + actionDelete: function() { + this.model.destroy(); + }, + actionEdit: function() { + this.editing = true; + this.render(); + }, + actionSubmit: function() { + this.model.set('name', this.$('input').val()); + this.model.save(); + this.actionCancel(); + }, + actionCancel: function() { + this.editing = false; + this.render(); + } +}); diff --git a/js/views/TagsNavigation.js b/js/views/TagsNavigation.js new file mode 100644 index 000000000..0cdb65ee5 --- /dev/null +++ b/js/views/TagsNavigation.js @@ -0,0 +1,10 @@ +import Backbone from 'backbone'; +import TagView from '../views/TagsNavigationTag'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.CollectionView.extend({ + tagName: 'ul', + childView: TagView +}); diff --git a/js/views/TagsNavigationTag.js b/js/views/TagsNavigationTag.js new file mode 100644 index 000000000..415d5932b --- /dev/null +++ b/js/views/TagsNavigationTag.js @@ -0,0 +1,28 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/TagsNavigationTag.html'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + className: 'tag-nav-item', + tagName: 'li', + template: _.template(templateString), + events: { + 'click': 'open' + }, + initialize: function() { + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this); + }, + open: function(e) { + e.preventDefault(); + Backbone.history.navigate('tags/' + encodeURIComponent(this.model.get('name')), {trigger: true}); + }, + onNavigate: function(category, tags) { + this.$el.removeClass('active'); + if (category === 'tags' && ~tags.indexOf(this.model.get('name'))) { + this.$el.addClass('active'); + } + } +}); diff --git a/js/views/TagsSelection.js b/js/views/TagsSelection.js new file mode 100644 index 000000000..c824e9be8 --- /dev/null +++ b/js/views/TagsSelection.js @@ -0,0 +1,60 @@ +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tag from '../models/Tag'; +import Tags from '../models/Tags'; + +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; + +export default Marionette.View.extend({ + tagName: 'select', + template: _.template(''), + className: 'tags-selection', + events: { + 'select2:select': 'onAddByUser', + 'select2:unselect': 'onRemoveByUser' + }, + initialize: function(options) { + this.app = options.app; + this.selected = options.selected || new Tags; + this.selected.comparator = 'name'; + + this.listenTo(this.selected, 'add', this.onChangeByAlgo); + this.listenTo(this.selected, 'remove', this.onChangeByAlgo); + this.listenTo(this.selected, 'reset', this.onChangeByAlgo); + this.listenTo(this.collection, 'add', this.onAttach); + }, + onAttach: function() { + if (this.$el.hasClass('select2-hidden-accessible')) { + this.$el.select2('destroy'); + } + this.$el.select2({ + placeholder: t('bookmarks', 'Set tags'), + width: '100%', + tags: true, + multiple: true, + tokenSeparators: [',', ' '], + data: this.app.tags.pluck('name').map(function(name) { + return {id: name, text: name}; + }) + }) + .val(this.selected.pluck('name')) + .trigger('change'); + }, + onDetach: function() { + this.$el.select2('destroy'); + }, + onAddByUser: function(e) { + var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}); + this.selected.add(tag); + }, + onRemoveByUser: function(e) { + var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}); + this.selected.remove(tag); + }, + onChangeByAlgo: function(e) { + this.$el + .val(this.selected.pluck('name')) + .trigger('change'); + } +}); diff --git a/package.json b/package.json new file mode 100644 index 000000000..fc76d35fc --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "bookmarks", + "version": "0.10.1", + "main": "js/index.js", + "scripts": { + "build": "webpack" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nextcloud/bookmarks.git" + }, + "bugs": { + "url": "https://github.com/nextcloud/bookmarks/issues" + }, + "homepage": "https://github.com/nextcloud/bookmarks#readme", + "devDependencies": { + "html-loader": "^0.5.5", + "webpack": "^3.10.0" + }, + "dependencies": { + "backbone": "^1.3.3", + "backbone.marionette": "^3.5.1", + "jquery": "^3.3.1", + "nice-color-palettes": "^2.0.0", + "select2": "^4.0.6-rc.1", + "underscore": "^1.8.3" + } +} diff --git a/templates/addBookmarklet.php b/templates/addBookmarklet.php index 356a2f989..47339632c 100644 --- a/templates/addBookmarklet.php +++ b/templates/addBookmarklet.php @@ -1,64 +1,54 @@ <?php -OCP\Util::addscript('bookmarks', '3rdparty/tag-it'); -OCP\Util::addscript('bookmarks', 'bookmarklet'); -OCP\Util::addStyle('bookmarks', 'bookmarks'); -OCP\Util::addStyle('bookmarks', '3rdparty/jquery.tagit'); +OCP\Util::addscript('bookmarks', 'dist/bookmarklet.bundle'); +OCP\Util::addStyle('bookmarks', 'bookmarklet'); +style('bookmarks', 'select2'); $bookmarkExists = $_['bookmarkExists']; ?> <div id="bookmarklet_form"> <form class="addBm" action=""> - <script type="text/javascript" src="tag"></script> - - <h1 style="display: block; float: left"><?php p($l->t('Add a bookmark')); ?></h1> - <span style="display: inline; float: right"><div id="add_form_loading" style="margin: 3px;"><img src="<?php print_unescaped(image_path("bookmarks", "loading.gif")); ?>"> </div></span> - - <div style="color: red; clear: both; visibility: <?php - if ($bookmarkExists === false) { - print_unescaped('hidden'); - } - ?>"> - <strong> - <?php p($l->t('This URL is already bookmarked! Overwrite?')); ?> - </strong> - </div> - - <fieldset class="bm_desc"> - <ul> - <li> - <?php if($bookmarkExists !== false) { ?> + <div id="add_form_loading" style=""><span class="icon-loading"></span></div> + <h1><?php p($l->t('Add a bookmark')); ?></h1> + + <div class="bookmark-exists" style="display: <?php + if ($bookmarkExists === false) { + print_unescaped('none'); + } + ?>"> + <?php p($l->t('This URL is already bookmarked! Overwrite?')); ?> + </div> + <ul class="addBm"> + <li> + <?php if($bookmarkExists !== false) { ?> <input id="bookmarkID" type="hidden" class="hidden" value="<?php p($bookmarkExists); ?>" /> - <?php } ?> - <input id="title" type="text" name="title" class="title" value="<?php p($_['title']); ?>" - placeholder="<?php p($l->t('The title of the page')); ?>" /> - </li> - - <li> - <input id="url" type="text" name="url" class="url_input" value="<?php p($_['url']); ?>" - placeholder="<?php p($l->t('The address of the page')); ?>" /> - </li> - - <li> - <ul id="tags" class="tags" > - <?php foreach ($_['tags'] as $tag): ?> - <li><?php p($tag); ?></li> - <?php endforeach; ?> - </ul> - </li> - - <li> - <textarea id="description" name="description" class="desc" - placeholder="<?php p($l->t('Description of the page')); ?>"><?php p($_['description']); ?></textarea> - </li> - - <li> - <input type="submit" class="submit" value="<?php p($l->t("Save")); ?>" /> - <input type="hidden" class="record_id" value="" name="record_id" /> - <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>"> - </li> - - </ul> - - </fieldset> + <?php } ?> + <input id="title" type="text" name="title" class="title" value="<?php p($_['title']); ?>" + placeholder="<?php p($l->t('The title of the page')); ?>" /> + </li> + + <li> + <input id="url" type="text" name="url" class="url_input" value="<?php p($_['url']); ?>" + placeholder="<?php p($l->t('The address of the page')); ?>" /> + </li> + + <li> + <ul id="tags" class="tags" > + <?php foreach ($_['tags'] as $tag): ?> + <li><?php p($tag); ?></li> + <?php endforeach; ?> + </ul> + </li> + + <li> + <textarea id="description" name="description" class="desc" + placeholder="<?php p($l->t('Description of the page')); ?>"><?php p($_['description']); ?></textarea> + </li> + + <li> + <input type="submit" class="submit" value="<?php p($l->t("Save")); ?>" /> + <input type="hidden" class="record_id" value="" name="record_id" /> + <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>"> + </li> + </ul> </form> </div> diff --git a/templates/js_tpl.php b/templates/js_tpl.php deleted file mode 100644 index a78bc8894..000000000 --- a/templates/js_tpl.php +++ /dev/null @@ -1,64 +0,0 @@ -<script type="text/html" id="item_tmpl"> - <div class="bookmark_single" data-id="<&= id &>"> - <p class="bookmark_actions"> - <span class="bookmark_delete"> - <img class="svg" src="<?php print_unescaped(image_path("", "actions/delete.svg")); ?>" - title="<?php p($l->t('Delete')); ?>"> - </span>  - </p> - <p class="bookmark_title"> - <a href="<&= checkURL(encodeURI(url)) &>" target="_blank" class="bookmark_link" rel="nofollow noopener noreferrer"> - <&= escapeHTML(title == '' ? encodeURI(url) : title ) &> - </a> - <span class="bookmark_edit bookmark_edit_btn"> - <img class="svg" src="<?php print_unescaped(image_path("", "actions/rename.svg")); ?>" title="<?php p($l->t('Edit')); ?>"> - </span> - </p> - <span class="bookmark_desc"><&= escapeHTML(description)&> </span> - <span class="bookmark_date"><&= formatDate(added_date) &></span> - </div> -</script> - -<script type="text/html" id="item_form_tmpl"> - <div class="bookmark_single_form" data-id="<&= id &>"> - <form method="post" action="bookmark" > - <input type="hidden" name="record_id" value="<&= id &>" /> - <p class="bookmark_form_title"> - <input type="text" name="title" placeholder="<?php p($l->t('The title of the page')); ?>" - value="<&= escapeHTML(title) &>"/> - </p> - <p class="bookmark_form_url"> - <input type="text" name="url" placeholder="<?php p($l->t('The address of the page')); ?>" - value="<&= escapeHTML(url)&>"/> - </p> - <div class="bookmark_form_tags"><ul> - <& for ( var i = 0; i < tags.length; i++ ) { &> - <li><&= escapeHTML(tags[i]) &></li> - <& } &> - </ul></div> - <p class="bookmark_form_desc"> - <textarea name="description" placeholder="<?php p($l->t('Description of the page')); ?>" - ><&= escapeHTML(description) &></textarea> - </p> - <p class="bookmark_form_submit"> - <button class="reset" ><?php p($l->t('Cancel')); ?></button> - <input type="submit" class="primary" value="<?php p($l->t('Save')); ?>"> - </p> - </form> - </div> -</script> -<script type="text/html" id="tag_tmpl"> - <li><a href="" class="tag"><&= escapeHTML(tag) &></a> - <div class="tags_actions"> - <span class="tag_delete"> - <img class="svg" src="<?php print_unescaped(image_path("", "actions/delete.svg")); ?>" - title="<?php p($l->t('Delete')); ?>"> - </span> - <span class="tag_edit"> - <img class="svg" src="<?php print_unescaped(image_path("", "actions/rename.svg")); ?>" - title="<?php p($l->t('Edit')); ?>"> - </span> - <em><&= nbr &></em> - </div> - </li> -</script> diff --git a/templates/main.php b/templates/main.php index de7de6b29..00fd23e96 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,82 +1,6 @@ <?php -script('bookmarks', 'settings'); -script('bookmarks', 'bookmarks'); +script('bookmarks', 'dist/main.bundle'); style('bookmarks', 'bookmarks'); -script('bookmarks', '3rdparty/tag-it'); -script('bookmarks', '3rdparty/js_tpl'); -style('bookmarks', '3rdparty/jquery.tagit'); - -/** - * Copyright (c) 2011 Marvin Thomas Rabe <mrabe@marvinrabe.de> - * Copyright (c) 2011 Arthur Schiwon <blizzz@arthur-schiwon.de> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ -$bookmarkleturl = $_['bookmarkleturl']; -$bookmarkletscript = bookmarklet($bookmarkleturl); - -function bookmarklet($bookmarkleturl) { - $l = \OC::$server->getL10N('bookmarks'); - $defaults = \OC::$server->getThemingDefaults(); - $blet = "javascript:(function(){var a=window,b=document,c=encodeURIComponent,e=c(document.title),d=a.open('"; - $blet .= $bookmarkleturl; - $blet .= "?output=popup&url='+c(b.location)+'&title='+e,'bkmk_popup','left='+((a.screenX||a.screenLeft)+10)+',top='+((a.screenY||a.screenTop)+10)+',height=400px,width=550px,resizable=1,alwaysRaised=1');a.setTimeout(function(){d.focus()},300);})();"; - $help_msg = $l->t('Drag this to your browser bookmarks and click it, when you want to bookmark a webpage quickly:'); - $output = '<div id="bookmarklet_hint" class="bkm_hint">' . $help_msg . '</div><a class="button bookmarklet" href="' . $blet . '">' . $l->t('Add to ' . \OCP\Util::sanitizeHTML($defaults->getName())) . '</a>'; - return $output; -} +style('bookmarks', 'select2'); ?> - -<div id="app-navigation"> - <ul id="navigation-list"> - <li> - <form id="add_form"> - <input type="text" id="add_url" value="" placeholder="<?php p($l->t('Address')); ?>"/> - <button id="bookmark_add_submit" title="Add" class="icon-add"></button> - </form> - <p id="tag_filter" class="open"> - <input type="text" value="<?php if(isset($_['req_tag'])) p($_['req_tag']); else ""; ?>"/> - - - </p> - <input type="hidden" id="bookmarkFilterTag" value="<?php if(isset($_['req_tag'])) p($_['req_tag']); else ""; ?>" /> - <label id="tag_select_label"><?php p($l->t('Filterable Tags')); ?></label> - </li> - <li class="tag_list"> - </li> - </ul> - - <div id="app-settings"> - <div id="app-settings-header"> - <button class="settings-button generalsettings" data-apps-slide-toggle="#app-settings-content" tabindex="0"></button> - </div> - <div id="app-settings-content"> - - - <?php require 'settings.php'; ?> - </div> - </div> - -</div> -<div id="app-content"> - <div id="emptycontent" style="display: none;"> - <p class="title"><?php - p($l->t('You have no bookmarks')); - $embedded = true; - print_unescaped($bookmarkletscript); - ?></p> - <br/><br/> - - - <div class="bkm_hint"> - <a href="#" id="firstrun_setting"> - <?php p($l->t('You can also import a bookmark file')); ?> - </a></div> - </div> - <div class="bookmarks_list"></div> -</div> - -<?php -require 'js_tpl.php'; diff --git a/tests/bookmarkcontroller_test.php b/tests/bookmarkcontroller_test.php index 1fe87b150..4cd173b35 100644 --- a/tests/bookmarkcontroller_test.php +++ b/tests/bookmarkcontroller_test.php @@ -4,6 +4,7 @@ use OCA\Bookmarks\Controller\Rest\BookmarkController; use OCA\Bookmarks\Controller\Lib\Bookmarks; +use OCA\Bookmarks\Controller\Lib\LinkExplorer; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -43,18 +44,18 @@ protected function setUp() { $config = \OC::$server->getConfig(); $l = \OC::$server->getL10N('bookmarks'); - $clientService = \OC::$server->getHTTPClientService(); + $linkExplorer = \OC::$server->query(LinkExplorer::class); $event = \OC::$server->getEventDispatcher(); $logger = \OC::$server->getLogger(); - $this->libBookmarks = new Bookmarks($this->db, $config, $l, $clientService, $event, $logger); + $this->libBookmarks = new Bookmarks($this->db, $config, $l, $linkExplorer, $event, $logger); $this->controller = new BookmarkController("bookmarks", $this->request, $this->userid, $this->db, $l, $this->libBookmarks, $this->userManager); $this->publicController = new BookmarkController("bookmarks", $this->request, $this->otherUser, $this->db, $l, $this->libBookmarks, $this->userManager); } function setupBookmarks() { - $this->testSubjectPrivateBmId = $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", false); - $this->testSubjectPublicBmId = $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->testSubjectPrivateBmId = $this->libBookmarks->addBookmark($this->userid, "https://www.golem.de", "Golem", array("four"), "PublicNoTag", false); + $this->testSubjectPublicBmId = $this->libBookmarks->addBookmark($this->userid, "https://9gag.com", "9gag", array("two", "three"), "PublicTag", true); } function testPrivateRead() { @@ -63,7 +64,7 @@ function testPrivateRead() { $output = $this->controller->getSingleBookmark($this->testSubjectPublicBmId); $data = $output->getData(); $this->assertEquals('success', $data['status']); - $this->assertEquals("http://www.9gag.com", $data['item']['url']); + $this->assertEquals("https://9gag.com", $data['item']['url']); } function testPublicReadSuccess() { @@ -72,7 +73,7 @@ function testPublicReadSuccess() { $output = $this->publicController->getSingleBookmark($this->testSubjectPublicBmId, $this->userid); $data = $output->getData(); $this->assertEquals('success', $data['status']); - $this->assertEquals("http://www.9gag.com", $data['item']['url']); + $this->assertEquals("https://9gag.com", $data['item']['url']); } function testPublicReadFailure() { @@ -112,10 +113,10 @@ function testPublicQuery() { function testPublicCreate() { $this->cleanDB(); $this->setupBookmarks(); - $this->controller->newBookmark("http://www.heise.de", array("tags"=> array("four")), "Heise", true, "PublicNoTag"); + $this->controller->newBookmark("https://www.heise.de", array("tags"=> array("four")), "Heise", true, "PublicNoTag"); // the bookmark should exist - $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("http://www.heise.de", $this->userid)); + $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("https://www.heise.de", $this->userid)); // user should see this bookmark $output = $this->controller->getBookmarks(); $data = $output->getData(); @@ -130,10 +131,10 @@ function testPublicCreate() { function testPrivateCreate() { $this->cleanDB(); $this->setupBookmarks(); - $this->controller->newBookmark("http://www.heise.de", array("tags"=> array("four")), "Heise", false, "PublicNoTag"); + $this->controller->newBookmark("https://www.heise.de", array("tags"=> array("four")), "Heise", false, "PublicNoTag"); // the bookmark should exist - $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("http://www.heise.de", $this->userid)); + $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("https://www.heise.de", $this->userid)); // user should see this bookmark $output = $this->controller->getBookmarks(); @@ -149,7 +150,7 @@ function testPrivateCreate() { function testPrivateEditBookmark() { $this->cleanDB(); $this->setupBookmarks(); - $id = $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Golem", array("four"), "PublicNoTag", true); + $id = $this->libBookmarks->addBookmark($this->userid, "https://www.heise.de", "Golem", array("four"), "PublicNoTag", true); $this->controller->editBookmark($id, 'https://www.heise.de', null, '', true, $id, ''); @@ -160,10 +161,10 @@ function testPrivateEditBookmark() { function testPrivateDeleteBookmark() { $this->cleanDB(); $this->setupBookmarks(); - $id = $this->libBookmarks->addBookmark($this->userid, "http://www.google.com", "Heise", array("one", "two"), "PrivatTag", false); + $id = $this->libBookmarks->addBookmark($this->userid, "https://www.google.com", "Heise", array("one", "two"), "PrivatTag", false); $this->controller->deleteBookmark($id); - $this->assertFalse($this->libBookmarks->bookmarkExists("http://www.google.com", $this->userid)); + $this->assertFalse($this->libBookmarks->bookmarkExists("https://www.google.com", $this->userid)); } function testFindBookmarksEmptyTags() { @@ -179,28 +180,11 @@ public function testClick() { $this->cleanDB(); $this->setupBookmarks(); - $r = $this->publicController->clickBookmark('http://www.golem.de'); + $r = $this->publicController->clickBookmark('https://www.golem.de'); $this->assertInstanceOf(JSONResponse::class, $r); $this->assertSame(Http::STATUS_OK, $r->getStatus()); } - public function testClickEscapeParam() { - $this->cleanDB(); - $url1 = 'https://example.com/?foo%bar'; - $id1 = $this->libBookmarks->addBookmark($this->userid, $url1, "Example Domain 1"); - $id2 = $this->libBookmarks->addBookmark($this->userid, "https://example.com/?foo%bier", "Example Domain 2"); - - $r = $this->controller->clickBookmark('https://example.com/?foo%25bar'); - $this->assertInstanceOf(JSONResponse::class, $r); - $this->assertSame(Http::STATUS_OK, $r->getStatus()); - - $bm1 = $this->libBookmarks->findUniqueBookmark($id1, $this->userid); - $bm2 = $this->libBookmarks->findUniqueBookmark($id2, $this->userid); - - $this->assertSame(1, intval($bm1['clickcount'])); - $this->assertSame(0, intval($bm2['clickcount'])); - } - function cleanDB() { $query1 = \OC_DB::prepare('DELETE FROM *PREFIX*bookmarks'); $query1->execute(); diff --git a/tests/lib_bookmark_test.php b/tests/lib_bookmark_test.php index 89006b3bc..4e07209bb 100644 --- a/tests/lib_bookmark_test.php +++ b/tests/lib_bookmark_test.php @@ -2,12 +2,8 @@ namespace OCA\Bookmarks\Tests; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\RequestException; use OCA\Bookmarks\Controller\Lib\Bookmarks; -use OCP\Http\Client\IClient; -use OCP\Http\Client\IClientService; -use OCP\Http\Client\IResponse; +use OCA\Bookmarks\Controller\Lib\LinkExplorer; use OCP\User; /** @@ -30,10 +26,10 @@ protected function setUp() { $db = \OC::$server->getDatabaseConnection(); $config = \OC::$server->getConfig(); $l = \OC::$server->getL10N('bookmarks'); - $clientService = \OC::$server->getHTTPClientService(); + $linkExplorer = \OC::$server->query(LinkExplorer::class); $event = \OC::$server->getEventDispatcher(); $logger = \OC::$server->getLogger(); - $this->libBookmarks = new Bookmarks($db, $config, $l, $clientService, $event, $logger); + $this->libBookmarks = new Bookmarks($db, $config, $l, $linkExplorer, $event, $logger); $this->otherUser = "otheruser"; $this->userManager = \OC::$server->getUserManager(); @@ -57,7 +53,7 @@ function testFindBookmarks() { $this->libBookmarks->addBookmark($this->userid, "http://www.google.de", "Google", array("one"), "PrivateTwoTags", false); $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("one"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($this->userid, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $outputPrivate = $this->libBookmarks->findBookmarks($this->userid, 0, "", [], true, -1, false); $this->assertCount(5, $outputPrivate); $outputPrivateFiltered = $this->libBookmarks->findBookmarks($this->userid, 0, "", ["one"], true, -1, false); @@ -74,11 +70,11 @@ function testFindBookmarksSelectAndOrFilteredTags() { $this->libBookmarks->addBookmark($this->userid, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($this->userid, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $this->libBookmarks->addBookmark($secondUser, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($secondUser, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($secondUser, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $resultSetOne = $this->libBookmarks->findBookmarks($this->userid, 0, 'lastmodified', array('one', 'three'), true, -1, false, array('url', 'title', 'tags'), 'or'); $this->assertEquals(3, count($resultSetOne)); $resultOne = $resultSetOne[0]; @@ -116,11 +112,11 @@ function testRenameTag() { $this->libBookmarks->addBookmark($this->userid, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($this->userid, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $this->libBookmarks->addBookmark($secondUser, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($secondUser, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($secondUser, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $firstUserTags = $this->libBookmarks->findTags($this->userid); $this->assertTrue(in_array(['tag' => 'one', 'nbr' => 2], $firstUserTags)); @@ -156,11 +152,11 @@ function testDeleteTag() { $this->libBookmarks->addBookmark($this->userid, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($this->userid, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $this->libBookmarks->addBookmark($secondUser, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); $this->libBookmarks->addBookmark($secondUser, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($secondUser, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $this->libBookmarks->addBookmark($secondUser, "http://9gag.com", "9gag", array("two", "three"), "PublicTag", true); $firstUserTags = $this->libBookmarks->findTags($this->userid); $this->assertTrue(in_array(['tag' => 'one', 'nbr' => 2], $firstUserTags)); @@ -201,129 +197,30 @@ function testFindUniqueBookmark() { function testEditBookmark() { $this->cleanDB(); - $control_bm_id = $this->libBookmarks->addBookmark($this->userid, "http://www.golem.de", "Golem", array("four"), "PublicNoTag", true); - $this->libBookmarks->addBookmark($this->userid, "http://www.9gag.com", "9gag", array("two", "three"), "PublicTag", true); - $id = $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); - $this->libBookmarks->editBookmark($this->userid, $id, "http://www.google.de", "NewTitle", array("three", "four")); + $control_bm_id = $this->libBookmarks->addBookmark($this->userid, "https://www.golem.de", "Golem", array("four"), "PublicNoTag", true); + $this->libBookmarks->addBookmark($this->userid, "https://9gag.com", "9gag", array("two", "three"), "PublicTag", true); + $id = $this->libBookmarks->addBookmark($this->userid, "https://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); + $this->libBookmarks->editBookmark($this->userid, $id, "https://www.google.de", "NewTitle", array("three", "four")); $bookmark = $this->libBookmarks->findUniqueBookmark($id, $this->userid); $this->assertEquals("NewTitle", $bookmark['title']); - $this->assertEquals("http://www.google.de", $bookmark['url']); + $this->assertEquals("https://www.google.de", $bookmark['url']); $this->assertEquals(['four', 'three'], $bookmark['tags']); // Make sure nothing else changed $control_bookmark = $this->libBookmarks->findUniqueBookmark($control_bm_id, $this->userid); $this->assertEquals("Golem", $control_bookmark['title']); - $this->assertEquals("http://www.golem.de", $control_bookmark['url']); + $this->assertEquals("https://www.golem.de", $control_bookmark['url']); $this->assertEquals($control_bookmark['tags'], ['four']); } function testDeleteBookmark() { $this->cleanDB(); - $this->libBookmarks->addBookmark($this->userid, "http://www.google.de", "Google", array("one"), "PrivateNoTag", false); - $id = $this->libBookmarks->addBookmark($this->userid, "http://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); - $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("http://www.google.de", $this->userid)); - $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("http://www.heise.de", $this->userid)); + $this->libBookmarks->addBookmark($this->userid, "https://www.google.de", "Google", array("one"), "PrivateNoTag", false); + $id = $this->libBookmarks->addBookmark($this->userid, "https://www.heise.de", "Heise", array("one", "two"), "PrivatTag", false); + $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("https://www.google.de", $this->userid)); + $this->assertNotEquals(false, $this->libBookmarks->bookmarkExists("https://www.heise.de", $this->userid)); $this->libBookmarks->deleteUrl($this->userid, $id); - $this->assertFalse($this->libBookmarks->bookmarkExists("http://www.heise.de", $this->userid)); - } - - function testGetURLMetadata() { - $amazonResponse = $this->fetchMock(IResponse::class); - $amazonResponse->expects($this->once()) - ->method('getBody') - ->will($this->returnValue(file_get_contents(__DIR__ . '/res/amazonHtml.file'))); - $amazonResponse->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('')); - - $golemResponse = $this->fetchMock(IResponse::class); - $golemResponse->expects($this->once()) - ->method('getBody') - ->will($this->returnValue(file_get_contents(__DIR__ . '/res/golemHtml.file'))); - $golemResponse->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('text/html; charset=UTF-8')); - - $clientMock = $this->fetchMock(IClient::class); - $clientMock->expects($this->exactly(2)) - ->method('get') - ->will($this->returnCallback(function ($page) use($amazonResponse, $golemResponse) { - if($page === 'amazonHtml') { - return $amazonResponse; - } else if($page === 'golemHtml') { - return $golemResponse; - } - return null; - })); - - $clientServiceMock = $this->fetchMock(IClientService::class); - $clientServiceMock->expects($this->any()) - ->method('newClient') - ->will($this->returnValue($clientMock)); - - $this->registerHttpService($clientServiceMock); - - // ugly, but works - $db = \OC::$server->getDatabaseConnection(); - $config = \OC::$server->getConfig(); - $l = \OC::$server->getL10N('bookmarks'); - $clientService = \OC::$server->getHTTPClientService(); - $event = \OC::$server->getEventDispatcher(); - $logger = \OC::$server->getLogger(); - $this->libBookmarks = new Bookmarks($db, $config, $l, $clientService, $event, $logger); - - $metadataAmazon = $this->libBookmarks->getURLMetadata('amazonHtml'); - $this->assertTrue($metadataAmazon['url'] == 'amazonHtml'); - $this->assertTrue(strpos($metadataAmazon['title'], 'ΓΌ') !== false); - - $metadataGolem = $this->libBookmarks->getURLMetadata('golemHtml'); - $this->assertTrue($metadataGolem['url'] == 'golemHtml'); - $this->assertTrue(strpos($metadataGolem['title'], 'für') == false); - } - - /** - * @expectedException \GuzzleHttp\Exception\RequestException - */ - public function testGetURLMetaDataTryHarder() { - $url = 'https://yolo.swag/check'; - - $curlOptions = [ 'curl' => - [ CURLOPT_HTTPHEADER => ['Expect:'] ] - ]; - if(version_compare(ClientInterface::VERSION, '6') === -1) { - $options = ['config' => $curlOptions]; - } else { - $options = $curlOptions; - } - - $exceptionMock = $this->getMockBuilder(RequestException::class) - ->disableOriginalConstructor() - ->getMock(); - $clientMock = $this->fetchMock(IClient::class); - $clientMock->expects($this->exactly(2)) - ->method('get') - ->withConsecutive( - [$url, []], - [$url, $options] - ) - ->willThrowException($exceptionMock); - - $clientServiceMock = $this->fetchMock(IClientService::class); - $clientServiceMock->expects($this->any()) - ->method('newClient') - ->will($this->returnValue($clientMock)); - - // ugly, but works - $db = \OC::$server->getDatabaseConnection(); - $config = \OC::$server->getConfig(); - $l = \OC::$server->getL10N('bookmarks'); - $event = \OC::$server->getEventDispatcher(); - $logger = \OC::$server->getLogger(); - $this->libBookmarks = new Bookmarks($db, $config, $l, $clientServiceMock, $event, $logger); - - $this->libBookmarks->getURLMetadata($url); + $this->assertFalse($this->libBookmarks->bookmarkExists("https://www.heise.de", $this->userid)); } protected function tearDown() { diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..3333f55b0 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,25 @@ +var path = require('path') +module.exports = { + entry: { + main: './js/main.js', + bookmarklet: './js/bookmarklet.js' + }, + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'js/dist') + }, + module: { + rules: [ + { + test: /\.html$/, + use: [ { + loader: 'html-loader', + options: { + minimize: true, + exportAsEs6Default: true + } + }], + } + ] + } +};