Compare commits

..

58 Commits

Author SHA1 Message Date
Tobias Reich 727c74c294 Fixed the search
8 years ago
Tobias Reich 635b31eb0b Always create new album in current album
8 years ago
Tobias Reich a0abb1a460 Rebuild
8 years ago
Tobias Reich f460569bdc Merge branch 'develop' into feature/subalbums
8 years ago
Tobias Reich a2def81b52 Just a small code adjustment
8 years ago
Tobias Reich ab3a5a4d63 Merge pull request #611 from hrniels/multiselect
8 years ago
Nils Asmussen 3f7f2ffcd1 Disallow to select a mixture of albums and photos.
8 years ago
Nils Asmussen a765b5ee06 Support metaKey and ctrlKey for selection.
8 years ago
Nils Asmussen 425749eb85 Fixed condition in {album,photo}ContextMenu.
8 years ago
Tobias Reich 7002e039b3 Merge branch 'develop' into feature/subalbums
8 years ago
Tobias Reich 0cb50e5090 Updated keyboard shortcuts doc as we are now using the meta key to trigger selection
8 years ago
Tobias Reich 1fc3ca05cc Merge branch 'develop' into feature/subalbums
8 years ago
Tobias Reich bdd2aca6f2 Custom function to check if id is already selected
8 years ago
Tobias Reich d2c7cab3e3 Prevent smart albums to be part of a selection #603
8 years ago
Tobias Reich ff5637a7da Use cmd on macOS and ctrl on Windows to trigger multi select of individual items
8 years ago
Tobias Reich 4b08f7e6f0 Trim whitespace on save
8 years ago
Tobias Reich 1bf8f8d2f8 Merge pull request #603 from hrniels/multiselect-contextmenu
8 years ago
Nils Asmussen 774fda3a7c Don't show context menu on finished multiselect.
8 years ago
Tobias Reich e64e8ae621 Removed select all
8 years ago
Tobias Reich 9d5e9a4137 Code style adjustment
8 years ago
Tobias Reich 5c18fbeed8 Use shiftkey for multiselect
8 years ago
Tobias Reich 4c58ae0733 Code style adjustment
8 years ago
Tobias Reich 993738af49 Merge branch 'multiselect' of https://github.com/hrniels/Lychee into hrniels-multiselect
8 years ago
Tobias Reich 7ba384e70a Updated to minor version
8 years ago
Tobias Reich 9548f43b06 Use new album.isSmartID to check if it's a smart album
8 years ago
Tobias Reich 5bbac75481 Merge branch 'develop' into feature/subalbums
8 years ago
Nils Asmussen 7ebe2a1aa0 Clear selected items when multiselect starts.
8 years ago
Nils Asmussen ea073b4324 Use outline for selected items, not border.
8 years ago
Tobias Reich 55ae8a4be2 Merge pull request #591 from hrniels/subalbums
8 years ago
Nils Asmussen 0746acda79 Allow to select photos/albums individually (fixes #339).
8 years ago
Nils Asmussen 3040ab1d62 Improved display of root album in move operation.
8 years ago
Nils Asmussen 5ee34c50a4 Properly redirect after album {delete,move,merge}.
8 years ago
Nils Asmussen 7126354d7a Hide "Photos" divider if photos are deleted.
8 years ago
Nils Asmussen 58d3ad727d Hide download button only w/o photos and subalbums.
8 years ago
Nils Asmussen 20c194f00c Always show merge&move operations in context menu.
8 years ago
Nils Asmussen b91759e6c1 Added a move album operation.
8 years ago
Nils Asmussen 9b5328d888 Don't validate merge request on server.
8 years ago
Nils Asmussen adaa23d39f Syntax adjustments in album.js.
8 years ago
Nils Asmussen 96bd6b240b Better highlighting of selected items.
8 years ago
Tobias Reich 61a62421f4 Check if album.json exists
8 years ago
Tobias Reich 066fc90056 Syntax adjustment for contextMenu.js
8 years ago
Tobias Reich 2b698ca024 Removed disabled context menu item class
8 years ago
Tobias Reich 2da195f925 Use disabled functionality of basicContext
8 years ago
Tobias Reich 0c7d95fb15 Syntax adjustments
8 years ago
Tobias Reich 1020b0d1dc Merge branch 'subalbums' of https://github.com/hrniels/Lychee into feature/subalbums
8 years ago
Nils Asmussen 8b37d122b9 Fixed bug in countSubAlbums.
8 years ago
Nils Asmussen f7e31c2be7 Fixed bug in album.setTitle.
8 years ago
Nils Asmussen ffef7e3d8f Fixed bug in multiselect, introduced by 6d4df5f.
8 years ago
Nils Asmussen daf7d37c84 Prevent album merge errors on client-side.
8 years ago
Nils Asmussen cdd130b243 Let the user know that deleting an album deletes subalbums, too.
8 years ago
Nils Asmussen d121e2c4f6 Go to parent album on ESC.
8 years ago
Nils Asmussen 55247e4f56 Load album view just once.
8 years ago
Nils Asmussen 6feae99bf1 Include subalbums in setPublic.
8 years ago
Nils Asmussen 1b6c74bcb6 Merge albums including subalbums.
8 years ago
Nils Asmussen eddb666b5a Delete albums including subalbums.
8 years ago
Nils Asmussen 212b241d0e Include subalbums in album archives.
8 years ago
Nils Asmussen ef9040870a Added basic subalbum support.
8 years ago
Nils Asmussen 6d4df5f6b7 Introduced album.isSmartID.
8 years ago

@ -1,47 +0,0 @@
---
kind: pipeline
name: default
platform:
os: linux
arch: amd64
steps:
- name: publish
pull: default
image: plugins/docker:18.09
settings:
registry: https://registry.nixaid.com
repo: "registry.nixaid.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}"
tags:
- latest
username:
from_secret: docker_username
password:
from_secret: docker_password
# storage_path: /drone/docker
# storage_driver: aufs
# ipv6: false
# debug: true
when:
branch:
- master
event:
- push
- tag
- name: notify
pull: default
image: drillster/drone-email:latest
settings:
from: "Drone CI <noreply@nixaid.com>"
host: mx.nixaid.com
port: 587
subject: "NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification"
when:
event:
- push
- tag
status:
- success
- failure

@ -10,13 +10,6 @@ Options -Indexes
# php_value upload_max_filesize 20M
# php_value max_file_uploads 100
#</IfModule>
#<IfModule mod_php7.c>
# php_value max_execution_time 200
# php_value post_max_size 200M
# php_value upload_max_size 200M
# php_value upload_max_filesize 20M
# php_value max_file_uploads 100
#</IfModule>
# ---
# Uncomment these lines when you want to allow access to the Lychee API from different origins

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at lychee@electerious.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

@ -1,58 +0,0 @@
FROM alpine
ARG LYCHEE_VERSION=3.1.6
ENV UID=991 GID=991
RUN BUILD_DEPS=" \
imagemagick-dev \
tar \
libressl \
ca-certificates \
build-base \
autoconf \
pcre-dev \
libtool" \
&& apk -U upgrade && apk add \
${BUILD_DEPS} \
nginx \
php7-mbstring \
php7-fpm \
php7-exif \
php7-gd \
php7-json \
php7-mysqli \
php7-zip \
php7-session \
php7-pear \
php7-dev \
s6 \
su-exec \
imagemagick \
&& pecl install imagick \
&& echo "extension=imagick.so" > /etc/php7/conf.d/imagick.ini \
&& sed -i -e "s/max_execution_time = 30/max_execution_time = 200/g" \
-e "s/post_max_size = 8M/post_max_size = 100M/g" \
-e "s/upload_max_filesize = 2M/upload_max_filesize = 20M/g" \
-e "s/memory_limit = 256M/memory_limit = 512M/g" \
/etc/php7/php.ini \
&& mkdir /lychee \
&& apk del ${BUILD_DEPS} php7-pear php7-dev \
&& rm -rf /var/cache/apk/* /tmp/*
COPY . /lychee/
RUN mv /lychee/docker/nginx.conf /etc/nginx/nginx.conf && \
mv /lychee/docker/php-fpm.conf /etc/php7/php-fpm.conf && \
mv /lychee/docker/s6.d /etc/ && \
mv /lychee/docker/run.sh /usr/local/bin/run.sh
RUN chmod +x /usr/local/bin/run.sh /etc/s6.d/*/* /etc/s6.d/.s6-svscan/*
VOLUME /lychee/uploads /lychee/data
EXPOSE 8888
LABEL maintainer="Andrey Arapov <andrey.arapov@nixaid.com>"
CMD ["run.sh"]

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2017 Tobias Reich
Copyright (c) 2016 Tobias Reich (http://electerious.com/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

@ -1,15 +1,11 @@
[![Build Status](https://drone.nixaid.com/api/badges/arno/lychee/status.svg)](https://drone.nixaid.com/arno/lychee)
# Lychee
#### A great looking and easy-to-use photo-management-system.
*Since the 1st of April 2018 this project has moved to it's own Organisation (https://github.com/LycheeOrg) where people are able to submit their fixes to it. We, the Organisation owners, want to thank electerious (Tobias Reich) for the opportunity to make this project live on.*
![Lychee](https://s.electerious.com/images/lychee/1.jpeg)
![Lychee](https://s.electerious.com/images/lychee/2.jpeg)
![Lychee](http://l.electerious.com/uploads/big/c4b58cb87d95aeaed78fdca581cc908c.jpg)
![Lychee](http://l.electerious.com/uploads/big/075ac5de5b5d6c593acbb700f0e1d739.jpg)
Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely. Read more on our [website](http://lychee.electerious.com).
Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely. Try the [Live Demo](http://ld.electerious.com) or read more on our [Website](http://lychee.electerious.com).
## Installation
@ -21,7 +17,7 @@ You can use Lychee right after the installation. Here are some advanced features
### Settings
Sign in and click the gear in the top left corner to change your settings. If you want to edit them manually: MySQL details are stored in `data/config.php`. Other options and hidden settings are stored directly in the database. [Settings &#187;](docs/Settings.md)
Sign in and click the gear on the top left corner to change your settings. If you want to edit them manually: MySQL details are stored in `data/config.php`. Other options and hidden settings are stored directly in the database. [Settings &#187;](docs/Settings.md)
### Update
@ -33,7 +29,7 @@ Lychee is ready to use, right out of the box. If you want to contribute and edit
### Keyboard Shortcuts
These shortcuts will help you to use Lychee even faster. [Keyboard Shortcuts &#187;](docs/Keyboard%20Shortcuts.md)
These shortcuts will help you to use Lychee even faster. [Keyboard Shortcuts &#187;](docs/Keyboard Shortcuts.md)
### Dropbox import
@ -49,13 +45,13 @@ Lychee uses [Imagick](http://www.imagemagick.org) when installed on your server.
### Docker
Browse the [Docker Hub Registry](https://hub.docker.com/r/kdelfour/lychee-docker/) for various automated Lychee-Docker builds. We recommend using [lychee-docker](https://hub.docker.com/r/kdelfour/lychee-docker/) by [kdelfour](https://github.com/kdelfour).
Browse the [Docker Hub Registry](https://hub.docker.com/r/kdelfour/lychee-docker/) for various automated Lychee-Docker builds. We recommed to use [lychee-docker](https://hub.docker.com/r/kdelfour/lychee-docker/) by [kdelfour](https://github.com/kdelfour).
### Plugins and Extensions
The plugin-system of Lychee allows you to execute scripts when a certain action fires. Plugins are hooks, which are injected directly into Lychee. [Plugin documentation &#187;](docs/Plugins.md)
The plugin-system of Lychee allows you to execute scripts, when a certain action fires. Plugins are hooks, which are injected directly into Lychee. [Plugin documentation &#187;](docs/Plugins.md)
It's also possible to build extensions upon Lychee. The way to do so isn't documented and can change every time. We recommend using the plugin-system, when possible.
It's also possible to build extensions upon Lychee. The way to do so isn't documented and can change every time. We recommend to use the plugin-system, when possible.
Here's a list of all available Plugins and Extensions:
@ -74,3 +70,7 @@ Here's a list of all available Plugins and Extensions:
## Troubleshooting
Take a look at the [FAQ](docs/FAQ.md) if you have problems. Discovered a bug? Please create an issue here on GitHub!
## Donate
I am working hard on continuously developing and maintaining Lychee. Please consider making a donation via [Flattr](https://flattr.com/submit/auto?user_id=electerious&url=http%3A%2F%2Flychee.electerious.com&title=Lychee&category=software) or PayPal (from [our site](http://lychee.electerious.com/)) to keep the project going strong and me motivated.

2
dist/main.css vendored

File diff suppressed because one or more lines are too long

21
dist/main.js vendored

File diff suppressed because one or more lines are too long

8
dist/view.js vendored

File diff suppressed because one or more lines are too long

@ -1,26 +0,0 @@
## wonderfall/lychee
![](http://lychee.electerious.com/assets/images/showcase.jpg)
#### What is this?
Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.
#### Features
- Based on Alpine Linux.
- nginx + PHP7.
- Active Imagemagick + pecl ext.
#### Build-time variables
- **LYCHEE_VERSION** : version of Lychee.
- **IMAGICK_EXT_VERSION** : version of imagick pecl extension.
#### Environment variables
- **UID** : lychee user id *(default : 991)*.
- **GID** : lychee group id *(default : 991)*.
#### Volumes
- **/lychee/uploads** : uploads.
- **/lychee/data** : data files.
#### Ports
- **8888** [(reverse proxy!)](https://github.com/hardware/mailserver/wiki/Reverse-proxy-configuration)

@ -1,88 +0,0 @@
worker_processes auto;
pid /tmp/nginx.pid;
daemon off;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
error_log /tmp/ngx_error.log error;
sendfile on;
keepalive_timeout 15;
keepalive_disable msie6;
keepalive_requests 100;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
fastcgi_temp_path /tmp/fastcgi 1 2;
client_body_temp_path /tmp/client_body 1 2;
proxy_temp_path /tmp/proxy 1 2;
uwsgi_temp_path /tmp/uwsgi 1 2;
scgi_temp_path /tmp/scgi 1 2;
gzip on;
gzip_comp_level 5;
gzip_min_length 512;
gzip_buffers 4 8k;
gzip_proxied any;
gzip_vary on;
gzip_disable "msie6";
gzip_types
text/css
text/javascript
text/xml
text/plain
text/x-component
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/vnd.ms-fontobject
font/truetype
font/opentype
image/svg+xml;
server {
listen 8888;
root /lychee;
index index.php index.html;
client_max_body_size 100M;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|woff)\$ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public, mustrevalidate, proxy-revalidate";
}
location ~ \.php$ {
fastcgi_split_path_info ^(.*\.php)(/.*)?$;
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
location ~ /\.ht {
deny all;
}
}
}

@ -1,11 +0,0 @@
[global]
daemonize = no
[www]
listen = /tmp/php-fpm.sock
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /

@ -1,4 +0,0 @@
#!/bin/sh
mkdir /lychee/uploads/big /lychee/uploads/import /lychee/uploads/medium /lychee/uploads/thumb
chown -R $UID:$GID /lychee /etc/nginx /etc/php7 /var/log /var/lib/nginx /tmp /etc/s6.d
exec su-exec $UID:$GID /bin/s6-svscan /etc/s6.d

@ -1,3 +0,0 @@
#!/bin/sh
exit 0

@ -1,2 +0,0 @@
#!/bin/sh
exec nginx

@ -1,2 +0,0 @@
#!/bin/sh
exec php-fpm7

@ -1,10 +1,3 @@
## v3.1.6
Released March 20, 2017
- `Fixed` Downloading a SmartAlbum results in crash (#652)
- `Fixed` htaccess IfModule for PHP7 (#653)
## v3.1.5
Released October 25, 2016

@ -27,7 +27,7 @@ You can also use the [direct download](https://github.com/electerious/Lychee/arc
Change the permissions of `uploads/`, `data/` and all their subfolders. Sufficient read/write privileges are required.
chmod -R 750 uploads/ data/
chmod -R 777 uploads/ data/
### 4. Finish

@ -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 |

@ -26,9 +26,9 @@ final class Admin extends Access {
case 'Album::setTitle': self::setAlbumTitleAction(); break;
case 'Album::setDescription': self::setAlbumDescriptionAction(); break;
case 'Album::setPublic': self::setAlbumPublicAction(); break;
case 'Album::setPosition': self::setPositionAction(); 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;
@ -73,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']));
}
@ -91,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);
}
@ -116,15 +118,6 @@ final class Admin extends Access {
}
private static function setPositionAction() {
Validator::required(isset($_POST['albumID'],$_POST['photoOrder']), __METHOD__);
$album = new Album($_POST['albumID']);
Response::json($album->setPosition());
}
private static function setAlbumPublicAction() {
Validator::required(isset($_POST['albumID'], $_POST['password'], $_POST['visible'], $_POST['downloadable']), __METHOD__);
@ -151,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() {

@ -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']));
}

@ -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,15 +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;
}
// Check if album empty
if ($photos->num_rows==0) {
Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create ZipArchive without images');
return false;
// 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;
}
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()) {
@ -269,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
@ -279,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)) {
@ -287,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++;
@ -302,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;
}
/**
@ -344,41 +380,6 @@ final class Album {
}
public function setPosition(){
// Check dependencies
Validator::required(isset($_POST['photoOrder']), __METHOD__);
// Call plugins
Plugins::get()->activate(__METHOD__, 0, func_get_args());
$id_list = implode(',', $_POST['photoOrder']);
$indices = [];
$size = count(explode(',',$id_list));
for($i = 0; $i < $size; $i++){
$indices[$i] = $i;
}
$whens = implode(
" ",
array_map(
function ($id, $value) {
return "WHEN {$id} THEN {$value}";
},
explode(',',$id_list),
$indices
)
);
$query = Database::prepare(Database::get(), "UPDATE ? SET position = CASE id ? END WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $whens, $id_list));
$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.
*/
@ -474,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.
*/
@ -490,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;
@ -499,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;
@ -608,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__);
@ -617,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__);
@ -628,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.
*/
@ -639,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;
@ -664,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

@ -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__);

@ -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
);
/**

@ -208,7 +208,7 @@ final class Photo {
$info = $this->getInfo($path);
// Use title of file if IPTC title missing
if ($info['title']==='') $info['title'] = substr(basename($file['name'], $extension), 0, 100);
if ($info['title']==='') $info['title'] = substr(basename($file['name'], $extension), 0, 30);
if ($exists===false) {
@ -709,7 +709,7 @@ final class Photo {
// Parse photo
$photo['sysdate'] = strftime('%d %b. %Y', substr($photo['id'], 0, -4));
if (strlen($photo['takestamp'])>1) $photo['takedate'] = strftime('%d %b. %Y %T', $photo['takestamp']);
if (strlen($photo['takestamp'])>1) $photo['takedate'] = strftime('%d %b. %Y', $photo['takestamp']);
// Parse medium
if ($photo['medium']==='1') $photo['medium'] = LYCHEE_URL_UPLOADS_MEDIUM . $photo['url'];
@ -832,7 +832,7 @@ final class Photo {
}
// Read EXIF
if ($info['mime']=='image/jpeg') $exif = @exif_read_data($url, 'EXIF, IFD0', false, false);
if ($info['mime']=='image/jpeg') $exif = @exif_read_data($url, 'EXIF', false, false);
else $exif = false;
// EXIF Metadata

@ -24,8 +24,7 @@ CREATE TABLE IF NOT EXISTS `?` (
`album` bigint(20) unsigned NOT NULL,
`checksum` char(40) DEFAULT NULL,
`medium` tinyint(1) NOT NULL DEFAULT '0',
`position` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `Index_album` (`album`),
KEY `Index_star` (`star`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

@ -1,25 +0,0 @@
<?php
/**
* Update to version 3.1.7
*/
use Lychee\Modules\Database;
use Lychee\Modules\Response;
// Add position to photos
$query = Database::prepare($connection, "SELECT `position` FROM `?` LIMIT 1", array(LYCHEE_TABLE_PHOTOS));
$result = Database::execute($connection, $query, 'update_030107', __LINE__);
if ($result===false) {
$query = Database::prepare($connection, "ALTER TABLE `?` ADD `position` TINYINT(1) NOT NULL", array(LYCHEE_TABLE_PHOTOS));
$result = Database::execute($connection, $query, 'update_030107', __LINE__);
if ($result===false) Response::error('Could not add position to database!');
}
// Set version
if (Database::setVersion($connection, '030107')===false) Response::error('Could not update version of database!');
?>

@ -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!');
?>

@ -83,7 +83,6 @@ paths.main = {
],
scripts: [
'node_modules/jquery/dist/jquery.min.js',
'node_modules/jquery-ui-dist/jquery-ui.min.js',
'node_modules/mousetrap/mousetrap.min.js',
'node_modules/mousetrap/plugins/global-bind/mousetrap-global-bind.min.js',
'node_modules/basiccontext/dist/basicContext.min.js',

@ -1,6 +1,6 @@
{
"name": "Lychee",
"version": "3.1.6",
"version": "3.2.0",
"description": "Self-hosted photo-management done right.",
"authors": "Tobias Reich <tobias@electerious.com>",
"license": "MIT",
@ -14,21 +14,20 @@
"compile": "gulp"
},
"dependencies": {
"babel-preset-es2015": "^6.24.0",
"babel-preset-es2015": "^6.18.0",
"basiccontext": "^3.5.1",
"basicmodal": "^3.3.8",
"basicmodal": "^3.3.7",
"gulp": "^3.9.1",
"gulp-autoprefixer": "3.1.1",
"gulp-babel": "^6.1.2",
"gulp-concat": "^2.6.1",
"gulp-inject": "^4.2.0",
"gulp-load-plugins": "^1.5.0",
"gulp-concat": "^2.6.0",
"gulp-inject": "^4.1.0",
"gulp-load-plugins": "^1.3.0",
"gulp-minify-css": "^1.2.4",
"gulp-rimraf": "^0.2.1",
"gulp-sass": "^3.1.0",
"gulp-uglify": "^2.1.1",
"jquery": "^3.2.0",
"jquery-ui": "^1.12.1",
"gulp-rimraf": "^0.2.0",
"gulp-sass": "^2.3.2",
"gulp-uglify": "^2.0.0",
"jquery": "^3.1.1",
"mousetrap": "^1.6.0"
}
}

@ -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,38 +40,34 @@ 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
}
album.load = function(albumID, refresh = false) {
$('.content').sortable({
stop: function(event, ui){
let albumID = ui.item.data('album-id');
let photoOrder = $('.content').children().map(function(){
return $(this).data('id');
}).get();
let params = {
albumID: albumID,
photoOrder: photoOrder
};
api.post('Album::setPosition', params, function(data) {
if (data!==true) lychee.error(null, params, data)
})
}
});
password.get(albumID, function() {
@ -93,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)
})
})
}
@ -114,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) {
@ -141,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
}
}
}
})
})
}
@ -184,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()
@ -212,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>`
}
@ -257,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
}
@ -272,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()) {
@ -553,7 +639,7 @@ album.getArchive = function(albumID) {
}
album.merge = function(albumIDs) {
const getMessage = function(albumIDs, titles, operation) {
let title = ''
let sTitle = ''
@ -563,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'
@ -571,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()
@ -594,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',
@ -620,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
}

@ -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 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

@ -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

@ -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
// 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)
}
items = buildAlbumList(data.albums, exclude, (a) => album.merge([ albumID, a.id ], [ title, a.title ]))
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 (items.length===0) return false
if (this.id!=albumID) items.push({
title: html,
fn: () => album.merge([ albumID, this.id ])
})
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()
}

@ -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()

@ -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) })

@ -5,8 +5,8 @@
lychee = {
title : document.title,
version : '3.1.7',
versionCode : '030107',
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)
}
}

@ -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')
if (id!=='0' && id!==0 && id!=='f' && id!=='s' && id!=='r' && id!=null) {
let id = $(this).attr('data-id')
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()
}

@ -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',

@ -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
}

@ -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()
}
})

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

Loading…
Cancel
Save