From 6d4df5f6b7821003a4d18d3df385de389177182d Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Thu, 28 Jul 2016 12:54:03 +0200 Subject: [PATCH 01/13] Introduced album.isSmartID. --- src/scripts/album.js | 8 +++++++- src/scripts/contextMenu.js | 2 +- src/scripts/multiselect.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/scripts/album.js b/src/scripts/album.js index 544eb25..ee93954 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -9,12 +9,18 @@ album = { } +album.isSmartID = function(id) { + + return id==='0' || id==='f' || id==='s' || id==='r' + +} + album.getID = function() { let id = null let isID = (id) => { - if (id==='0' || id==='f' || id==='s' || id==='r') return true + if (album.isSmartID(id)) return true return $.isNumeric(id) } diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index 806487d..bc77423 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -47,7 +47,7 @@ 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 + if (album.isSmartID(albumID)) 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) diff --git a/src/scripts/multiselect.js b/src/scripts/multiselect.js index e9c60e2..a742734 100644 --- a/src/scripts/multiselect.js +++ b/src/scripts/multiselect.js @@ -199,7 +199,7 @@ multiselect.getSelection = function(e) { let id = $(this).data('id') - if (id!=='0' && id!==0 && id!=='f' && id!=='s' && id!=='r' && id!=null) { + if (!album.isSmartID(id) && id!=null) { ids.push(id) $(this).addClass('active') From ef9040870a25df51c58a821e89d915d577d2d54d Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Thu, 28 Jul 2016 16:01:36 +0200 Subject: [PATCH 02/13] 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. --- php/Access/Admin.php | 8 +- php/Access/Guest.php | 4 +- php/Modules/Album.php | 6 +- php/Modules/Albums.php | 6 +- php/Modules/Database.php | 3 +- php/database/update_030103.php | 24 +++++ src/package.json | 2 +- src/scripts/album.js | 127 ++++++++++++++++++----- src/scripts/albums.js | 25 +++-- src/scripts/contextMenu.js | 137 ++++++++++++++----------- src/scripts/header.js | 4 +- src/scripts/lychee.js | 7 +- src/scripts/view.js | 36 ++++++- src/styles/_basicContext.extended.scss | 4 + 14 files changed, 277 insertions(+), 116 deletions(-) create mode 100644 php/database/update_030103.php diff --git a/php/Access/Admin.php b/php/Access/Admin.php index c706653..e66dc5f 100644 --- a/php/Access/Admin.php +++ b/php/Access/Admin.php @@ -72,8 +72,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 +92,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); } diff --git a/php/Access/Guest.php b/php/Access/Guest.php index 84e3f69..7b988a2 100644 --- a/php/Access/Guest.php +++ b/php/Access/Guest.php @@ -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'])); } diff --git a/php/Modules/Album.php b/php/Modules/Album.php index 1c03ba5..88f2be1 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -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 @@ -79,6 +79,8 @@ final class Album { // Parse thumbs or set default value $album['thumbs'] = (isset($data['thumbs']) ? explode(',', $data['thumbs']) : array()); + $album['parent'] = $data['parent']; + return $album; } diff --git a/php/Modules/Albums.php b/php/Modules/Albums.php index e57ee84..f3aa9c3 100644 --- a/php/Modules/Albums.php +++ b/php/Modules/Albums.php @@ -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__); diff --git a/php/Modules/Database.php b/php/Modules/Database.php index 11924b5..9ef27c5 100755 --- a/php/Modules/Database.php +++ b/php/Modules/Database.php @@ -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 + '030103' // 3.1.3 ); /** diff --git a/php/database/update_030103.php b/php/database/update_030103.php new file mode 100644 index 0000000..ebacc15 --- /dev/null +++ b/php/database/update_030103.php @@ -0,0 +1,24 @@ +error . ')'); + return false; + } +} + +// Set version +if (Database::setVersion($connection, '030103')===false) Response::error('Could not update version of database!'); + +?> \ No newline at end of file diff --git a/src/package.json b/src/package.json index cdf7be2..db58d4e 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "Lychee", - "version": "3.1.2", + "version": "3.1.3", "description": "Self-hosted photo-management done right.", "authors": "Tobias Reich ", "license": "MIT", diff --git a/src/scripts/album.js b/src/scripts/album.js index ee93954..d95198f 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -5,7 +5,8 @@ album = { - json: null + json: null, + subjson: null } @@ -24,18 +25,32 @@ album.getID = function() { 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 } +album.getParent = function () { + + let id = album.json.id; + + if (album.isSmartID(id) || album.json.parent==0) { + return '' + } else { + return album.json.parent + } + +} + album.load = function(albumID, refresh = false) { password.get(albumID, function() { @@ -96,6 +111,28 @@ album.load = function(albumID, refresh = false) { }) + if (!album.isSmartID(albumID)) { + 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(() => { + view.album.init() + }, waitTime) + + }) + } }) } @@ -106,18 +143,35 @@ album.parse = function() { } -album.add = function() { +function buildAlbumOptions(albums, select, parent = 0, layer = 0) { + var cmbxOptions = '' + for (i in albums) { + if (albums[i].parent == parent) { + let title = (layer > 0 ? "  ".repeat(layer - 1) + "└ " : "") + albums[i].title + cmbxOptions += `` + cmbxOptions += buildAlbumOptions(albums, select, albums[i].id, layer + 1) + } + } + return cmbxOptions +} + +album.add = function(albumID = 0) { const action = function(data) { let title = data.title + let parent = data.parent const isNumber = (n) => (!isNaN(parseFloat(n)) && isFinite(n)) basicModal.close() let params = { - title + title, + parent } api.post('Album::add', params, function(data) { @@ -133,18 +187,28 @@ album.add = function() { } - basicModal.show({ - body: `

Enter a title for the new album:

`, - buttons: { - action: { - title: 'Create Album', - fn: action - }, - cancel: { - title: 'Cancel', - fn: basicModal.close + api.post('Albums::get', { + parent: -1 + }, function (data) { + var cmbxOptions = `' + + basicModal.show({ + body: `

Enter a title for the new album:

` + + `

Select the parent album:
` + cmbxOptions + `

`, + buttons: { + action: { + title: 'Create Album', + fn: action + }, + cancel: { + title: 'Cancel', + fn: basicModal.close + } } - } + }) }) } @@ -204,8 +268,8 @@ 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' @@ -249,8 +313,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 } @@ -264,10 +328,17 @@ 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) { + albums.getByID(id).title = newTitle + view.album.content.title(id) + }) + } } else if (visible.albums()) { @@ -545,7 +616,7 @@ album.getArchive = function(albumID) { } -album.merge = function(albumIDs) { +album.merge = function(albumIDs, titles = []) { let title = '' let sTitle = '' @@ -555,7 +626,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' @@ -563,7 +635,8 @@ 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' diff --git a/src/scripts/albums.js b/src/scripts/albums.js index 26f8ea7..6bb7667 100644 --- a/src/scripts/albums.js +++ b/src/scripts/albums.js @@ -17,7 +17,11 @@ albums.load = function() { if (albums.json===null) { - api.post('Albums::get', {}, function(data) { + params = { + parent: 0 + } + + api.post('Albums::get', params, function(data) { let waitTime = 0 @@ -109,18 +113,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 elem = albums.json.albums[i] - - if (elem.id==albumID) json = elem + let func = function() { + if (this.id==albumID) json = this + } - }) + 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 diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index bc77423..b31d53f 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -3,9 +3,47 @@ * @copyright 2015 by Tobias Reich */ +function buildAlbumList(albums, albumID, 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 ? "  ".repeat(layer - 1) + "└ " : "" + let html = prefix + lychee.html`
$${ album.title }
` + + if (album.id!=albumID) { + items.push({ + title: html, + fn: () => action(album) + }) + } + else { + html = "
" + html + "
" + items.push({ + title: html, + fn: () => {} + }) + } + + items = items.concat(buildAlbumList(albums, albumID, action, album.id, layer + 1)) + } + } + + return items +} + contextMenu = {} -contextMenu.add = function(e) { +contextMenu.add = function(albumID, e) { let items = [ { title: build.iconic('image') + 'Upload Photo', fn: () => $('#upload_files').click() }, @@ -14,7 +52,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) @@ -90,26 +128,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`
$${ this.title }
` - - 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({ }) @@ -125,25 +150,15 @@ 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 title = albums.getByID(albumID).title - if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' - if (this.title==='') this.title = 'Untitled' - - let html = lychee.html`
$${ this.title }
` - - if (this.id!=albumID) items.push({ - title: html, - fn: () => album.merge([ albumID, this.id ]) - }) - - }) + items = buildAlbumList(data.albums, albumID, (a) => album.merge([ albumID, a.id ], [title, a.title])) } @@ -177,14 +192,41 @@ contextMenu.photo = function(photoID, e) { } +function countSubAlbums(photoIDs) { + let count = 0 + for (i in photoIDs) { + for (j in album.subjson.albums) { + if (album.subjson.albums[j].id == photoIDs[i]) { + count++ + break + } + } + } + return count +} + contextMenu.photoMulti = function(photoIDs, e) { + let subcount = countSubAlbums(photoIDs) + let photocount = photoIDs.length - subcount + + if (subcount && photocount) { + $('.photo.active, .album.active').removeClass('active') + multiselect.close() + lychee.error("Please select either albums or photos!") + return + } + if (subcount) { + contextMenu.albumMulti(photoIDs, e) + return + } + + 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) }, @@ -211,19 +253,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`
$${ this.title }
` - - 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))) } @@ -251,7 +281,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) { @@ -262,20 +292,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`
$${ this.title }
` - - 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') { diff --git a/src/scripts/header.js b/src/scripts/header.js index 6c4b81c..93d2eba 100644 --- a/src/scripts/header.js +++ b/src/scripts/header.js @@ -44,7 +44,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) }) @@ -52,7 +52,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()) }) diff --git a/src/scripts/lychee.js b/src/scripts/lychee.js index 105e5ba..125d407 100644 --- a/src/scripts/lychee.js +++ b/src/scripts/lychee.js @@ -6,8 +6,8 @@ lychee = { title : document.title, - version : '3.1.2', - versionCode : '030102', + version : '3.1.3', + versionCode : '030103', updatePath : '//update.electerious.com/index.json', updateURL : 'https://github.com/electerious/Lychee', @@ -170,6 +170,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)) { @@ -182,6 +183,7 @@ lychee.load = function() { // Trash data photo.json = null + albums.json = null // Show Album if (visible.photo()) view.photo.hide() @@ -197,6 +199,7 @@ lychee.load = function() { } // Trash data + album.subjson = null album.json = null photo.json = null diff --git a/src/scripts/view.js b/src/scripts/view.js index 198a4fd..447cfcc 100644 --- a/src/scripts/view.js +++ b/src/scripts/view.js @@ -144,8 +144,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') + // Build photos $.each(album.json.content, function() { photosData += build.photo(this) @@ -164,13 +178,25 @@ view.album = { title: function(photoID) { - let title = album.json.content[photoID].title + if (album.json.content[photoID]) { + ntitle = album.json.content[photoID].title + prefix = '.photo' + } + else { + for (i in album.subjson.albums) { + if (album.subjson.albums[i].id == photoID) { + ntitle = album.subjson.albums[i].title + break + } + } + prefix = '.album' + } - title = lychee.escapeHTML(title) + ntitle = lychee.escapeHTML(ntitle) - $('.photo[data-id="' + photoID + '"] .overlay h1') - .html(title) - .attr('title', title) + $(prefix + '[data-id="' + photoID + '"] .overlay h1') + .html(ntitle) + .attr('title', ntitle) }, diff --git a/src/styles/_basicContext.extended.scss b/src/styles/_basicContext.extended.scss index b886c50..d1665d6 100644 --- a/src/styles/_basicContext.extended.scss +++ b/src/styles/_basicContext.extended.scss @@ -8,6 +8,10 @@ box-shadow: 0 0 0 1px black(.5); } + &__data .disabled { + opacity: 0.5; + } + &__data .title { display: inline-block; margin: 0 0 3px 26px; From 212b241d0e1c0090c56adc9eed096447abc247bd Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Fri, 29 Jul 2016 21:47:58 +0200 Subject: [PATCH 03/13] Include subalbums in album archives. --- php/Modules/Album.php | 132 ++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/php/Modules/Album.php b/php/Modules/Album.php index 88f2be1..197f07a 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -195,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': @@ -216,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'; @@ -254,17 +248,72 @@ final class Album { return false; } - // Execute query - $photos = Database::execute(Database::get(), $photos, __METHOD__, __LINE__); + // 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; + } - if ($album===null) return false; + // Finish zip + $zip->close(); - // Check if album empty - if ($photos->num_rows==0) { - Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create ZipArchive without images'); - return false; + // Send zip + header("Content-Type: application/zip"); + header("Content-Disposition: attachment; filename=\"$zipTitle.zip\""); + header("Content-Length: " . filesize($filename)); + readfile($filename); + + // Delete zip + unlink($filename); + + // Call plugins + Plugins::get()->activate(__METHOD__, 1, func_get_args()); + + return true; + + } + + 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()) { @@ -273,7 +322,7 @@ final class Album { $photo->url = LYCHEE_UPLOADS_BIG . $photo->url; // Parse title - $photo->title = str_replace($badChars, '', $photo->title); + $photo->title = $this->cleanZipName($photo->title); if (!isset($photo->title)||$photo->title==='') $photo->title = 'Untitled'; // Check if readable @@ -283,7 +332,7 @@ final class Album { $extension = getExtension($photo->url, false); // Set title for photo - $zipFileName = $zipTitle . '/' . $photo->title . $extension; + $zipFileName = $path . '/' . $photo->title . $extension; // Check for duplicates if (!empty($files)) { @@ -291,7 +340,7 @@ final class Album { while (in_array($zipFileName, $files)) { // Set new title for photo - $zipFileName = $zipTitle . '/' . $photo->title . '-' . $i . $extension; + $zipFileName = $path . '/' . $photo->title . '-' . $i . $extension; $i++; @@ -306,23 +355,6 @@ final class Album { } - // Finish zip - $zip->close(); - - // Send zip - header("Content-Type: application/zip"); - header("Content-Disposition: attachment; filename=\"$zipTitle.zip\""); - header("Content-Length: " . filesize($filename)); - readfile($filename); - - // Delete zip - unlink($filename); - - // Call plugins - Plugins::get()->activate(__METHOD__, 1, func_get_args()); - - return true; - } /** From eddb666b5a06ece05af6c460b11033321208f557 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Fri, 29 Jul 2016 21:56:48 +0200 Subject: [PATCH 04/13] Delete albums including subalbums. --- php/Modules/Album.php | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/php/Modules/Album.php b/php/Modules/Album.php index 197f07a..ceb56d9 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -475,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. */ @@ -640,11 +665,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; @@ -665,7 +693,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 From 1b6c74bcb616603014bd7e6781987842838eca40 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Fri, 29 Jul 2016 21:58:29 +0200 Subject: [PATCH 05/13] Merge albums including subalbums. --- php/Modules/Album.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/php/Modules/Album.php b/php/Modules/Album.php index ceb56d9..510f2fd 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -634,6 +634,17 @@ final class Album { $albumID = array_splice($albumIDs, 0, 1); $albumID = $albumID[0]; + // Ensure that we don't merge an album into its own subalbum + foreach($albumIDs as $id) { + foreach($this->getSubAlbums($id) as $sid) { + if($sid == $albumID) { + Log::error(Database::get(), __METHOD__, __LINE__, 'Cannot move album into its own subalbum'); + return false; + } + } + } + + // 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__); @@ -643,6 +654,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__); From 6feae99bf1dcd876312efa681f869ba6f84f7d1f Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Fri, 29 Jul 2016 21:58:40 +0200 Subject: [PATCH 06/13] 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. --- php/Modules/Album.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/php/Modules/Album.php b/php/Modules/Album.php index 510f2fd..c83e3a8 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -516,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; @@ -525,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; From 55247e4f56296ee5721e0db5e533cc2b2c431b2c Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Fri, 29 Jul 2016 23:56:45 +0200 Subject: [PATCH 07/13] Load album view just once. --- src/scripts/album.js | 49 ++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/scripts/album.js b/src/scripts/album.js index d95198f..d7cdc0d 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -100,39 +100,44 @@ 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 (refresh===false) { + lychee.animate(lychee.content, 'contentZoomIn') + header.setMode('album') + } } - }, waitTime) + if (!album.isSmartID(albumID)) { + params = { + parent: albumID + } - }) + api.post('Albums::get', params, function(data) { - if (!album.isSmartID(albumID)) { - params = { - parent: albumID - } + let waitTime = 0 - api.post('Albums::get', params, function(data) { + album.subjson = data - let waitTime = 0 + // Calculate delay + let durationTime = (new Date().getTime() - startTime) + if (durationTime>300) waitTime = 0 + else waitTime = 300 - durationTime - album.subjson = data + setTimeout(() => { + finish() + }, waitTime) - // Calculate delay - let durationTime = (new Date().getTime() - startTime) - if (durationTime>300) waitTime = 0 - else waitTime = 300 - durationTime + }) + } + else { + finish() + } - setTimeout(() => { - view.album.init() - }, waitTime) + }, waitTime) - }) - } + }) }) } From d121e2c4f6698076cb53760cf0416998ed8f6bbf Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 00:00:54 +0200 Subject: [PATCH 08/13] Go to parent album on ESC. --- src/scripts/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/init.js b/src/scripts/init.js index 71520e5..fa981b8 100755 --- a/src/scripts/init.js +++ b/src/scripts/init.js @@ -68,7 +68,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 }) From cdd130b243dba615530d4874f24151c7ab3d2b05 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 11:20:30 +0200 Subject: [PATCH 09/13] Let the user know that deleting an album deletes subalbums, too. --- src/scripts/album.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/album.js b/src/scripts/album.js index d7cdc0d..e946b03 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -279,14 +279,14 @@ album.delete = function(albumIDs) { // Fallback for album without a title if (albumTitle==='') albumTitle = 'Untitled' - msg = lychee.html`

Are you sure you want to delete the album '$${ albumTitle }' and all of the photos it contains? This action can't be undone!

` + msg = lychee.html`

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!

` } else { action.title = 'Delete Albums and Photos' cancel.title = 'Keep Albums' - msg = lychee.html`

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!

` + msg = lychee.html`

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!

` } From daf7d37c84592c4ee307617ef73e1f9a142a870a Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 12:02:56 +0200 Subject: [PATCH 10/13] 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. --- php/Modules/Album.php | 5 +---- src/scripts/contextMenu.js | 33 +++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/php/Modules/Album.php b/php/Modules/Album.php index c83e3a8..3aaf980 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -640,10 +640,7 @@ final class Album { // Ensure that we don't merge an album into its own subalbum foreach($albumIDs as $id) { foreach($this->getSubAlbums($id) as $sid) { - if($sid == $albumID) { - Log::error(Database::get(), __METHOD__, __LINE__, 'Cannot move album into its own subalbum'); - return false; - } + if($sid == $albumID) return false; } } diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index b31d53f..382beff 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -3,7 +3,7 @@ * @copyright 2015 by Tobias Reich */ -function buildAlbumList(albums, albumID, action, parent = 0, layer = 0) { +function buildAlbumList(albums, exclude, action, parent = 0, layer = 0) { let items = [] for (i in albums) { @@ -20,7 +20,7 @@ function buildAlbumList(albums, albumID, action, parent = 0, layer = 0) { let prefix = layer > 0 ? "  ".repeat(layer - 1) + "└ " : "" let html = prefix + lychee.html`
$${ album.title }
` - if (album.id!=albumID) { + if (exclude.indexOf(album.id) == -1) { items.push({ title: html, fn: () => action(album) @@ -34,7 +34,7 @@ function buildAlbumList(albums, albumID, action, parent = 0, layer = 0) { }) } - items = items.concat(buildAlbumList(albums, albumID, action, album.id, layer + 1)) + items = items.concat(buildAlbumList(albums, exclude, action, album.id, layer + 1)) } } @@ -134,7 +134,7 @@ contextMenu.albumTitle = function(albumID, e) { if (data.albums && data.num>1) { - items = buildAlbumList(data.albums, albumID, (a) => lychee.goto(a.id)) + items = buildAlbumList(data.albums, [albumID], (a) => lychee.goto(a.id)) items.unshift({ }) @@ -148,6 +148,14 @@ contextMenu.albumTitle = function(albumID, e) { } +function getAlbumFrom(albums, id) { + for (a in albums) { + if (albums[a].id == id) + return albums[a] + } + return null +} + contextMenu.mergeAlbum = function(albumID, e) { api.post('Albums::get', { parent: -1 }, function(data) { @@ -156,9 +164,18 @@ contextMenu.mergeAlbum = function(albumID, e) { if (data.albums && data.num>1) { - let title = albums.getByID(albumID).title + let selalbum = albums.getByID(albumID) + let title = selalbum.title + + // disable all parents; we cannot 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) + } - items = buildAlbumList(data.albums, albumID, (a) => album.merge([ albumID, a.id ], [title, a.title])) + items = buildAlbumList(data.albums, exclude, (a) => album.merge([ albumID, a.id ], [title, a.title])) } @@ -253,7 +270,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) { items.push({ }) - items = items.concat(buildAlbumList(data.content, photoID, (a) => lychee.goto(albumID + '/' + a.id))) + items = items.concat(buildAlbumList(data.content, [photoID], (a) => lychee.goto(albumID + '/' + a.id))) } @@ -292,7 +309,7 @@ contextMenu.move = function(photoIDs, e) { } else { - items = buildAlbumList(data.albums, album.getID(), (a) => photo.setAlbum(photoIDs, a.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') { From ffef7e3d8fbbbbe58bb311f4f08c85b56b07b028 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 14:51:24 +0200 Subject: [PATCH 11/13] Fixed bug in multiselect, introduced by 6d4df5f. --- src/scripts/multiselect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/multiselect.js b/src/scripts/multiselect.js index a742734..5420f1d 100644 --- a/src/scripts/multiselect.js +++ b/src/scripts/multiselect.js @@ -199,7 +199,7 @@ multiselect.getSelection = function(e) { let id = $(this).data('id') - if (!album.isSmartID(id) && id!=null) { + if (id!=null && id!==0 && !album.isSmartID(id)) { ids.push(id) $(this).addClass('active') From f7e31c2be7b7e4ae6a6eb24c17e5fac96ee5e946 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 15:04:03 +0200 Subject: [PATCH 12/13] Fixed bug in album.setTitle. --- src/scripts/album.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/scripts/album.js b/src/scripts/album.js index e946b03..66fe9b2 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -340,8 +340,11 @@ album.setTitle = function(albumIDs) { if (albums.json || album.subjson) { albumIDs.forEach(function(id) { - albums.getByID(id).title = newTitle - view.album.content.title(id) + let a = albums.getByID(id) + if (a) { + a.title = newTitle + view.album.content.title(id) + } }) } From 8b37d122b9eb5a8902d71d2e88d990a3906194a9 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 30 Jul 2016 15:11:49 +0200 Subject: [PATCH 13/13] Fixed bug in countSubAlbums. --- src/scripts/contextMenu.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index 382beff..ba46ce0 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -211,11 +211,13 @@ contextMenu.photo = function(photoID, e) { function countSubAlbums(photoIDs) { let count = 0 - for (i in photoIDs) { - for (j in album.subjson.albums) { - if (album.subjson.albums[j].id == photoIDs[i]) { - count++ - break + if (album.subjson) { + for (i in photoIDs) { + for (j in album.subjson.albums) { + if (album.subjson.albums[j].id == photoIDs[i]) { + count++ + break + } } } }