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 @@
+.
+ *
+ */
+
+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 @@
+ $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 = $('').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 = $('').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($('').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('');
- }
- }
-
- // 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 ? '' : '').text(value);
-
- // Create tag.
- var tag = $('')
- .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
- .addClass(additionalClass)
- .append(label);
-
- // Button for removing the tag.
- var removeTagIcon = $('')
- .addClass('ui-icon ui-icon-close');
- var removeTag = $('\xd7') // \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('');
- }
-
- 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 + '' + escapeHTML(tags[i]) + ' ';
- }
- 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('' + taglist + '
');
- }
- 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('');
- 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 = $("").addClass('setting_error_list');
- console.log(data);
- $.each(data.data, function (index, item) {
- list.append($("").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 = $('').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 @@
+
+ <%- t('bookmarks', 'Add a Bookmark') %>
+
+
+
+
+
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 @@
+
+
+
+
<%- t('bookmarks', 'Tags') %>
+
+
+
+
+
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 @@
+
+
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 @@
+
+
+
+ <%- t('bookmarks', 'Saved') %>
+
+
+
+
+<%- title %>
+Link
+
+<%- t('bookmarks', 'Tags') %>
+
+<%- t('bookmarks', 'Description') %>
+<%- description.trim()? description : t('bookmarks', 'Add a description...') %>
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+<%- t('bookmarks', 'No bookmarks here.') %>
+<%- t('bookmarks', 'There are no bookmarks available for this query. Try changing your filter or add some using the menu entry on the left.') %>
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 @@
+
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 @@
+
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 @@
+
+ <%- t('bookmarks', 'All bookmarks') %>
+
+
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 @@
+
+
+
<%- t('bookmarks', 'Bookmarklet') %>
+
<%- t('bookmarks', 'Drag this to your browser bookmarks and click it, when you want to bookmark a webpage quickly:') %>
+
<%- t('bookmarks', 'Add to {instanceName} ', {instanceName: oc_defaults.name}) %>
+
<%- t('bookmarks', 'Import & Export') %>
+
+
+
+
+
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 @@
+
+ <%- name %>
+
+
+
+ - <%- count > 999 ? "999+" :count %>
+
+
+
+
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 @@
+
+
+
+
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 @@
+<%- name %>
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 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 = $('').addClass('setting_error_list');
+ console.log(data);
+ $.each(data.data, function (index, item) {
+ list.append($('').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 @@
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 @@
-
-
-
-
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 @@
- * Copyright (c) 2011 Arthur Schiwon
- * 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 = '' . $help_msg . '
' . $l->t('Add to ' . \OCP\Util::sanitizeHTML($defaults->getName())) . '';
- return $output;
-}
+style('bookmarks', 'select2');
?>
-
-
-
-
-
t('You have no bookmarks'));
- $embedded = true;
- print_unescaped($bookmarkletscript);
- ?>
-
-
-
-
-
-
-
-
-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
+ }
+ }],
+ }
+ ]
+ }
+};