Compare commits

...

58 Commits

Author SHA1 Message Date
Tobias Reich
727c74c294 Fixed the search 2016-10-25 18:49:25 +02:00
Tobias Reich
635b31eb0b Always create new album in current album 2016-10-25 18:40:00 +02:00
Tobias Reich
a0abb1a460 Rebuild 2016-10-25 18:34:25 +02:00
Tobias Reich
f460569bdc Merge branch 'develop' into feature/subalbums
# Conflicts:
#	src/package.json
#	src/scripts/lychee.js
2016-10-25 18:32:14 +02:00
Tobias Reich
a2def81b52 Just a small code adjustment 2016-08-30 19:14:25 +02:00
Tobias Reich
ab3a5a4d63 Merge pull request #611 from hrniels/multiselect
Improvements to selection feature.
2016-08-30 19:06:18 +02:00
Nils Asmussen
3f7f2ffcd1 Disallow to select a mixture of albums and photos.
This commit uses a different approach than before. Now we prevent the
selection and display an error to the user to explain him whats going
on. Thus, we do no longer need to check in contextMenu whether a mixture
of photos and albums has been selected.

This fixes the open bug in #603.
2016-08-28 16:51:39 +02:00
Nils Asmussen
a765b5ee06 Support metaKey and ctrlKey for selection.
MetaKey is the windows key on Windows/Linux. Using only that for
selecting items is really confusing, because AFAIK no existing
application does that. With this commit, ctrl can be used for selecting
items as well.
2016-08-28 16:25:57 +02:00
Nils Asmussen
425749eb85 Fixed condition in {album,photo}ContextMenu.
This has been introduced in d2c7cab3.
2016-08-28 16:25:57 +02:00
Tobias Reich
7002e039b3 Merge branch 'develop' into feature/subalbums 2016-08-28 13:17:42 +02:00
Tobias Reich
0cb50e5090 Updated keyboard shortcuts doc as we are now using the meta key to trigger selection 2016-08-28 13:06:00 +02:00
Tobias Reich
1fc3ca05cc Merge branch 'develop' into feature/subalbums
# Conflicts:
#	src/package.json
#	src/scripts/lychee.js
2016-08-28 13:01:37 +02:00
Tobias Reich
bdd2aca6f2 Custom function to check if id is already selected
Replaces all hard-to-read $.inArray and -1 checks
2016-08-28 12:59:16 +02:00
Tobias Reich
d2c7cab3e3 Prevent smart albums to be part of a selection #603
This bug has been introduced by #603
2016-08-28 12:51:24 +02:00
Tobias Reich
ff5637a7da Use cmd on macOS and ctrl on Windows to trigger multi select of individual items
Using https://api.jquery.com/event.metakey/
2016-08-28 12:29:21 +02:00
Tobias Reich
4b08f7e6f0 Trim whitespace on save 2016-08-28 12:21:34 +02:00
Tobias Reich
1bf8f8d2f8 Merge pull request #603 from hrniels/multiselect-contextmenu
Don't show context menu on finished multiselect.
2016-08-27 18:20:50 +02:00
Nils Asmussen
774fda3a7c Don't show context menu on finished multiselect.
Previously, we showed the context menu if a multiselect was finished
right away. Since we can select items individually now, it makes more
sense to just select the items as well and show the context menu later
via right mouse button or similar.

A multiselect can only add items to the selection, because that seems to
be easier to use. As before, shift+click can be used to deselect items.

The selection is cleared if the multiselect rectangle is empty and shift
is not pressed. This allows the user to click somewhere on the free area
to clear the selection again. If the shift key is pressed, it is not
cleared to prevent that a user triggers that by accident if he misses a
photo/album.
2016-08-22 22:18:19 +02:00
Tobias Reich
e64e8ae621 Removed select all
Makes no sense anymore as albums can contain albums and photos and you can only select one type => Error when using select all
2016-08-22 14:53:34 +02:00
Tobias Reich
9d5e9a4137 Code style adjustment 2016-08-22 14:20:24 +02:00
Tobias Reich
5c18fbeed8 Use shiftkey for multiselect
The only key that works on every OS (hopefully)
2016-08-22 14:19:20 +02:00
Tobias Reich
4c58ae0733 Code style adjustment 2016-08-22 14:17:35 +02:00
Tobias Reich
993738af49 Merge branch 'multiselect' of https://github.com/hrniels/Lychee into hrniels-multiselect 2016-08-22 14:10:18 +02:00
Tobias Reich
7ba384e70a Updated to minor version 2016-08-22 14:08:03 +02:00
Tobias Reich
9548f43b06 Use new album.isSmartID to check if it's a smart album 2016-08-22 14:06:54 +02:00
Tobias Reich
5bbac75481 Merge branch 'develop' into feature/subalbums 2016-08-22 14:01:10 +02:00
Nils Asmussen
7ebe2a1aa0 Clear selected items when multiselect starts. 2016-08-15 22:03:03 +02:00
Nils Asmussen
ea073b4324 Use outline for selected items, not border. 2016-08-15 22:02:03 +02:00
Tobias Reich
55ae8a4be2 Merge pull request #591 from hrniels/subalbums
Some cleanup, fixes and a new feature for subalbums.
2016-08-15 14:45:41 +02:00
Nils Asmussen
0746acda79 Allow to select photos/albums individually (fixes #339).
This commit adds support for selecting photos and albums individually
via ctrl+click. For that, the event handlers in init.js call to
multiselect now, which collects the photo/album ids and calls the
appropriate method of contextMenu in the end.
2016-08-14 20:15:20 +02:00
Nils Asmussen
3040ab1d62 Improved display of root album in move operation.
Root is now shown as the parent of all albums with parent=0.
2016-08-13 16:50:05 +02:00
Nils Asmussen
5ee34c50a4 Properly redirect after album {delete,move,merge}.
That is, we don't redirect to the albums view, but to the current album
or the parent album if the current one has been deleted.
2016-08-13 16:03:37 +02:00
Nils Asmussen
7126354d7a Hide "Photos" divider if photos are deleted.
If we have deleted all photos from an album, we need to remove the
divider between albums and photos.
2016-08-13 16:01:41 +02:00
Nils Asmussen
58d3ad727d Hide download button only w/o photos and subalbums.
Previously, an album with subalbums, but without photos, was hiding
the download button.
2016-08-07 14:47:45 +02:00
Nils Asmussen
20c194f00c Always show merge&move operations in context menu.
Previously, we calculated whether there are at least two albums so that
we can actually perform the operation. Due to subalbums this simple
check is not sufficient anymore. Since the rules are more complicated
and we don't have all albums loaded at that point to calculate it
properly, this commit removes the check entirely and always displays
the operations.

The only disadvantage is that if an installation really has just one
album (should rarely happen), the user still sees the merge operation
(not merge all), which results in an album-list with one disabled album.
I think this is okay.
2016-08-04 07:35:49 +02:00
Nils Asmussen
b91759e6c1 Added a move album operation.
Since albums can have subalbums now, it makes sense to not only merge
albums, but also move them.
2016-08-03 23:04:47 +02:00
Nils Asmussen
9b5328d888 Don't validate merge request on server.
Since we don't show the user invalid merge requests to send to the
server, the user would have to handcraft such requests. Only admins can
merge albums, which we trust anyway. So, let's remove this check.
2016-08-03 22:58:17 +02:00
Nils Asmussen
adaa23d39f Syntax adjustments in album.js. 2016-08-03 21:59:02 +02:00
Nils Asmussen
96bd6b240b Better highlighting of selected items.
Especially for albums, it is difficult to which ones are
chosen for an operation, because in contrast to photos, the
only change is the thin blue border.

This commit adds a new CSS class, that is set when items
have been selected. Currently, it sets a 2px border instead
of 1px.
2016-08-01 15:33:21 +02:00
Tobias Reich
61a62421f4 Check if album.json exists
album.json is null when in the overview. album.getParent() was unusable in this case.
2016-07-31 16:17:41 +02:00
Tobias Reich
066fc90056 Syntax adjustment for contextMenu.js 2016-07-31 16:12:14 +02:00
Tobias Reich
2b698ca024 Removed disabled context menu item class
Removed unsed disabled class from context menu now where we are using the disabled option of basicContext
2016-07-31 16:11:59 +02:00
Tobias Reich
2da195f925 Use disabled functionality of basicContext 2016-07-31 16:03:08 +02:00
Tobias Reich
0c7d95fb15 Syntax adjustments 2016-07-31 15:53:34 +02:00
Tobias Reich
1020b0d1dc Merge branch 'subalbums' of https://github.com/hrniels/Lychee into feature/subalbums 2016-07-31 15:26:34 +02:00
Nils Asmussen
8b37d122b9 Fixed bug in countSubAlbums. 2016-07-30 15:11:49 +02:00
Nils Asmussen
f7e31c2be7 Fixed bug in album.setTitle. 2016-07-30 15:04:03 +02:00
Nils Asmussen
ffef7e3d8f Fixed bug in multiselect, introduced by 6d4df5f. 2016-07-30 14:51:24 +02:00
Nils Asmussen
daf7d37c84 Prevent album merge errors on client-side.
Previously, we allowed that and reported an error in this case. Now,
the user can no longer select a parent album for merging it into a
subalbum. It is still checked on the server, though.
2016-07-30 12:04:07 +02:00
Nils Asmussen
cdd130b243 Let the user know that deleting an album deletes subalbums, too. 2016-07-30 11:20:30 +02:00
Nils Asmussen
d121e2c4f6 Go to parent album on ESC. 2016-07-30 00:00:54 +02:00
Nils Asmussen
55247e4f56 Load album view just once. 2016-07-29 23:56:45 +02:00
Nils Asmussen
6feae99bf1 Include subalbums in setPublic.
That means, when making an album public or private, all subalbums
are made public/private as well. This can be changed afterwards
by performing the opposite operation for a subalbum.

I think this is the better default, since most people probably
won't have public albums with private subalbums.
2016-07-29 22:36:41 +02:00
Nils Asmussen
1b6c74bcb6 Merge albums including subalbums. 2016-07-29 22:36:41 +02:00
Nils Asmussen
eddb666b5a Delete albums including subalbums. 2016-07-29 22:36:41 +02:00
Nils Asmussen
212b241d0e Include subalbums in album archives. 2016-07-29 22:36:41 +02:00
Nils Asmussen
ef9040870a Added basic subalbum support.
That is, albums can now contain other albums, which are shown at
the top of the album view. This required some changes to album.js
and the contextMenu.js, because this view contains now both
photos and albums.

The contextMenu on this view has been kept simple by requiring
the user to select either only albums or only photos, but not
a mixture of both.

This feature required a database change, so that the version
has been updated to 3.1.3.

At the moment, album and photo operations (make public, download,
delete, merge) are still "flat", i.e. don't respect the album
hierarchy.
2016-07-29 22:36:41 +02:00
Nils Asmussen
6d4df5f6b7 Introduced album.isSmartID. 2016-07-29 22:36:41 +02:00
23 changed files with 826 additions and 298 deletions

BIN
dist/main.css vendored

Binary file not shown.

BIN
dist/main.js vendored

Binary file not shown.

BIN
dist/view.js vendored Executable file → Normal file

Binary file not shown.

View File

@ -14,8 +14,8 @@ The following keys and shortcuts can be used in Lychee.
| Key | Action |
|:-----------|:------------|
| `s` or `f` | Search |
| `cmd`+`a` | Select all albums |
| `ctrl`+`a` | Select all albums |
| `meta-key`+`left-click` | Select album individually |
| `right-click` | Open context-menu |
### Album
| Key | Action |
@ -25,8 +25,8 @@ The following keys and shortcuts can be used in Lychee.
| `i` | Show information |
| `cmd`+`backspace` | Delete album |
| `ctrl`+`backspace` | Delete album |
| `cmd`+`a` | Select all photos |
| `ctrl`+`a` | Select all photos |
| `meta-key`+`left-click` | Select album/photo individually |
| `right-click` | Open context-menu |
### Photo
| Key | Action |

View File

@ -28,6 +28,7 @@ final class Admin extends Access {
case 'Album::setPublic': self::setAlbumPublicAction(); break;
case 'Album::delete': self::deleteAlbumAction(); break;
case 'Album::merge': self::mergeAlbumsAction(); break;
case 'Album::move': self::moveAlbumsAction(); break;
// Photo functions
case 'Photo::get': self::getPhotoAction(); break;
@ -72,8 +73,10 @@ final class Admin extends Access {
private static function getAlbumsAction() {
Validator::required(isset($_POST['parent']), __METHOD__);
$albums = new Albums();
Response::json($albums->get(false));
Response::json($albums->get(false, $_POST['parent']));
}
@ -90,10 +93,10 @@ final class Admin extends Access {
private static function addAlbumAction() {
Validator::required(isset($_POST['title']), __METHOD__);
Validator::required(isset($_POST['title'], $_POST['parent']), __METHOD__);
$album = new Album(null);
Response::json($album->add($_POST['title']), JSON_NUMERIC_CHECK);
Response::json($album->add($_POST['title'], $_POST['parent']), JSON_NUMERIC_CHECK);
}
@ -141,6 +144,14 @@ final class Admin extends Access {
}
private static function moveAlbumsAction() {
Validator::required(isset($_POST['albumIDs']), __METHOD__);
$album = new Album($_POST['albumIDs']);
Response::json($album->move());
}
// Photo functions
private static function getPhotoAction() {

View File

@ -44,8 +44,10 @@ final class Guest extends Access {
private static function getAlbumsAction() {
Validator::required(isset($_POST['parent']), __METHOD__);
$albums = new Albums();
Response::json($albums->get(true));
Response::json($albums->get(true, $_POST['parent']));
}

View File

@ -23,7 +23,7 @@ final class Album {
/**
* @return string|false ID of the created album.
*/
public function add($title = 'Untitled') {
public function add($title = 'Untitled', $parent = 0) {
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
@ -35,7 +35,7 @@ final class Album {
$visible = 1;
// Database
$query = Database::prepare(Database::get(), "INSERT INTO ? (id, title, sysstamp, public, visible) VALUES ('?', '?', '?', '?', '?')", array(LYCHEE_TABLE_ALBUMS, $id, $title, $sysstamp, $public, $visible));
$query = Database::prepare(Database::get(), "INSERT INTO ? (id, title, sysstamp, public, visible, parent) VALUES ('?', '?', '?', '?', '?', '?')", array(LYCHEE_TABLE_ALBUMS, $id, $title, $sysstamp, $public, $visible, $parent));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
// Call plugins
@ -69,6 +69,7 @@ final class Album {
if (isset($data['description'])) $album['description'] = $data['description'];
if (isset($data['visible'])) $album['visible'] = $data['visible'];
if (isset($data['downloadable'])) $album['downloadable'] = $data['downloadable'];
if (isset($data['parent'])) $album['parent'] = $data['parent'];
// Parse date
$album['sysdate'] = strftime('%B %Y', $data['sysstamp']);
@ -79,6 +80,7 @@ final class Album {
// Parse thumbs or set default value
$album['thumbs'] = (isset($data['thumbs']) ? explode(',', $data['thumbs']) : array());
return $album;
}
@ -193,12 +195,6 @@ final class Album {
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
// Illicit chars
$badChars = array_merge(
array_map('chr', range(0,31)),
array("<", ">", ":", '"', "/", "\\", "|", "?", "*")
);
// Photos query
switch($this->albumIDs) {
case 's':
@ -214,34 +210,34 @@ final class Album {
$zipTitle = 'Recent';
break;
default:
$photos = Database::prepare(Database::get(), "SELECT title, url FROM ? WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
$zipTitle = 'Unsorted';
}
// Get title from database when album is not a SmartAlbum
if ($this->albumIDs!=0&&is_numeric($this->albumIDs)) {
// Get title from database when album is not a SmartAlbum
if ($this->albumIDs!=0 && is_numeric($this->albumIDs)) {
$query = Database::prepare(Database::get(), "SELECT title FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
$album = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
$query = Database::prepare(Database::get(), "SELECT title FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
$album = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
if ($album===false) return false;
if ($album===false) return false;
// Get album object
$album = $album->fetch_object();
// Get album object
$album = $album->fetch_object();
// Album not found?
if ($album===null) {
Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified album');
return false;
}
// Album not found?
if ($album===null) {
Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified album');
return false;
}
// Set title
$zipTitle = $album->title;
// Set title
$zipTitle = $album->title;
}
break;
}
// Escape title
$zipTitle = str_replace($badChars, '', $zipTitle);
$zipTitle = $this->cleanZipName($zipTitle);
$filename = LYCHEE_DATA . $zipTitle . '.zip';
@ -252,56 +248,16 @@ final class Album {
return false;
}
// Execute query
$photos = Database::execute(Database::get(), $photos, __METHOD__, __LINE__);
if ($album===null) return false;
// Check if album empty
if ($photos->num_rows==0) {
Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create ZipArchive without images');
return false;
}
// Parse each path
$files = array();
while ($photo = $photos->fetch_object()) {
// Parse url
$photo->url = LYCHEE_UPLOADS_BIG . $photo->url;
// Parse title
$photo->title = str_replace($badChars, '', $photo->title);
if (!isset($photo->title)||$photo->title==='') $photo->title = 'Untitled';
// Check if readable
if (!@is_readable($photo->url)) continue;
// Get extension of image
$extension = getExtension($photo->url, false);
// Set title for photo
$zipFileName = $zipTitle . '/' . $photo->title . $extension;
// Check for duplicates
if (!empty($files)) {
$i = 1;
while (in_array($zipFileName, $files)) {
// Set new title for photo
$zipFileName = $zipTitle . '/' . $photo->title . '-' . $i . $extension;
$i++;
}
}
// Add to array
$files[] = $zipFileName;
// Add photo to zip
$zip->addFile($photo->url, $zipFileName);
// Add photos to zip
switch($this->albumIDs) {
case 's':
case 'f':
case 'r':
$this->addPhotosToZip($zip, $zipTitle, $photos);
break;
default:
$this->addAlbumToZip($zip, $zipTitle, $this->albumIDs);
break;
}
// Finish zip
@ -323,6 +279,84 @@ final class Album {
}
private function cleanZipName($name) {
// Illicit chars
$badChars = array_merge(
array_map('chr', range(0,31)),
array("<", ">", ":", '"', "/", "\\", "|", "?", "*")
);
return str_replace($badChars, '', $name);
}
private function addAlbumToZip($zip, $path, $albumID) {
// Fetch album title
$photos = Database::prepare(Database::get(), "SELECT title, url FROM ? WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $albumID));
$this->addPhotosToZip($zip, $path, $photos);
// Fetch subalbums
$query = Database::prepare(Database::get(), "SELECT id, title FROM ? WHERE parent = '?'", array(LYCHEE_TABLE_ALBUMS, $albumID));
$albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
// Add them recursively
while($album = $albums->fetch_assoc()) {
$this->addAlbumToZip($zip, $path . '/' . $this->cleanZipName($album['title']), $album['id']);
}
}
private function addPhotosToZip($zip, $path, $query) {
// Execute query
$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
// Parse each path
$files = array();
while ($photo = $photos->fetch_object()) {
// Parse url
$photo->url = LYCHEE_UPLOADS_BIG . $photo->url;
// Parse title
$photo->title = $this->cleanZipName($photo->title);
if (!isset($photo->title)||$photo->title==='') $photo->title = 'Untitled';
// Check if readable
if (!@is_readable($photo->url)) continue;
// Get extension of image
$extension = getExtension($photo->url, false);
// Set title for photo
$zipFileName = $path . '/' . $photo->title . $extension;
// Check for duplicates
if (!empty($files)) {
$i = 1;
while (in_array($zipFileName, $files)) {
// Set new title for photo
$zipFileName = $path . '/' . $photo->title . '-' . $i . $extension;
$i++;
}
}
// Add to array
$files[] = $zipFileName;
// Add photo to zip
$zip->addFile($photo->url, $zipFileName);
}
}
/**
* @return boolean Returns true when successful.
*/
@ -441,6 +475,31 @@ final class Album {
}
private function getSubAlbums($albumID) {
$query = Database::prepare(Database::get(), "SELECT id FROM ? WHERE parent = '?'", array(LYCHEE_TABLE_ALBUMS, $albumID));
$albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
$ids = array();
while($album = $albums->fetch_assoc()) {
$ids = array_merge($ids, array($album['id']), $this->getSubAlbums($album['id']));
}
return $ids;
}
private function addSubAlbumIDs($ids) {
$res = array();
foreach(explode(',', $ids) as $id)
$res = array_merge($res, array($id), $this->getSubAlbums($id));
return implode(',', $res);
}
/**
* @return boolean Returns true when successful.
*/
@ -457,8 +516,11 @@ final class Album {
$visible = ($visible==='1' ? 1 : 0);
$downloadable = ($downloadable==='1' ? 1 : 0);
// Get all album ids, including subalbums
$ids = $this->addSubAlbumIDs($this->albumIDs);
// Set public
$query = Database::prepare(Database::get(), "UPDATE ? SET public = '?', visible = '?', downloadable = '?', password = NULL WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $public, $visible, $downloadable, $this->albumIDs));
$query = Database::prepare(Database::get(), "UPDATE ? SET public = '?', visible = '?', downloadable = '?', password = NULL WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $public, $visible, $downloadable, $ids));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
if ($result===false) return false;
@ -466,7 +528,7 @@ final class Album {
// Reset permissions for photos
if ($public===1) {
$query = Database::prepare(Database::get(), "UPDATE ? SET public = 0 WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
$query = Database::prepare(Database::get(), "UPDATE ? SET public = 0 WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $ids));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
if ($result===false) return false;
@ -575,6 +637,7 @@ final class Album {
$albumID = array_splice($albumIDs, 0, 1);
$albumID = $albumID[0];
// Move photos
$query = Database::prepare(Database::get(), "UPDATE ? SET album = ? WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $albumID, $this->albumIDs));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
@ -584,6 +647,13 @@ final class Album {
// Convert to string
$filteredIDs = implode(',', $albumIDs);
// Move subalbums
$query = Database::prepare(Database::get(), "UPDATE ? SET parent = ? WHERE parent IN (?)", array(LYCHEE_TABLE_ALBUMS, $albumID, $filteredIDs));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
if ($result===false) return false;
// Delete other albums
$query = Database::prepare(Database::get(), "DELETE FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $filteredIDs));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
@ -595,6 +665,40 @@ final class Album {
}
/**
* @return boolean Returns true when successful.
*/
public function move() {
// Check dependencies
Validator::required(isset($this->albumIDs), __METHOD__);
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
// Convert to array
$albumIDs = explode(',', $this->albumIDs);
// Get first albumID
$albumID = array_splice($albumIDs, 0, 1);
$albumID = $albumID[0];
// $albumIDs contains all IDs without the first albumID
// Convert to string
$filteredIDs = implode(',', $albumIDs);
// Move albums
$query = Database::prepare(Database::get(), "UPDATE ? SET parent = ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $albumID, $filteredIDs));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
// Call plugins
Plugins::get()->activate(__METHOD__, 1, func_get_args());
if ($result===false) return false;
return true;
}
/**
* @return boolean Returns true when successful.
*/
@ -606,11 +710,14 @@ final class Album {
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
// Get all album ids, including subalbums
$ids = $this->addSubAlbumIDs($this->albumIDs);
// Init vars
$photoIDs = array();
// Execute query
$query = Database::prepare(Database::get(), "SELECT id FROM ? WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
$query = Database::prepare(Database::get(), "SELECT id FROM ? WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $ids));
$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
if ($photos===false) return false;
@ -631,7 +738,7 @@ final class Album {
}
// Delete albums
$query = Database::prepare(Database::get(), "DELETE FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
$query = Database::prepare(Database::get(), "DELETE FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $ids));
$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);
// Call plugins

View File

@ -16,7 +16,7 @@ final class Albums {
/**
* @return array|false Returns an array of albums or false on failure.
*/
public function get($public = true) {
public function get($public = true, $parent = 0) {
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
@ -32,8 +32,8 @@ final class Albums {
if ($public===false) $return['smartalbums'] = $this->getSmartAlbums();
// Albums query
if ($public===false) $query = Database::prepare(Database::get(), 'SELECT id, title, public, sysstamp, password FROM ? ' . Settings::get()['sortingAlbums'], array(LYCHEE_TABLE_ALBUMS));
else $query = Database::prepare(Database::get(), 'SELECT id, title, public, sysstamp, password FROM ? WHERE public = 1 AND visible <> 0 ' . Settings::get()['sortingAlbums'], array(LYCHEE_TABLE_ALBUMS));
if ($public===false) $query = Database::prepare(Database::get(), "SELECT id, title, public, sysstamp, password, parent FROM ? " . ($parent != -1 ? "WHERE parent = '?' " : "") . Settings::get()['sortingAlbums'], array(LYCHEE_TABLE_ALBUMS, $parent));
else $query = Database::prepare(Database::get(), "SELECT id, title, public, sysstamp, password, parent FROM ? " . ($parent != -1 ? "WHERE parent = '?' " : "") . " AND public = 1 AND visible <> 0 " . Settings::get()['sortingAlbums'], array(LYCHEE_TABLE_ALBUMS, $parent));
// Execute query
$albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

View File

@ -15,7 +15,8 @@ final class Database {
'030001', // 3.0.1
'030003', // 3.0.3
'030100', // 3.1.0
'030102' // 3.1.2
'030102', // 3.1.2
'030200' // 3.2.0
);
/**

View File

@ -0,0 +1,29 @@
<?php
/**
* Update to version 3.2.0
*/
use Lychee\Modules\Database;
use Lychee\Modules\Response;
// Add parent field to albums
$query = Database::prepare($connection, "SELECT `parent` FROM `?` LIMIT 1", array(LYCHEE_TABLE_ALBUMS));
$result = Database::execute($connection, $query, "update_030200", __LINE__);
if ($result===false) {
$query = Database::prepare($connection, "ALTER TABLE `?` ADD `parent` BIGINT(14) NOT NULL DEFAULT 0", array(LYCHEE_TABLE_ALBUMS));
$result = Database::execute($connection, $query, "update_030200", __LINE__);
if ($result===false) {
Log::error($database, 'update_030200', __LINE__, 'Could not update database (' . $database->error . ')');
return false;
}
}
// Set version
// if (Database::setVersion($connection, '030200')===false) Response::error('Could not update version of database!');
?>

View File

@ -1,6 +1,6 @@
{
"name": "Lychee",
"version": "3.1.5",
"version": "3.2.0",
"description": "Self-hosted photo-management done right.",
"authors": "Tobias Reich <tobias@electerious.com>",
"license": "MIT",

View File

@ -2,9 +2,36 @@
* @description Takes care of every action an album can handle and execute.
*/
const buildAlbumOptions = function(albums, select, parent = 0, layer = 0) {
var cmbxOptions = ''
for (i in albums) {
if (albums[i].parent==parent) {
let title = (layer>0 ? '&nbsp;&nbsp;'.repeat(layer - 1) + '└ ' : '') + albums[i].title
let selected = select==albums[i].id ? ' selected="selected"' : ''
cmbxOptions += `<option${ selected } value='${ albums[i].id }'>${ title }</option>`
cmbxOptions += buildAlbumOptions(albums, select, albums[i].id, layer + 1)
}
}
return cmbxOptions
}
album = {
json: null
json: null,
subjson: null
}
album.isSmartID = function(id) {
return (id==='0' || id==='f' || id==='s' || id==='r')
}
@ -13,19 +40,30 @@ album.getID = function() {
let id = null
let isID = (id) => {
if (id==='0' || id==='f' || id==='s' || id==='r') return true
if (album.isSmartID(id)===true) return true
return $.isNumeric(id)
}
if (photo.json) id = photo.json.album
else if (album.json) id = album.json.id
// Search
if (isID(id)===false) id = $('.album:hover, .album.active').attr('data-id')
if (isID(id)===false) id = $('.photo:hover, .photo.active').attr('data-album-id')
if (isID(id)===false) {
if (photo.json) id = photo.json.album
else if (album.json) id = album.json.id
}
if (isID(id)===true) return id
else return false
return false
}
album.getParent = function() {
if (album.json==null || album.isSmartID(album.json.id)===true || album.json.parent==0) return ''
return album.json.parent
}
@ -78,17 +116,45 @@ album.load = function(albumID, refresh = false) {
setTimeout(() => {
view.album.init()
let finish = function() {
view.album.init()
if (refresh===false) {
lychee.animate(lychee.content, 'contentZoomIn')
header.setMode('album')
}
}
if (album.isSmartID(albumID)===false) {
params = {
parent: albumID
}
api.post('Albums::get', params, function(data) {
let waitTime = 0
album.subjson = data
// Calculate delay
let durationTime = (new Date().getTime() - startTime)
if (durationTime>300) waitTime = 0
else waitTime = 300 - durationTime
setTimeout(finish, waitTime)
})
} else {
finish()
if (refresh===false) {
lychee.animate(lychee.content, 'contentZoomIn')
header.setMode('album')
}
}, waitTime)
})
})
}
@ -99,18 +165,20 @@ album.parse = function() {
}
album.add = function() {
album.add = function(albumID = 0) {
const action = function(data) {
let title = data.title
let parent = albumID
const isNumber = (n) => (!isNaN(parseFloat(n)) && isFinite(n))
basicModal.close()
let params = {
title
title,
parent
}
api.post('Album::add', params, function(data) {
@ -126,18 +194,26 @@ album.add = function() {
}
basicModal.show({
body: `<p>Enter a title for the new album: <input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='Untitled'></p>`,
buttons: {
action: {
title: 'Create Album',
fn: action
},
cancel: {
title: 'Cancel',
fn: basicModal.close
api.post('Albums::get', { parent: -1 }, function(data) {
let msg = `
<p>Enter a title for the new album: <input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='Untitled'></p>
`
basicModal.show({
body: msg,
buttons: {
action: {
title: 'Create Album',
fn: action
},
cancel: {
title: 'Cancel',
fn: basicModal.close
}
}
}
})
})
}
@ -169,6 +245,21 @@ album.delete = function(albumIDs) {
albums.deleteByID(id)
})
} else if (visible.album()) {
// if we deleted the current album, go to its parent
if (albumIDs.length==1 && album.json.id==albumIDs[0]) {
let id = album.getParent()
album.refresh()
lychee.goto(id)
}
// otherwise, we deleted a subalbum
else {
album.reload()
}
} else {
albums.refresh()
@ -197,20 +288,20 @@ album.delete = function(albumIDs) {
cancel.title = 'Keep Album'
// Get title
if (album.json) albumTitle = album.json.title
else if (albums.json) albumTitle = albums.getByID(albumIDs).title
if (album.json && album.json.id == albumIDs[0]) albumTitle = album.json.title
else if (albums.json || album.subjson) albumTitle = albums.getByID(albumIDs).title
// Fallback for album without a title
if (albumTitle==='') albumTitle = 'Untitled'
msg = lychee.html`<p>Are you sure you want to delete the album '$${ albumTitle }' and all of the photos it contains? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete the album '$${ albumTitle }' and all of the photos and subalbums it contains? This action can't be undone!</p>`
} else {
action.title = 'Delete Albums and Photos'
cancel.title = 'Keep Albums'
msg = lychee.html`<p>Are you sure you want to delete all $${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete all $${ albumIDs.length } selected albums and all of the photos and subalbums they contain? This action can't be undone!</p>`
}
@ -242,8 +333,8 @@ album.setTitle = function(albumIDs) {
if (albumIDs.length===1) {
// Get old title if only one album is selected
if (album.json) oldTitle = album.json.title
else if (albums.json) oldTitle = albums.getByID(albumIDs).title
if (album.json && album.json.id == albumIDs[0]) oldTitle = album.json.title
else if (albums.json || album.subjson) oldTitle = albums.getByID(albumIDs).title
}
@ -257,10 +348,20 @@ album.setTitle = function(albumIDs) {
// Rename only one album
album.json.title = newTitle
view.album.title()
if (album.json.id == albumIDs[0]) {
album.json.title = newTitle
view.album.title()
}
if (albums.json) albums.getByID(albumIDs[0]).title = newTitle
if (albums.json || album.subjson) {
albumIDs.forEach(function(id) {
let a = albums.getByID(id)
if (a) {
a.title = newTitle
view.album.content.title(id)
}
})
}
} else if (visible.albums()) {
@ -538,7 +639,7 @@ album.getArchive = function(albumID) {
}
album.merge = function(albumIDs) {
const getMessage = function(albumIDs, titles, operation) {
let title = ''
let sTitle = ''
@ -548,7 +649,8 @@ album.merge = function(albumIDs) {
if (albumIDs instanceof Array===false) albumIDs = [ albumIDs ]
// Get title of first album
if (albums.json) title = albums.getByID(albumIDs[0]).title
if (titles.length > 0) title = titles[0]
else if (albums.json || album.subjson) title = albums.getByID(albumIDs[0]).title
// Fallback for first album without a title
if (title==='') title = 'Untitled'
@ -556,19 +658,26 @@ album.merge = function(albumIDs) {
if (albumIDs.length===2) {
// Get title of second album
if (albums.json) sTitle = albums.getByID(albumIDs[1]).title
if (titles.length > 1) sTitle = titles[1]
else if (albums.json || album.subjson) sTitle = albums.getByID(albumIDs[1]).title
// Fallback for second album without a title
if (sTitle==='') sTitle = 'Untitled'
msg = lychee.html`<p>Are you sure you want to merge the album '$${ sTitle }' into the album '$${ title }'?</p>`
msg = lychee.html`<p>Are you sure you want to ${ operation } the album '$${ sTitle }' into '$${ title }'?</p>`
} else {
msg = lychee.html`<p>Are you sure you want to merge all selected albums into the album '$${ title }'?</p>`
msg = lychee.html`<p>Are you sure you want to ${ operation } all selected albums into '$${ title }'?</p>`
}
return msg
}
album.merge = function(albumIDs, titles = []) {
const action = function() {
basicModal.close()
@ -579,19 +688,15 @@ album.merge = function(albumIDs) {
api.post('Album::merge', params, function(data) {
if (data!==true) {
lychee.error(null, params, data)
} else {
albums.refresh()
lychee.goto()
}
if (data!==true) lychee.error(null, params, data)
else album.reload()
})
}
basicModal.show({
body: msg,
body: getMessage(albumIDs, titles, 'merge'),
buttons: {
action: {
title: 'Merge Albums',
@ -605,4 +710,59 @@ album.merge = function(albumIDs) {
}
})
}
album.move = function(albumIDs, titles = []) {
const action = function() {
basicModal.close()
let params = {
albumIDs: albumIDs.join()
}
api.post('Album::move', params, function(data) {
if (data!==true) lychee.error(null, params, data)
else album.reload()
})
}
basicModal.show({
body: getMessage(albumIDs, titles, 'move'),
buttons: {
action: {
title: 'Move Albums',
fn: action,
class: 'red'
},
cancel: {
title: "Don't Move",
fn: basicModal.close
}
}
})
}
album.reload = function() {
let albumID = album.getID()
album.refresh()
albums.refresh()
if (visible.album()) lychee.goto(albumID)
else lychee.goto()
}
album.refresh = function() {
album.json = null
album.subjson = null
}

View File

@ -16,7 +16,11 @@ albums.load = function() {
if (albums.json===null) {
api.post('Albums::get', {}, function(data) {
let params = {
parent: 0
}
api.post('Albums::get', params, function(data) {
let waitTime = 0
@ -108,18 +112,21 @@ albums.getByID = function(albumID) {
// Function returns the JSON of an album
if (albumID==null) return undefined
if (!albums.json) return undefined
if (!albums.json.albums) return undefined
if (albumID instanceof Array)
albumID = albumID[0]
let json = undefined
$.each(albums.json.albums, function(i) {
let func = function() {
if (this.id==albumID) json = this
}
let elem = albums.json.albums[i]
if (elem.id==albumID) json = elem
})
if (albums.json && albums.json.albums) {
$.each(albums.json.albums, func)
}
else if (album.subjson && album.subjson.albums) {
$.each(album.subjson.albums, func)
}
return json

View File

@ -14,11 +14,11 @@ build.iconic = function(icon, classes = '') {
}
build.divider = function(title) {
build.divider = function(title, id = '') {
let html = ''
html += lychee.html`<div class='divider'><h1>$${ title }</h1></div>`
html += lychee.html`<div class='divider' id='$${ id }'><h1>$${ title }</h1></div>`
return html

View File

@ -2,9 +2,75 @@
* @description This module is used for the context menu.
*/
const buildAlbumList = function(albums, exclude, action, parent = 0, layer = 0) {
let items = []
for (i in albums) {
if ((layer==0 && !albums[i].parent) || albums[i].parent==parent) {
let album = albums[i]
let thumb = 'src/images/no_cover.svg'
if (album.thumbs && album.thumbs[0]) thumb = album.thumbs[0]
else if (album.thumbUrl) thumb = album.thumbUrl
if (album.title==='') album.title = 'Untitled'
let prefix = (layer > 0 ? '&nbsp;&nbsp;'.repeat(layer - 1) + '└ ' : '')
let html = lychee.html`
${ prefix }
<img class='cover' width='16' height='16' src='$${ thumb }'>
<div class='title'>$${ album.title }</div>
`
items.push({
title: html,
disabled: (exclude.indexOf(album.id)!==-1),
fn: () => action(album)
})
items = items.concat(buildAlbumList(albums, exclude, action, album.id, layer + 1))
}
}
return items
}
const getAlbumFrom = function(albums, id) {
for (a in albums) {
if (albums[a].id == id) return albums[a]
}
return null
}
const getSubIDs = function(albums, albumID) {
let ids = [ albumID ]
for (a in albums) {
if (albums[a].parent==albumID) {
let sub = getSubIDs(albums, albums[a].id)
for (id in sub)
ids.push(sub[id])
}
}
return ids
}
contextMenu = {}
contextMenu.add = function(e) {
contextMenu.add = function(albumID, e) {
let items = [
{ title: build.iconic('image') + 'Upload Photo', fn: () => $('#upload_files').click() },
@ -13,7 +79,7 @@ contextMenu.add = function(e) {
{ title: build.iconic('dropbox', 'ionicons') + 'Import from Dropbox', fn: upload.start.dropbox },
{ title: build.iconic('terminal') + 'Import from Server', fn: upload.start.server },
{ },
{ title: build.iconic('folder') + 'New Album', fn: album.add }
{ title: build.iconic('folder') + 'New Album', fn: () => album.add(albumID) }
]
basicContext.show(items, e.originalEvent)
@ -46,18 +112,16 @@ contextMenu.album = function(albumID, e) {
// fn must call basicContext.close() first,
// in order to keep the selection
if (albumID==='0' || albumID==='f' || albumID==='s' || albumID==='r') return false
// Show merge-item when there's more than one album
let showMerge = (albums.json && albums.json.albums && Object.keys(albums.json.albums).length>1)
if (album.isSmartID(albumID)) return false
let items = [
{ title: build.iconic('pencil') + 'Rename', fn: () => album.setTitle([ albumID ]) },
{ title: build.iconic('collapse-left') + 'Merge', visible: showMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumID, e) } },
{ title: build.iconic('collapse-left') + 'Merge', fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumID, e) } },
{ title: build.iconic('folder') + 'Move', fn: () => { basicContext.close(); contextMenu.moveAlbum([ albumID ], e) } },
{ title: build.iconic('trash') + 'Delete', fn: () => album.delete([ albumID ]) }
]
$('.album[data-id="' + albumID + '"]').addClass('active')
multiselect.select('.album[data-id="' + albumID + '"]')
basicContext.show(items, e.originalEvent, contextMenu.close)
@ -71,13 +135,11 @@ contextMenu.albumMulti = function(albumIDs, e) {
// Show list of albums otherwise
let autoMerge = (albumIDs.length>1 ? true : false)
// Show merge-item when there's more than one album
let showMerge = (albums.json && albums.json.albums && Object.keys(albums.json.albums).length>1)
let items = [
{ title: build.iconic('pencil') + 'Rename All', fn: () => album.setTitle(albumIDs) },
{ title: build.iconic('collapse-left') + 'Merge All', visible: showMerge && autoMerge, fn: () => album.merge(albumIDs) },
{ title: build.iconic('collapse-left') + 'Merge', visible: showMerge && !autoMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumIDs[0], e) } },
{ title: build.iconic('collapse-left') + 'Merge All', visible: autoMerge, fn: () => album.merge(albumIDs) },
{ title: build.iconic('collapse-left') + 'Merge', visible: !autoMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumIDs[0], e) } },
{ title: build.iconic('folder') + 'Move All', fn: () => { basicContext.close(); contextMenu.moveAlbum(albumIDs, e) } },
{ title: build.iconic('trash') + 'Delete All', fn: () => album.delete(albumIDs) }
]
@ -89,26 +151,13 @@ contextMenu.albumMulti = function(albumIDs, e) {
contextMenu.albumTitle = function(albumID, e) {
api.post('Albums::get', {}, function(data) {
api.post('Albums::get', { parent: -1 }, function(data) {
let items = []
if (data.albums && data.num>1) {
// Generate list of albums
$.each(data.albums, function() {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
if (this.title==='') this.title = 'Untitled'
let html = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=albumID) items.push({
title: html,
fn: () => lychee.goto(this.id)
})
})
items = buildAlbumList(data.albums, [ albumID ], (a) => lychee.goto(a.id))
items.unshift({ })
@ -124,25 +173,57 @@ contextMenu.albumTitle = function(albumID, e) {
contextMenu.mergeAlbum = function(albumID, e) {
api.post('Albums::get', {}, function(data) {
api.post('Albums::get', { parent: -1 }, function(data) {
let items = []
if (data.albums && data.num>1) {
$.each(data.albums, function() {
let selalbum = albums.getByID(albumID)
let title = selalbum.title
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
if (this.title==='') this.title = 'Untitled'
// Disable all parents
// It's not possible to move them into us
let exclude = [ albumID ]
let a = getAlbumFrom(data.albums, selalbum.parent)
while (a!=null) {
exclude.push(a.id)
a = getAlbumFrom(data.albums, a.parent)
}
let html = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
items = buildAlbumList(data.albums, exclude, (a) => album.merge([ albumID, a.id ], [ title, a.title ]))
if (this.id!=albumID) items.push({
title: html,
fn: () => album.merge([ albumID, this.id ])
})
}
})
if (items.length===0) return false
basicContext.show(items, e.originalEvent, contextMenu.close)
})
}
contextMenu.moveAlbum = function(albumIDs, e) {
api.post('Albums::get', { parent: -1 }, function(data) {
let items = []
if (data.albums && data.num>1) {
let title = albums.getByID(albumIDs[0]).title
// Disable all childs
// It's not possible to move us into them
let exclude = []
for (i in albumIDs) {
let sub = getSubIDs(data.albums, String(albumIDs[i]))
for (s in sub)
exclude.push(sub[s])
}
items = buildAlbumList(data.albums, exclude, (a) => album.move([ a.id ].concat(albumIDs), [ a.title, title ]), 0, 1)
items.unshift({ title: 'Root', fn: () => album.move([ 0 ].concat(albumIDs), [ 'Root', title ]) })
}
@ -170,7 +251,7 @@ contextMenu.photo = function(photoID, e) {
{ title: build.iconic('trash') + 'Delete', fn: () => photo.delete([ photoID ]) }
]
$('.photo[data-id="' + photoID + '"]').addClass('active')
multiselect.select('.photo[data-id="' + photoID + '"]')
basicContext.show(items, e.originalEvent, contextMenu.close)
@ -178,12 +259,12 @@ contextMenu.photo = function(photoID, e) {
contextMenu.photoMulti = function(photoIDs, e) {
multiselect.stopResize()
// Notice for 'Move All':
// fn must call basicContext.close() first,
// in order to keep the selection and multiselect
multiselect.stopResize()
let items = [
{ title: build.iconic('star') + 'Star All', fn: () => photo.setStar(photoIDs) },
{ title: build.iconic('tag') + 'Tag All', fn: () => photo.editTags(photoIDs) },
@ -210,19 +291,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) {
items.push({ })
// Generate list of albums
$.each(data.content, function(index) {
if (this.title==='') this.title = 'Untitled'
let html = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbUrl }'><div class='title'>$${ this.title }</div>`
if (this.id!=photoID) items.push({
title: html,
fn: () => lychee.goto(albumID + '/' + this.id)
})
})
items = items.concat(buildAlbumList(data.content, [ photoID ], (a) => lychee.goto(albumID + '/' + a.id)))
}
@ -250,7 +319,7 @@ contextMenu.move = function(photoIDs, e) {
let items = []
api.post('Albums::get', {}, function(data) {
api.post('Albums::get', { parent: -1 }, function(data) {
if (data.num===0) {
@ -261,20 +330,7 @@ contextMenu.move = function(photoIDs, e) {
} else {
// Generate list of albums
$.each(data.albums, function() {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
if (this.title==='') this.title = 'Untitled'
let html = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=album.getID()) items.push({
title: html,
fn: () => photo.setAlbum(photoIDs, this.id)
})
})
items = buildAlbumList(data.albums, [ album.getID() ], (a) => photo.setAlbum(photoIDs, a.id))
// Show Unsorted when unsorted is not the current album
if (album.getID()!=='0') {
@ -344,7 +400,7 @@ contextMenu.close = function() {
basicContext.close()
$('.photo.active, .album.active').removeClass('active')
multiselect.deselect('.photo.active, .album.active')
if (visible.multiselect()) multiselect.close()
}

View File

@ -43,7 +43,7 @@ header.bind = function() {
header.dom('#button_settings') .on(eventName, contextMenu.settings)
header.dom('#button_info_album') .on(eventName, sidebar.toggle)
header.dom('#button_info') .on(eventName, sidebar.toggle)
header.dom('.button_add') .on(eventName, contextMenu.add)
header.dom('.button_add') .on(eventName, function(e) { contextMenu.add(album.getID(), e) })
header.dom('#button_more') .on(eventName, function(e) { contextMenu.photoMore(photo.getID(), e) })
header.dom('#button_move') .on(eventName, function(e) { contextMenu.move([ photo.getID() ], e) })
header.dom('.header__hostedwith') .on(eventName, function() { window.open(lychee.website) })
@ -51,7 +51,7 @@ header.bind = function() {
header.dom('#button_trash') .on(eventName, function() { photo.delete([ photo.getID() ]) })
header.dom('#button_archive') .on(eventName, function() { album.getArchive(album.getID()) })
header.dom('#button_star') .on(eventName, function() { photo.setStar([ photo.getID() ]) })
header.dom('#button_back_home') .on(eventName, function() { lychee.goto() })
header.dom('#button_back_home') .on(eventName, function() { lychee.goto(album.getParent()) })
header.dom('#button_back') .on(eventName, function() { lychee.goto(album.getID()) })
header.dom('.header__search').on('keyup click', function() { search.find($(this).val()) })
@ -132,8 +132,8 @@ header.setMode = function(mode) {
header.dom('.header__toolbar--album').addClass('header__toolbar--visible')
// Hide download button when album empty
if (album.json.content===false) $('#button_archive').hide()
else $('#button_archive').show()
if (album.json.content===false && album.subjson.num==0) $('#button_archive').hide()
else $('#button_archive').show()
// Hide download button when not logged in and album not downloadable
if (lychee.publicMode===true && album.json.downloadable==='0') $('#button_archive').hide()

View File

@ -54,10 +54,6 @@ $(document).ready(function() {
if (visible.photo() && basicModal.visible()===false) { photo.delete([photo.getID()]); return false }
else if (visible.album() && basicModal.visible()===false) { album.delete([album.getID()]); return false }
})
.bind([ 'command+a', 'ctrl+a' ], function() {
if (visible.album() && basicModal.visible()===false) { multiselect.selectAll(); return false }
else if (visible.albums() && basicModal.visible()===false) { multiselect.selectAll(); return false }
})
Mousetrap.bindGlobal('enter', function() {
if (basicModal.visible()===true) basicModal.action()
@ -67,7 +63,7 @@ $(document).ready(function() {
if (basicModal.visible()===true) basicModal.cancel()
else if (visible.contextMenu()) contextMenu.close()
else if (visible.photo()) lychee.goto(album.getID())
else if (visible.album()) lychee.goto()
else if (visible.album()) lychee.goto(album.getParent())
else if (visible.albums() && header.dom('.header__search').val().length!==0) search.reset()
return false
})
@ -95,12 +91,12 @@ $(document).ready(function() {
$(document)
// Navigation
.on('click', '.album', function() { lychee.goto($(this).attr('data-id')) })
.on('click', '.photo', function() { lychee.goto(album.getID() + '/' + $(this).attr('data-id')) })
.on('click', '.album', function(e) { multiselect.albumClick(e, $(this)) })
.on('click', '.photo', function(e) { multiselect.photoClick(e, $(this)) })
// Context Menu
.on('contextmenu', '.photo', function(e) { contextMenu.photo(photo.getID(), e) })
.on('contextmenu', '.album', function(e) { contextMenu.album(album.getID(), e) })
.on('contextmenu', '.photo', function(e) { multiselect.photoContextMenu(e, $(this)) })
.on('contextmenu', '.album', function(e) { multiselect.albumContextMenu(e, $(this)) })
// Upload
.on('change', '#upload_files', function() { basicModal.close(); upload.start.local(this.files) })

View File

@ -5,8 +5,8 @@
lychee = {
title : document.title,
version : '3.1.5',
versionCode : '030105',
version : '3.2.0',
versionCode : '030200',
updatePath : '//update.electerious.com/index.json',
updateURL : 'https://github.com/electerious/Lychee',
@ -169,6 +169,7 @@ lychee.load = function() {
// Trash data
photo.json = null
albums.json = null
// Show Photo
if (lychee.content.html()==='' || (header.dom('.header__search').length && header.dom('.header__search').val().length!==0)) {
@ -181,10 +182,11 @@ lychee.load = function() {
// Trash data
photo.json = null
albums.json = null
// Show Album
if (visible.photo()) view.photo.hide()
if (visible.sidebar() && (albumID==='0' || albumID==='f' || albumID==='s' || albumID==='r')) sidebar.toggle()
if (visible.sidebar() && album.isSmartID(albumID)) sidebar.toggle()
if (album.json && albumID==album.json.id) view.album.title()
else album.load(albumID)
@ -197,6 +199,7 @@ lychee.load = function() {
}
// Trash data
album.subjson = null
album.json = null
photo.json = null
@ -426,4 +429,4 @@ lychee.error = function(errorThrown, params, data) {
loadingBar.show('error', errorThrown)
}
}

View File

@ -2,7 +2,19 @@
* @description Select multiple albums or photos.
*/
multiselect = {}
const isSelectKeyPressed = function(e) {
return e.metaKey || e.ctrlKey
}
multiselect = {
ids : [],
albumsSelected : 0,
photosSelected : 0
}
multiselect.position = {
@ -21,6 +33,121 @@ multiselect.bind = function() {
}
multiselect.isSelected = function(id) {
let pos = $.inArray(id, multiselect.ids)
return {
selected : (pos===-1 ? false : true),
position : pos
}
}
multiselect.toggleItem = function(object, id) {
let selected = multiselect.isSelected(id).selected
if (selected===false) multiselect.addItem(object, id)
else multiselect.removeItem(object, id)
}
multiselect.addItem = function(object, id) {
if (album.isSmartID(id)) return
if (multiselect.isSelected(id).selected===true) return
let isAlbum = object.hasClass('album')
if ((isAlbum && multiselect.photosSelected > 0) ||
(!isAlbum && multiselect.albumsSelected > 0)) {
lychee.error('Please select either albums or photos!')
return
}
multiselect.ids.push(id)
multiselect.select(object)
if (isAlbum) multiselect.albumsSelected++
else multiselect.photosSelected++
}
multiselect.removeItem = function(object, id) {
let { selected, pos } = multiselect.isSelected(id)
if (selected===false) return
multiselect.ids.splice(pos, 1)
multiselect.deselect(object)
let isAlbum = object.hasClass('album')
if (isAlbum) multiselect.albumsSelected--
else multiselect.photosSelected--
}
multiselect.albumClick = function(e, albumObj) {
let id = albumObj.attr('data-id')
if (isSelectKeyPressed(e)) multiselect.toggleItem(albumObj, id)
else lychee.goto(id)
}
multiselect.photoClick = function(e, photoObj) {
let id = photoObj.attr('data-id')
if (isSelectKeyPressed(e)) multiselect.toggleItem(photoObj, id)
else lychee.goto(album.getID() + '/' + id)
}
multiselect.albumContextMenu = function(e, albumObj) {
let id = albumObj.attr('data-id')
let selected = multiselect.isSelected(id).selected
if (selected!==false) {
contextMenu.albumMulti(multiselect.ids, e)
multiselect.clearSelection(false)
} else {
multiselect.clearSelection()
contextMenu.album(album.getID(), e)
}
}
multiselect.photoContextMenu = function(e, photoObj) {
let id = photoObj.attr('data-id')
let selected = multiselect.isSelected(id).selected
if (selected!==false) {
contextMenu.photoMulti(multiselect.ids, e)
multiselect.clearSelection(false)
} else {
multiselect.clearSelection()
contextMenu.photo(photo.getID(), e)
}
}
multiselect.clearSelection = function(deselect = true) {
if (deselect) multiselect.deselect('.photo.active, .album.active')
multiselect.ids = []
multiselect.albumsSelected = 0
multiselect.photosSelected = 0
}
multiselect.show = function(e) {
if (lychee.publicMode) return false
@ -44,43 +171,6 @@ multiselect.show = function(e) {
}
multiselect.selectAll = function() {
if (lychee.publicMode) return false
if (visible.search()) return false
if (!visible.albums() && !visible.album) return false
if (visible.multiselect()) $('#multiselect').remove()
sidebar.setSelectable(false)
multiselect.position.top = 70
multiselect.position.right = 40
multiselect.position.bottom = 90
multiselect.position.left = 20
$('body').append(build.multiselect(multiselect.position.top, multiselect.position.left))
let documentSize = {
width : $(document).width(),
height : $(document).height()
}
let newSize = {
width : documentSize.width - multiselect.position.right + 2,
height : documentSize.height - multiselect.position.bottom
}
let e = {
pageX : documentSize.width - (multiselect.position.right / 2),
pageY : documentSize.height - multiselect.position.bottom
}
$('#multiselect').css(newSize)
multiselect.getSelection(e)
}
multiselect.resize = function(e) {
if (multiselect.position.top === null ||
@ -187,6 +277,11 @@ multiselect.getSelection = function(e) {
if (visible.contextMenu()) return false
if (!visible.multiselect()) return false
if (!isSelectKeyPressed(e) && (size.width==0 || size.height==0)) {
multiselect.close()
return false
}
$('.photo, .album').each(function() {
let offset = $(this).offset()
@ -196,26 +291,37 @@ multiselect.getSelection = function(e) {
(offset.top + 206)<=(size.top + size.height + tolerance) &&
(offset.left + 206)<=(size.left + size.width + tolerance)) {
let id = $(this).data('id')
let id = $(this).attr('data-id')
if (id!=='0' && id!==0 && id!=='f' && id!=='s' && id!=='r' && id!=null) {
ids.push(id)
$(this).addClass('active')
}
multiselect.addItem($(this), id)
}
})
if (ids.length!==0 && visible.album()) contextMenu.photoMulti(ids, e)
else if (ids.length!==0 && visible.albums()) contextMenu.albumMulti(ids, e)
else multiselect.close()
multiselect.hide()
}
multiselect.close = function() {
multiselect.select = function(id) {
let el = $(id)
el.addClass('selected')
el.addClass('active')
}
multiselect.deselect = function(id) {
let el = $(id)
el.removeClass('selected')
el.removeClass('active')
}
multiselect.hide = function() {
sidebar.setSelectable(true)
@ -229,4 +335,12 @@ multiselect.close = function() {
lychee.animate('#multiselect', 'fadeOut')
setTimeout(() => $('#multiselect').remove(), 300)
}
multiselect.close = function() {
multiselect.clearSelection()
multiselect.hide()
}

View File

@ -72,7 +72,7 @@ settings.createConfig = function() {
if (data==='Warning: Could not create file!') {
basicModal.show({
body: "<p>Unable to save this configuration. Permission denied in <b>'data/'</b>. Please set the read, write and execute rights for others in <b>'data/'</b> and <b>'uploads/'</b>. Take a look at the readme for more information.</p>",
body: `<p>Unable to save this configuration. Permission denied in <b>'data/'</b>. Please set the read, write and execute rights for others in <b>'data/'</b> and <b>'uploads/'</b>. Take a look at the readme for more information.</p>`,
buttons: {
action: {
title: 'Retry',

View File

@ -124,14 +124,18 @@ sidebar.createStructure.photo = function(data) {
// Set value for public
switch (data.public) {
case '0' : _public = 'No'
break
case '1' : _public = 'Yes'
break
case '2' : _public = 'Yes (Album)'
break
default : _public = '-'
break
case '0':
_public = 'No'
break
case '1':
_public = 'Yes'
break
case '2':
_public = 'Yes (Album)'
break
default:
_public = '-'
break
}

View File

@ -143,8 +143,22 @@ view.album = {
let photosData = ''
// Sub albums
if (album.subjson && album.subjson.albums && album.subjson.num!==0) {
photosData = build.divider('Albums')
$.each(album.subjson.albums, function() {
albums.parse(this)
photosData += build.album(this)
})
}
if (album.json.content && album.json.content!==false) {
photosData += build.divider('Photos', 'divider-photos')
// Build photos
$.each(album.json.content, function() {
photosData += build.photo(this)
@ -163,13 +177,32 @@ view.album = {
title: function(photoID) {
let title = album.json.content[photoID].title
let ntitle = ''
let prefix = ''
title = lychee.escapeHTML(title)
if (album.json.content[photoID]) {
$('.photo[data-id="' + photoID + '"] .overlay h1')
.html(title)
.attr('title', title)
prefix = '.photo'
ntitle = album.json.content[photoID].title
} else {
prefix = '.album'
for (i in album.subjson.albums) {
if (album.subjson.albums[i].id==photoID) {
ntitle = album.subjson.albums[i].title
break
}
}
}
ntitle = lychee.escapeHTML(ntitle)
$(prefix + '[data-id="' + photoID + '"] .overlay h1')
.html(ntitle)
.attr('title', ntitle)
},
@ -202,6 +235,7 @@ view.album = {
if (!visible.albums()) {
album.json.num--
view.album.num()
if (album.json.num == 0) $('#divider-photos').hide()
}
})

View File

@ -72,6 +72,10 @@
transition: none;
border-color: darken($colorBlue, 15%);
}
&.selected img {
outline: 1px solid $colorBlue;
}
}
// Album -------------------------------------------------------------- //