From ef9040870a25df51c58a821e89d915d577d2d54d Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Thu, 28 Jul 2016 16:01:36 +0200 Subject: [PATCH] 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;