Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
241e8b8ea1 | ||
|
|
e0f13489a4 | ||
|
|
1ca3137dd1 | ||
|
|
b791c609c2 | ||
|
|
388feba7c0 | ||
|
|
542d3efdf5 | ||
|
|
a57815a2ea | ||
|
|
44c0d5a9c9 | ||
|
|
ebb5282579 | ||
|
|
d03ce97c8b | ||
|
|
a4ca866790 | ||
|
|
73b09d28c6 | ||
|
|
5c45be0099 | ||
|
|
150f27bf9b | ||
|
|
d0b388834f | ||
|
|
e951a44324 | ||
|
|
9fcefcdbd4 | ||
|
|
05892366d8 | ||
|
|
989412046b | ||
|
|
f2baf273ca | ||
|
|
420a37ee75 | ||
|
|
940faed363 | ||
|
|
f86b0e71d1 | ||
|
|
7f99d73691 | ||
|
|
4633fdacef | ||
|
|
bf0e2d47b9 | ||
|
|
f044c1e953 | ||
|
|
277053d2a8 | ||
|
|
b450ea7607 | ||
|
|
510bfb3eed | ||
|
|
673d80cec2 | ||
|
|
d8498b89fd | ||
|
|
846d8a1bfe | ||
|
|
75dde62989 | ||
|
|
d9c094ff16 | ||
|
|
37244e866c | ||
|
|
124ccd6ec2 | ||
|
|
3fb3bac3ef | ||
|
|
00ccb56c8a | ||
|
|
8696cd43c4 | ||
|
|
313bae584c | ||
|
|
63651bd277 | ||
|
|
04d38bfdf7 | ||
|
|
d05827102e | ||
|
|
eb0dac2b4b | ||
|
|
9f130e1761 | ||
|
|
1b580a2e76 | ||
|
|
2a7cb615ba | ||
|
|
6e9a8a79c8 | ||
|
|
a7704987a3 | ||
|
|
6d155a57ab | ||
|
|
cc99fba3fa | ||
|
|
cd699dd6e8 | ||
|
|
c84d5cf691 | ||
|
|
ac301e104c | ||
|
|
b0af4ccd75 | ||
|
|
6b71bbaf35 | ||
|
|
0f3b99e617 | ||
|
|
ad5032b720 | ||
|
|
7e60f4876a | ||
|
|
8264356176 | ||
|
|
52b1005b69 | ||
|
|
c7c8a7d786 | ||
|
|
12e0bf093a | ||
|
|
2c4416985b | ||
|
|
123d597c5f | ||
|
|
096bf1acc3 | ||
|
|
5349d327ed | ||
|
|
ad0e799601 | ||
|
|
ff8ab141f9 | ||
|
|
ebb9cafb1e | ||
|
|
d6cbaa1a6f | ||
|
|
2fae7345d9 | ||
|
|
1f93835060 | ||
|
|
2786a3bda2 | ||
|
|
9a283274e0 | ||
|
|
8953ddf840 | ||
|
|
f40c8e1195 | ||
|
|
e794edd6d1 | ||
|
|
b8401e259e | ||
|
|
afb5eb3ecd | ||
|
|
551b9cadc5 | ||
|
|
0b106f6b99 | ||
|
|
04b220a665 | ||
|
|
b1c73555aa | ||
|
|
37feb11430 | ||
|
|
9c896f1a59 | ||
|
|
e014bc229b | ||
|
|
9787a7210a | ||
|
|
662ef1c461 | ||
|
|
54d8bf12a8 | ||
|
|
fb51b92e09 | ||
|
|
96dc4758ef | ||
|
|
584e9d2dd2 | ||
|
|
dee5d0a22e | ||
|
|
682923726c | ||
|
|
f8807288c6 | ||
|
|
fc2b866238 | ||
|
|
373ec23bbc | ||
|
|
b0b905b139 | ||
|
|
49f1838ed0 | ||
|
|
2c9eca80e3 | ||
|
|
96072d0a2a | ||
|
|
b2a408a207 | ||
|
|
8eb2c200c8 | ||
|
|
be80462d2e | ||
|
|
9d9e29e906 | ||
|
|
fcd1b1489b | ||
|
|
cacd1a46ee | ||
|
|
d3bde59421 | ||
|
|
de1dff9f35 | ||
|
|
096e2b3e07 | ||
|
|
bc7a7fabf0 | ||
|
|
6561a60bdc | ||
|
|
5373ec63a8 | ||
|
|
23a74b2b86 | ||
|
|
51e0483652 | ||
|
|
6766ed6aef | ||
|
|
dbd68796e8 | ||
|
|
3388577790 | ||
|
|
5da1e9ee03 | ||
|
|
992850d99b | ||
|
|
1f7559effc | ||
|
|
4cb9f4adf5 | ||
|
|
239f49b2e4 | ||
|
|
6f9c1bcdf4 | ||
|
|
3a11b93d35 | ||
|
|
4b11f1b38c | ||
|
|
b3420967ee | ||
|
|
067b2198fd | ||
|
|
b1ecf6aa4b | ||
|
|
47aa06d3b0 |
4
.gitignore
vendored
@@ -3,3 +3,7 @@ lutim.db
|
||||
script/hypnotoad.pid
|
||||
local/*
|
||||
files/*
|
||||
templates/data.html.ep
|
||||
public/img/rezopole.png
|
||||
public/img/rezopole.xcf
|
||||
public/packed/*
|
||||
|
||||
47
Changes
Normal file
@@ -0,0 +1,47 @@
|
||||
Revision history for Lutim
|
||||
|
||||
0.4 2014-07-12
|
||||
- Webapp ! Downloadable directly from the Lutim instance
|
||||
- Configure expiration delay after uploading (#12)
|
||||
- Twitter share button in the "upload success" message (#35)
|
||||
|
||||
0.3 2014-06-01
|
||||
- Add a delete link to images (#28)
|
||||
- Concatenated css and js with Mojolicious::Plugin::AssetPack
|
||||
- Antiflood protection for the "Download by URL" feature (#29)
|
||||
- Stats page improved
|
||||
- Self-documented configuration template
|
||||
- Remote port detection can now use the X-Remote-Port header if set
|
||||
- Lutim now uses the X-Forwarded-Proto header to set the scheme to https if needed
|
||||
The "https" option in configuration file is deprecated and will be removed in 0.4
|
||||
- Optionally delete images that are no longer viewed after a configurable delay in order to avoid saturation (#27)
|
||||
- Provide init script
|
||||
- Update Shutter plugin
|
||||
- Small bugfixes
|
||||
|
||||
0.2 2014-03-07
|
||||
- Server-side encryption available
|
||||
- Thumbnails of uploaded images in response
|
||||
- Bugfixes
|
||||
- HTML validity
|
||||
- Stats (via cron stats command)
|
||||
- Anonymize IP in DB after a delay (via cron cleanbdd command)
|
||||
- Watch files directory size (via cron watch command)
|
||||
- Anonymize logs (log only the senders' IP address)
|
||||
- Favicon and logo
|
||||
- Better MIME type detection
|
||||
- Broadcast message on all pages available
|
||||
- File max size configurable
|
||||
- Progress bar
|
||||
- More options for suppression delay
|
||||
- Updated documentation
|
||||
- Cross-domain API
|
||||
- Upload by image URL
|
||||
- Add HTTP headers Expires and Content-Cache
|
||||
|
||||
0.1 2014-02-15
|
||||
- Image viewing link
|
||||
- Image downloading link
|
||||
- Image twitter card link
|
||||
- Shutter Plugin
|
||||
- Configurable "Hosted by" information
|
||||
265
README.md
@@ -1,61 +1,252 @@
|
||||
#LUTIm
|
||||
# Lutim
|
||||
|
||||
##What LUTIm means?
|
||||
## What Lutim means?
|
||||
It means Let's Upload That Image.
|
||||
|
||||
##What does it do?
|
||||
It stores images and allows you to see them or download them.
|
||||
Images are indefinitly stored unless you request that they will be deleted at first view or after 24 hours.
|
||||
## What does it do?
|
||||
It stores images and allows you to see them, download them or use them in Twitter.
|
||||
Images are indefinitly stored unless you request that they will be deleted at first view or after 24 hours / one week / one month / one year.
|
||||
|
||||
##License
|
||||
LUTIm is licensed under the terms of the AGPL. See the LICENSE file.
|
||||
## License
|
||||
Lutim is licensed under the terms of the AGPL. See the LICENSE file.
|
||||
|
||||
##Dependancies
|
||||
* Carton : Perl dependancies manager, it will get what you need, so don't bother for dependancies (but you can read the file `cpanfile` if you want).
|
||||
## Official instance
|
||||
You can see it working at http://lut.im.
|
||||
|
||||
## Logo
|
||||
Lutim's logo is an adaptation of [Lutin](http://commons.wikimedia.org/wiki/File:Lutin_by_godo.jpg) by [Godo](http://godoillustrateur.wordpress.com/), licensed under the terms of the CC-BY-SA 3.0 license.
|
||||
|
||||

|
||||
|
||||
## Dependencies
|
||||
* Carton : Perl dependencies manager, it will get what you need, so don't bother for Perl modules dependencies (but you can read the file `cpanfile` if you want).
|
||||
|
||||
```shell
|
||||
sudo cpan Carton
|
||||
```
|
||||
|
||||
##Installation
|
||||
or
|
||||
|
||||
```shell
|
||||
sudo apt-get install carton
|
||||
```
|
||||
|
||||
* But, on another hand, some modules that Carton will install need to be compiled. So you will need some tools:
|
||||
|
||||
```shell
|
||||
sudo apt-get install build-essential libssl-dev
|
||||
```
|
||||
|
||||
### Thumbnails dependancy
|
||||
If you want to provide thumbnails of uploaded images, you have to install the *ImageMagick* image manipulation software (<http://www.imagemagick.org/>) and the Image::Magick CPAN module.
|
||||
|
||||
On Debian, you can do:
|
||||
```shell
|
||||
sudo apt-get install perlmagick
|
||||
```
|
||||
|
||||
## Installation
|
||||
After installing Carton :
|
||||
```shell
|
||||
git clone https://github.com/ldidry/lutim.git
|
||||
cd lutim
|
||||
carton install
|
||||
cp lutim.conf.template lutim.conf
|
||||
vi lutim.conf
|
||||
```
|
||||
|
||||
##Usage
|
||||
## Configuration
|
||||
The `lutim.conf.template` is self-documented but here is the options that you can set:
|
||||
|
||||
* **hypnotoad :** address and port to listen to, user and group which runs hypnotoad (if you run Lutim with a different user from what is defined here, be sure that the user which launchs hypnotoad is able to setuid/setgid to the defined user/group, otherwise it will not work and you'll have 100% CPU consumption. Launch hypnotoad with the root user or with the user which is defined here);
|
||||
* **contact :** write something which make people able to contact you (contact form URL, email address, whatever);
|
||||
* **secrets :** an array of random string. Used by Mojolicious for encrypting session cookies.
|
||||
* **piwik_img :** the Piwik image provides you records of visits without javascript (better privacy than js and cookies);
|
||||
* **length :** length of the random string part of image's URL (default is 8);
|
||||
* **provis_step :** Lutim provisions random strings for image's URL per pack of `provis_step` (default is 5);
|
||||
* **provisioning :** number of random strings to provision (default is 100);
|
||||
* **hosted_by :** if someone hosts your Lutim instance, you can add some HTML (a logo for example) to make it appear on index page;
|
||||
* **tweet_card_via :** a Twitter account which will appear on Twitter cards;
|
||||
* **max_file_size :** well, this is explicit (default is 10Mio = 10485760 octets);
|
||||
* **https :** 1 if you want to provide secure images URLs (default is 0) DEPRECATED, PASS A `X-Forwarded-Proto` HEADER TO LUTIM FROM YOUR REVERSE PROXY INSTEAD;
|
||||
* **token_length :** length of the secret token used to allow people to delete their images when they want;
|
||||
* **stats_day_num :** when you generate statistics with `script/lutim cron stats`, you will have stats for the last `stats_day_num` days (default is 365);
|
||||
* **keep_ip_during :** when you delete IP addresses of image's senders with `script/lutim cron cleanbdd`, the IP addresses of images older than `keep_ip_during` days will be deleted (default is 365);
|
||||
* **broadcast_message :** put some string (not HTML) here and this message will be displayed on all Lutim pages (not in JSON responses);
|
||||
* **allowed_domains :** array of authorized domains for API calls. Example: `['http://1.example.com', 'http://2.example.com']`. If you want to authorize everyone to use the API: `['\*']`.
|
||||
* **default_delay :** what is the default time limit for files? Valid values are 0, 1, 7, 30 and 365;
|
||||
* **max_delay :** if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max_delay) option and a warning message will be displayed on homepage;
|
||||
* **always_encrypt :** if set to 1, all images will be encrypted.
|
||||
* **delete_no_longer_viewed_files :** if set, the images which have not been viewed since `delete_no_longer_viewed_files` days will be deleted by the `script/lutim cron cleanfiles` command
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting Lutim from Command line
|
||||
```
|
||||
carton exec hypnotoad script/lutim
|
||||
```
|
||||
|
||||
### Starting Lutim with the init script
|
||||
```
|
||||
cp utilities/lutim.init /etc/init.d/lutim
|
||||
cp utilities/lutim.default /etc/default/lutim
|
||||
chmod +x /etc/init.d/lutim
|
||||
chown root:root /etc/init.d/lutim /etc/default/lutim
|
||||
vim /etc/default/lutim
|
||||
|
||||
/etc/init.d/lutim start
|
||||
```
|
||||
|
||||
## Update
|
||||
```
|
||||
git pull
|
||||
carton install
|
||||
carton exec hypnotoad script/lutim
|
||||
```
|
||||
|
||||
Yup, that's all (Mojolicious magic), it will listen at "http://127.0.0.1:8080".
|
||||
|
||||
For more options (interfaces, user, etc.), change the configuration in `lutim.conf` (have a look at http://mojolicio.us/perldoc/Mojo/Server/Hypnotoad#SETTINGS for the available options).
|
||||
|
||||
##Reverse proxy
|
||||
You can use a reverse proxy like Nginx or Varnish (or Apache with the mod\_proxy module). The web is full of tutos.
|
||||
***Warning!!!***
|
||||
|
||||
If you want to update to Lutim **0.3**, from a previous version, you'll have to modify the database.
|
||||
|
||||
Here's a valid *Varnish* configuration:
|
||||
```
|
||||
backend lutim {
|
||||
.host = "127.0.0.1";
|
||||
.port = "8080";
|
||||
}
|
||||
sub vcl_recv {
|
||||
if (req.restarts == 0) {
|
||||
set req.http.X-Forwarded-For = client.ip;
|
||||
sqlite3 lutim.db
|
||||
PRAGMA writable_schema = 1;
|
||||
UPDATE SQLITE_MASTER SET SQL = 'CREATE TABLE lutim ( short TEXT PRIMARY KEY, path TEXT, footprint TEXT, enabled INTEGER, mediatype TEXT, filename TEXT, counter INTEGER, delete_at_first_view INTEGER, delete_at_day INTEGER, created_at INTEGER, created_by TEXT, last_access_at INTEGER, mod_token TEXT)' WHERE NAME = 'lutim';
|
||||
PRAGMA writable_schema = 0;
|
||||
```
|
||||
|
||||
## Reverse proxy
|
||||
You can use a reverse proxy like Nginx or Varnish (or Apache with the mod_proxy module). The web is full of tutos.
|
||||
|
||||
Here's a valid *Nginx* configuration:
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
root /path/to/lutim/public;
|
||||
|
||||
# This is important for user's privacy !
|
||||
access_log off;
|
||||
error_log /var/log/nginx/lutim.error.log;
|
||||
|
||||
# This is important ! Make it OK with your Lutim configuration
|
||||
client_max_body_size 40M;
|
||||
|
||||
location ~* ^/(img|css|font|js)/ {
|
||||
try_files $uri @lutim;
|
||||
add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
|
||||
add_header Cache-Control "public, max-age=315360000";
|
||||
|
||||
# HTTPS only header, improves security
|
||||
#add_header Strict-Transport-Security "max-age=15768000";
|
||||
}
|
||||
if (req.http.host == "lut.im") {
|
||||
set req.backend = lutim;
|
||||
return(pass);
|
||||
|
||||
location / {
|
||||
try_files $uri @lutim;
|
||||
|
||||
# HTTPS only header, improves security
|
||||
#add_header Strict-Transport-Security "max-age=15768000";
|
||||
}
|
||||
|
||||
location @lutim {
|
||||
# Adapt this to your configuration
|
||||
# My advice: put a varnish between nginx and Lutim, it's really useful when images are widely viewed
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# If you want to log the remote port of the image senders, you'll need that
|
||||
proxy_set_header X-Remote-Port $remote_port;
|
||||
|
||||
# Lutim reads this header and understands that the current session is actually HTTPS.
|
||||
# Enable it if you run a HTTPS server (in this case, don't forgot to change the listen port above)
|
||||
#proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# We expect the downsteam servers to redirect to the right hostname, so don't do any rewrites here.
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##Shutter integration
|
||||
## Cron jobs
|
||||
Lutim have commands which can be used in cron jobs.
|
||||
|
||||
To see what commands are available:
|
||||
```shell
|
||||
carton exec script/lutim cron
|
||||
```
|
||||
|
||||
### Statistics
|
||||
To generate statistics which can be viewed at the address `/stats` (we need to reload hypnotoad after the stats generation):
|
||||
```shell
|
||||
carton exec script/lutim cron stats && carton exec hypnotoad script/lutim
|
||||
```
|
||||
|
||||
### Delete IP adresses from database
|
||||
To automatically delete the IP addresses of image's senders after a configurable delay:
|
||||
```shell
|
||||
carton exec script/lutim cron cleanbdd
|
||||
```
|
||||
|
||||
### Delete expired files
|
||||
To automatically delete files which availability delay is over (when you choose that your image will be deleted after 24h / one week / etc.)
|
||||
If `delete_no_longer_viewed_files`, the files not viewed since `delete_no_longer_viewed_files` days will be deleted too.
|
||||
```shell
|
||||
carton exec script/lutim cron cleanfiles
|
||||
```
|
||||
|
||||
### Watch the size of the files directory
|
||||
To execute an action when the files directory is heavier than `max_total_size`.
|
||||
The available actions are `warn` and `stop-upload`:
|
||||
* `warn` prints a message on the standard out (which is normally mailed to you by `cron`) ;
|
||||
* `stop-upload` prints a message on the standard out and creates the `stop-upload` file which prevents uploading and put a warn on Lutim interface ;
|
||||
* **DANGEROUS OPTION!!!** `delete` prints a message on the standard out and delete older images until the files directory goes under quota.
|
||||
|
||||
If the files directory go under quota, the `stop-upload` file is deleted. If you want to manually prevents uploading, create a file named `stop-upload.manual`.
|
||||
|
||||
```shell
|
||||
carton exec script/lutim cron watch
|
||||
```
|
||||
|
||||
## Broadcast message
|
||||
Set a string in the `broadcast_message` option of `lutim.conf` and reload the server with:
|
||||
```shell
|
||||
carton exec hypnotoad script/lutim
|
||||
```
|
||||
|
||||
It may take a few reloads of page before the message is displayed.
|
||||
|
||||
## Encryption
|
||||
Lutim does encryption on the server if asked to, but does not store the key.
|
||||
|
||||
The encryption is made on the server since Lutim is made to be usable even without javascript. If you want to add client-side encryption for javascript-enabled browsers, patches are welcome.
|
||||
|
||||
## API
|
||||
You can add images by using the API. Here's the parameters of the `POST` request to `/` adress:.
|
||||
* format: json
|
||||
MANDATORY if you want to get a json response, otherwise it will send a web page
|
||||
* file: the image file
|
||||
MANDATORY
|
||||
* delete-day: number of days you want the image to stay
|
||||
OPTIONAL if 0, it will be available undefinitely
|
||||
* first-view: 1
|
||||
OPTIONAL if not 0, the image will be deleted at first view
|
||||
|
||||
|
||||
Exemple with curl:
|
||||
```shell
|
||||
curl -F "format=json" -F "file=@/tmp/snap0001.jpg" http://lut.im
|
||||
```
|
||||
|
||||
You can allow people to use your instance of Lutim from other domains.
|
||||
Add the allowed domains as an array in the `allowed_domains` conf option. Put '`[\*]`' if you want to allow all domains.
|
||||
|
||||
## Shutter integration
|
||||
See where Shutter (<http://en.wikipedia.org/wiki/Shutter_%28software%29>) keeps its plugins on your computer.
|
||||
On my computer, it's in `/usr/share/shutter/resources/system/upload_plugins/upload`.
|
||||
|
||||
@@ -66,22 +257,28 @@ sudo cp utilities/Shutter.pm /usr/share/shutter/resources/system/upload_plugins/
|
||||
|
||||
And restart Shutter if it was running.
|
||||
|
||||
Of course, this plugin is configured for the official instance of LUTIm (<http://lut.im>), feel free to edit it for your own instance.
|
||||
Of course, this plugin is configured for the official instance of Lutim (<http://lut.im>), feel free to edit it for your own instance.
|
||||
|
||||
##Internationalization
|
||||
LUTIm comes with english and french languages. It will choose the language to display from the browser's settings.
|
||||
## Internationalization
|
||||
Lutim comes with English and French languages. It will choose the language to display from the browser's settings.
|
||||
|
||||
If you want to add more languages, for example german:
|
||||
If you want to add more languages, for example German:
|
||||
```shell
|
||||
cd lib/I18N
|
||||
cd lib/Lutim/I18N
|
||||
cp en.pm de.pm
|
||||
vim de.pm
|
||||
```
|
||||
|
||||
There's just a few sentences, so it will be quick to translate. Please consider to send me you language file in order to help the other users :smile:.
|
||||
|
||||
##Others projects dependancies
|
||||
LUTIm is written in Perl with the Mojolicious framework, uses the Twitter bootstrap framework to look not too ugly, JQuery and JQuery File Uploader (<https://github.com/danielm/uploader/>) to add some modernity.
|
||||
## Others projects dependancies
|
||||
Lutim is written in Perl with the [Mojolicious](http://mojolicio.us) framework, uses the [Twitter bootstrap](http://getbootstrap.com) framework to look not too ugly, [JQuery](http://jquery.com) and [JQuery File Uploader](https://github.com/danielm/uploader/) (slightly modified) to add some modernity, [Raphaël](http://raphaeljs.com/) and [morris.js](http://www.oesmith.co.uk/morris.js/) for stats graphs.
|
||||
|
||||
##Official instance
|
||||
You can see it working at http://lut.im.
|
||||
## Main developers
|
||||
* Luc Didry, aka Sky (<http://www.fiat-tux.fr>), core developer, [@framasky](https://twitter.com/framasky)
|
||||
* Dattaz (<http://dattaz.fr>), webapp developer, [@dat_taz](https://twitter.com/dat_taz)
|
||||
|
||||
## Contributors
|
||||
* Jean-Bernard Marcon, aka Goofy (<https://github.com/goofy-bz>)
|
||||
* Jean-Christophe Bach (<https://github.com/jcb>)
|
||||
* Florian Bigard, aka Chocobozzz (<https://github.com/Chocobozzz>)
|
||||
|
||||
@@ -362,19 +362,20 @@
|
||||
"css": [
|
||||
"print.less",
|
||||
"type.less",
|
||||
"grid.less",
|
||||
"tables.less",
|
||||
"forms.less",
|
||||
"buttons.less",
|
||||
"input-groups.less",
|
||||
"alerts.less",
|
||||
"progress-bars.less",
|
||||
"close.less",
|
||||
"component-animations.less",
|
||||
"modals.less",
|
||||
"utilities.less",
|
||||
"responsive-utilities.less"
|
||||
],
|
||||
"js": [
|
||||
"alert.js",
|
||||
"modal.js",
|
||||
"transition.js"
|
||||
]
|
||||
}
|
||||
11
cpanfile
@@ -1,6 +1,15 @@
|
||||
requires 'Mojolicious';
|
||||
requires 'EV';
|
||||
requires 'Data::Validate::URI';
|
||||
requires 'Mojolicious::Plugin::I18N';
|
||||
requires 'Mojolicious::Plugin::ConfigHashMerge';
|
||||
requires 'Mojolicious::Plugin::AssetPack';
|
||||
requires 'ORLite';
|
||||
requires 'MIME::Types';
|
||||
requires 'File::Type';
|
||||
requires 'Text::Unidecode';
|
||||
requires 'DateTime';
|
||||
requires 'Filesys::DiskUsage';
|
||||
requires 'Switch';
|
||||
requires 'Data::Validate::URI';
|
||||
requires 'Crypt::CBC';
|
||||
requires 'Crypt::Blowfish';
|
||||
|
||||
1694
cpanfile.snapshot
58
fontello-config.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "",
|
||||
"css_prefix_text": "icon-",
|
||||
"css_use_suffix": false,
|
||||
"hinting": true,
|
||||
"units_per_em": 1000,
|
||||
"ascent": 850,
|
||||
"glyphs": [
|
||||
{
|
||||
"uid": "c5fd349cbd3d23e4ade333789c29c729",
|
||||
"css": "eye",
|
||||
"code": 59399,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
|
||||
"css": "download",
|
||||
"code": 59397,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
|
||||
"css": "trash",
|
||||
"code": 59396,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "2cb15eb2b295ee3c33fab1530e18a924",
|
||||
"css": "bitcoin",
|
||||
"code": 59395,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "0f6a2573a7b6df911ed199bb63717e27",
|
||||
"css": "github-circled",
|
||||
"code": 59393,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "627abcdb627cb1789e009c08e2678ef9",
|
||||
"css": "twitter",
|
||||
"code": 59392,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "fccd3ea0efb711b849045bee686b1ceb",
|
||||
"css": "spinner",
|
||||
"code": 59398,
|
||||
"src": "mfglabs"
|
||||
},
|
||||
{
|
||||
"uid": "c71d7db10ede1349b3a8ae0293b1dbf8",
|
||||
"css": "flattr",
|
||||
"code": 59394,
|
||||
"src": "zocial"
|
||||
}
|
||||
]
|
||||
}
|
||||
440
lib/Lutim.pm
@@ -1,31 +1,51 @@
|
||||
package Lutim;
|
||||
use Mojo::Base 'Mojolicious';
|
||||
use LutimModel;
|
||||
use MIME::Types 'by_suffix';
|
||||
use Mojo::Util qw(quote);
|
||||
use Mojo::JSON;;
|
||||
use Digest::file qw(digest_file_hex);
|
||||
use Text::Unidecode;
|
||||
use LutimModel;
|
||||
use Crypt::CBC;
|
||||
|
||||
$ENV{MOJO_TMPDIR} = 'tmp';
|
||||
mkdir($ENV{MOJO_TMPDIR}, 0700) unless (-d $ENV{MOJO_TMPDIR});
|
||||
# This method will run once at server start
|
||||
sub startup {
|
||||
my $self = shift;
|
||||
|
||||
$self->plugin('I18N');
|
||||
push @{$self->commands->namespaces}, 'Lutim::Command';
|
||||
|
||||
my $config = $self->plugin('Config');
|
||||
$self->{wait_for_it} = {};
|
||||
|
||||
$self->plugin('I18N');
|
||||
$self->plugin('AssetPack');
|
||||
|
||||
my $config = $self->plugin('ConfigHashMerge', {
|
||||
default => {
|
||||
provisioning => 100,
|
||||
provis_step => 5,
|
||||
length => 8,
|
||||
always_encrypt => 0,
|
||||
anti_flood_delay => 5,
|
||||
tweet_card_via => '@framasky',
|
||||
max_file_size => 10*1024*1024,
|
||||
https => 0,
|
||||
default_delay => 0,
|
||||
max_delay => 0,
|
||||
token_length => 24,
|
||||
}
|
||||
});
|
||||
|
||||
# Default values
|
||||
$config->{provisionning} = 100 unless (defined($config->{provisionning}));
|
||||
$config->{provis_step} = 5 unless (defined($config->{provis_step}));
|
||||
$config->{length} = 8 unless (defined($config->{length}));
|
||||
$config->{provisioning} = $config->{provisionning} if (defined($config->{provisionning}));
|
||||
|
||||
die "You need to provide a contact information in lutim.conf !" unless (defined($config->{contact}));
|
||||
|
||||
$ENV{MOJO_MAX_MESSAGE_SIZE} = $config->{max_file_size};
|
||||
|
||||
$self->secrets($config->{secrets});
|
||||
|
||||
$self->helper(
|
||||
render_file => sub {
|
||||
my $c = shift;
|
||||
my ($filename, $path, $mediatype, $dl) = @_;
|
||||
my ($filename, $path, $mediatype, $dl, $expires, $nocache, $key) = @_;
|
||||
|
||||
$filename = quote($filename);
|
||||
|
||||
@@ -37,13 +57,28 @@ sub startup {
|
||||
);
|
||||
return 500;
|
||||
}
|
||||
$asset = Mojo::Asset::File->new(path => $path);
|
||||
|
||||
$mediatype =~ s/x-//;
|
||||
|
||||
my $headers = Mojo::Headers->new();
|
||||
if ($nocache) {
|
||||
$headers->add('Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate');
|
||||
} else {
|
||||
$headers->add('Expires' => $expires);
|
||||
}
|
||||
$headers->add('Content-Type' => $mediatype.';name='.$filename);
|
||||
$headers->add('Content-Disposition' => $dl.';filename='.$filename);
|
||||
$headers->add('Content-Length' => $asset->size);
|
||||
$c->res->content->headers($headers);
|
||||
|
||||
$c->app->log->debug($key);
|
||||
if ($key) {
|
||||
$asset = $c->decrypt($key, $path);
|
||||
} else {
|
||||
$asset = Mojo::Asset::File->new(path => $path);
|
||||
}
|
||||
$c->res->content->asset($asset);
|
||||
$headers->add('Content-Length' => $asset->size);
|
||||
|
||||
return $c->rendered(200);
|
||||
}
|
||||
);
|
||||
@@ -51,30 +86,38 @@ sub startup {
|
||||
$self->helper(
|
||||
ip => sub {
|
||||
my $c = shift;
|
||||
my @ip = ($c->tx->remote_address eq '127.0.0.1' && $c->app->mode eq 'production') ? $c->tx->req->{content}->{headers}->{headers}->{'x-forwarded-for'}->[0]->[0] : ($c->tx->remote_address);
|
||||
return join(',', @ip);
|
||||
my $ip_only = shift || 0;
|
||||
|
||||
my $proxy = $c->req->headers->header('X-Forwarded-For');
|
||||
|
||||
my $ip = ($proxy) ? $proxy : $c->tx->remote_address;
|
||||
|
||||
my $remote_port = (defined($c->req->headers->header('X-Remote-Port'))) ? $c->req->headers->header('X-Remote-Port') : $c->tx->remote_port;
|
||||
|
||||
return ($ip_only) ? $ip : "$ip remote port:$remote_port";
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
provisionning => sub {
|
||||
provisioning => sub {
|
||||
my $c = shift;
|
||||
|
||||
# Create some short patterns for provisionning
|
||||
if (LutimModel::Lutim->count('WHERE path IS NULL') < $c->config->{provisionning}) {
|
||||
# Create some short patterns for provisioning
|
||||
if (LutimModel::Lutim->count('WHERE path IS NULL') < $c->config->{provisioning}) {
|
||||
for (my $i = 0; $i < $c->config->{provis_step}; $i++) {
|
||||
if (LutimModel->begin) {
|
||||
my $short;
|
||||
do {
|
||||
$short= $c->shortener($c->config->{length});
|
||||
} while (LutimModel::Lutim->count('WHERE short = ?', $short));
|
||||
} while (LutimModel::Lutim->count('WHERE short = ?', $short) || $short eq 'about' || $short eq 'stats' || $short eq 'd' || $short eq 'm');
|
||||
|
||||
LutimModel::Lutim->create(
|
||||
short => $short,
|
||||
counter => 0,
|
||||
enabled => 1,
|
||||
delete_at_first_view => 0,
|
||||
delete_at_day => 0
|
||||
delete_at_day => 0,
|
||||
mod_token => $c->shortener($c->config->{token_length})
|
||||
);
|
||||
LutimModel->commit;
|
||||
}
|
||||
@@ -97,190 +140,209 @@ sub startup {
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
stop_upload => sub {
|
||||
my $c = shift;
|
||||
|
||||
if (-f 'stop-upload' || -f 'stop-upload.manual') {
|
||||
$c->stash(
|
||||
stop_upload => $c->l('stop_upload', $config->{contact})
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
max_delay => sub {
|
||||
my $c = shift;
|
||||
|
||||
return $c->config->{max_delay} if ($c->config->{max_delay} >= 0);
|
||||
|
||||
warn "max_delay set to a negative value. Default to 0.";
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
default_delay => sub {
|
||||
my $c = shift;
|
||||
|
||||
return $c->config->{default_delay} if ($c->config->{default_delay} >= 0);
|
||||
|
||||
warn "default_delay set to a negative value. Default to 0.";
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
is_selected => sub {
|
||||
my $c = shift;
|
||||
my $num = shift;
|
||||
|
||||
return ($num == $c->default_delay) ? 'selected="selected"' : '';
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
crypt => sub {
|
||||
my $c = shift;
|
||||
my $upload = shift;
|
||||
my $filename = shift;
|
||||
|
||||
my $key = $c->shortener(8);
|
||||
|
||||
my $cipher = Crypt::CBC->new(
|
||||
-key => $key,
|
||||
-cipher => 'Blowfish',
|
||||
-header => 'none',
|
||||
-iv => 'dupajasi'
|
||||
);
|
||||
|
||||
$cipher->start('encrypting');
|
||||
|
||||
my $crypt_asset = Mojo::Asset::File->new;
|
||||
|
||||
$crypt_asset->add_chunk($cipher->crypt($upload->slurp));
|
||||
$crypt_asset->add_chunk($cipher->finish);
|
||||
|
||||
my $crypt_upload = Mojo::Upload->new;
|
||||
$crypt_upload->filename($filename);
|
||||
$crypt_upload->asset($crypt_asset);
|
||||
|
||||
return ($crypt_upload, $key);
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
decrypt => sub {
|
||||
my $c = shift;
|
||||
my $key = shift;
|
||||
my $file = shift;
|
||||
|
||||
my $cipher = Crypt::CBC->new(
|
||||
-key => $key,
|
||||
-cipher => 'Blowfish',
|
||||
-header => 'none',
|
||||
-iv => 'dupajasi'
|
||||
);
|
||||
|
||||
$cipher->start('decrypting');
|
||||
|
||||
my $decrypt_asset = Mojo::Asset::File->new;
|
||||
|
||||
open(my $f, "<",$file) or die "Unable to read encrypted file: $!";
|
||||
binmode $f;
|
||||
while (read($f, my $buffer,1024)) {
|
||||
$decrypt_asset->add_chunk($cipher->crypt($buffer));
|
||||
}
|
||||
$decrypt_asset->add_chunk($cipher->finish) ;
|
||||
|
||||
return $decrypt_asset;
|
||||
}
|
||||
);
|
||||
|
||||
$self->helper(
|
||||
delete_image => sub {
|
||||
my $c = shift;
|
||||
my $image = shift;
|
||||
unlink $image->path();
|
||||
$image->update(enabled => 0);
|
||||
}
|
||||
);
|
||||
|
||||
$self->hook(
|
||||
before_dispatch => sub {
|
||||
my $c = shift;
|
||||
$c->stop_upload();
|
||||
|
||||
# API allowed domains
|
||||
if (defined($c->config->{allowed_domains})) {
|
||||
if ($c->config->{allowed_domains}->[0] eq '*') {
|
||||
$c->res->headers->header('Access-Control-Allow-Origin' => '*');
|
||||
} elsif (my $origin = $c->req->headers->origin) {
|
||||
for my $domain ($c->config->{allowed_domains}) {
|
||||
if ($domain->[0] eq $origin) {
|
||||
$c->res->headers->header('Access-Control-Allow-Origin' => $origin);
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Scheme detection
|
||||
if ((defined($c->req->headers->header('X-Forwarded-Proto')) && $c->req->headers->header('X-Forwarded-Proto') eq 'https') || $c->config->{https}) {
|
||||
$c->req->url->base->scheme('https');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$self->hook(
|
||||
after_dispatch => sub {
|
||||
my $c = shift;
|
||||
$c->provisioning();
|
||||
|
||||
# Purge expired anti-flood protection
|
||||
my $wait_for_it = $c->app->{wait_for_it};
|
||||
delete @{$wait_for_it}{grep { time - $wait_for_it->{$_} > $c->config->{anti_flood_delay} } keys %{$wait_for_it}} if (defined($wait_for_it));
|
||||
}
|
||||
);
|
||||
|
||||
$self->asset('index.css' => 'css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/animation.css', 'css/uploader.css', 'css/hennypenny.css', 'css/lutim.css');
|
||||
$self->asset('stats.css' => 'css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/morris-0.4.3.min.css', 'css/hennypenny.css', 'css/lutim.css');
|
||||
$self->asset('about.css' => 'css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/hennypenny.css', 'css/lutim.css');
|
||||
|
||||
$self->asset('index.js' => 'js/jquery-2.1.0.min.js', 'js/bootstrap.min.js', 'js/lutim.js', 'js/dmuploader.min.js');
|
||||
$self->asset('stats.js' => 'js/jquery-2.1.0.min.js', 'js/bootstrap.min.js', 'js/lutim.js', 'js/raphael-min.js', 'js/morris-0.4.3.min.js', 'js/stats.js');
|
||||
|
||||
$self->defaults(layout => 'default');
|
||||
|
||||
$self->provisionning();
|
||||
$self->provisioning();
|
||||
|
||||
# Router
|
||||
my $r = $self->routes;
|
||||
|
||||
$r->get('/' => sub {
|
||||
my $c = shift;
|
||||
$r->options(sub {
|
||||
my $c = shift;
|
||||
$c->res->headers->allow('POST') if (defined($c->config->{allowed_domains}));
|
||||
$c->render(data => '', status => 204);
|
||||
});
|
||||
|
||||
$c->render( template => 'index');
|
||||
$r->get('/')->
|
||||
to('Controller#home')->
|
||||
name('index');
|
||||
|
||||
# Check provisionning
|
||||
$c->on(finish => sub {
|
||||
shift->provisionning();
|
||||
}
|
||||
);
|
||||
}
|
||||
)->name('index');
|
||||
$r->get('/about')->
|
||||
to('Controller#about')->
|
||||
name('about');
|
||||
|
||||
$r->post('/' => sub {
|
||||
my $c = shift;
|
||||
my $upload = $c->param('file');
|
||||
$r->get('/stats')->
|
||||
to('Controller#stats')->
|
||||
name('stats');
|
||||
|
||||
my ($mediatype, $encoding) = by_suffix $upload->filename;
|
||||
$r->get('/manifest.webapp')->
|
||||
to('Controller#webapp')->
|
||||
name('manifest.webapp');
|
||||
|
||||
my $ip = $c->ip;
|
||||
$r->post('/')->
|
||||
to('Controller#add')->
|
||||
name('add');
|
||||
|
||||
my ($msg, $short);
|
||||
# Check file type
|
||||
if (index($mediatype, 'image') >= 0) {
|
||||
# Create directory if needed
|
||||
mkdir('files', 0700) unless (-d 'files');
|
||||
$r->get('/d/:short/:token')->
|
||||
to('Controller#delete')->
|
||||
name('delete');
|
||||
|
||||
if(LutimModel->begin) {
|
||||
my @records = LutimModel::Lutim->select('WHERE path IS NULL LIMIT 1');
|
||||
if (scalar(@records)) {
|
||||
# Save file and create record
|
||||
my $filename = unidecode($upload->filename);
|
||||
my $ext = ($filename =~ m/([^.]+)$/)[0];
|
||||
my $path = 'files/'.$records[0]->short.'.'.$ext;
|
||||
$upload->move_to($path);
|
||||
$records[0]->update(
|
||||
path => $path,
|
||||
filename => $filename,
|
||||
mediatype => $mediatype,
|
||||
footprint => digest_file_hex($path, 'SHA-512'),
|
||||
enabled => 1,
|
||||
delete_at_day => ($c->param('delete-day')) ? 1 : 0,
|
||||
delete_at_first_view => ($c->param('first-view')) ? 1 : 0,
|
||||
created_at => time(),
|
||||
created_by => $ip
|
||||
);
|
||||
$r->post('/m/:short/:token')->
|
||||
to('Controller#modify')->
|
||||
name('modify');
|
||||
|
||||
# Log image creation
|
||||
$c->app->log->info('[CREATION] '.$ip.' pushed '.$filename.' (path: '.$path.')');
|
||||
$r->get('/:short')->
|
||||
to('Controller#short')->
|
||||
name('short');
|
||||
|
||||
# Give url to user
|
||||
$short = $records[0]->short;
|
||||
} else {
|
||||
# Houston, we have a problem
|
||||
$msg = $c->l('no_more_short', $c->config->{contact});
|
||||
}
|
||||
}
|
||||
LutimModel->commit;
|
||||
} else {
|
||||
$msg = $c->l('no_valid_file', $upload->filename);
|
||||
}
|
||||
|
||||
# Check provisionning
|
||||
$c->on(finish => sub {
|
||||
shift->provisionning();
|
||||
}
|
||||
);
|
||||
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
if (defined($short)) {
|
||||
$msg = {
|
||||
filename => $upload->filename,
|
||||
short => $short
|
||||
};
|
||||
} else {
|
||||
$msg = {
|
||||
filename => $upload->filename,
|
||||
msg => $msg
|
||||
};
|
||||
}
|
||||
$c->render(
|
||||
json => {
|
||||
success => (defined($short)) ? Mojo::JSON->true : Mojo::JSON->false,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $msg) if (defined($msg));
|
||||
$c->flash(short => $short) if (defined($short));
|
||||
$c->flash(filename => $upload->filename);
|
||||
$c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
)->name('add');
|
||||
|
||||
$r->get('/:short' => sub {
|
||||
my $c = shift;
|
||||
my $short = $c->param('short');
|
||||
my $touit = $c->param('t');
|
||||
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
|
||||
|
||||
my @images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 1 AND path IS NOT NULL', $short);
|
||||
my $ip = $c->ip;
|
||||
|
||||
if (scalar(@images)) {
|
||||
if($images[0]->delete_at_day && $images[0]->created_at + 86400 <= time()) {
|
||||
# Log deletion
|
||||
$c->app->log->info('[DELETION] '.$ip.' tried to view '.$images[0]->filename.' but it has been removed by expiration (path: '.$images[0]->path.')');
|
||||
|
||||
# Delete image
|
||||
unlink $images[0]->path();
|
||||
$images[0]->update(enabled => 0);
|
||||
|
||||
# Warn user
|
||||
$c->flash(
|
||||
msg => $c->l('image_not_found')
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
|
||||
my $test;
|
||||
if (defined($touit)) {
|
||||
$test = 1;
|
||||
$c->render(
|
||||
template => 'twitter',
|
||||
layout => undef,
|
||||
short => $images[0]->short,
|
||||
filename => $images[0]->filename
|
||||
);
|
||||
} else {
|
||||
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl);
|
||||
}
|
||||
|
||||
if ($test != 500) {
|
||||
# Update counter and check provisionning
|
||||
$c->on(finish => sub {
|
||||
# Log access
|
||||
$c->app->log->info('[VIEW] '.$ip.' viewed '.$images[0]->filename.' (path: '.$images[0]->path.')');
|
||||
|
||||
# Update record
|
||||
my $counter = $images[0]->counter + 1;
|
||||
$images[0]->update(counter => $counter);
|
||||
|
||||
$images[0]->update(last_access_at => time());
|
||||
$images[0]->update(last_access_by => $ip);
|
||||
|
||||
# Delete image if needed
|
||||
if ($images[0]->delete_at_first_view) {
|
||||
# Log deletion
|
||||
$c->app->log->info('[DELETION] '.$ip.' made '.$images[0]->filename.' removed (path: '.$images[0]->path.')');
|
||||
|
||||
# Delete image
|
||||
unlink $images[0]->path();
|
||||
$images[0]->update(enabled => 0);
|
||||
}
|
||||
|
||||
shift->provisionning();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 0 AND path IS NOT NULL', $short);
|
||||
|
||||
if (scalar(@images)) {
|
||||
# Log access try
|
||||
$c->app->log->info('[NOT FOUND] '.$ip.' tried to view '.$short.' but it does\'nt exist.');
|
||||
|
||||
# Warn user
|
||||
$c->flash(
|
||||
msg => $c->l('image_not_found')
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
} else {
|
||||
# Image never existed
|
||||
$c->render_not_found;
|
||||
}
|
||||
}
|
||||
})->name('short');
|
||||
$r->get('/:short/:key')->
|
||||
to('Controller#short');
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
26
lib/Lutim/Command/cron.pm
Normal file
@@ -0,0 +1,26 @@
|
||||
package Lutim::Command::cron;
|
||||
use Mojo::Base 'Mojolicious::Commands';
|
||||
|
||||
has description => 'Execute tasks.';
|
||||
has hint => <<EOF;
|
||||
|
||||
See 'script/lutim cron help TASK' for more information on a specific task.
|
||||
EOF
|
||||
has message => sub { shift->extract_usage . "\nCron tasks:\n" };
|
||||
has namespaces => sub { ['Lutim::Command::cron'] };
|
||||
|
||||
sub help { shift->run(@_) }
|
||||
|
||||
1;
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Lutim::Command::cron - Cron commands
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: script/lutim cron TASK [OPTIONS]
|
||||
|
||||
=cut
|
||||
39
lib/Lutim/Command/cron/cleanbdd.pm
Normal file
@@ -0,0 +1,39 @@
|
||||
package Lutim::Command::cron::cleanbdd;
|
||||
use Mojo::Base 'Mojolicious::Command';
|
||||
use LutimModel;
|
||||
use Mojo::Util qw(slurp decode);
|
||||
|
||||
has description => 'Delete IP addresses from database after configured delay.';
|
||||
has usage => sub { shift->extract_usage };
|
||||
|
||||
sub run {
|
||||
my $c = shift;
|
||||
|
||||
my $config = $c->app->plugin('ConfigHashMerge', {
|
||||
default => {
|
||||
keep_ip_during => 365,
|
||||
}
|
||||
});
|
||||
|
||||
my $separation = time() - $config->{keep_ip_during} * 86400;
|
||||
|
||||
LutimModel->do(
|
||||
'UPDATE lutim SET created_by = "" WHERE path IS NOT NULL AND created_at < ?',
|
||||
{},
|
||||
$separation
|
||||
);
|
||||
}
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Lutim::Command::cron::cleanbdd - Delete IP addresses from database after configured delay
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: script/lutim cron cleanbdd
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
43
lib/Lutim/Command/cron/cleanfiles.pm
Normal file
@@ -0,0 +1,43 @@
|
||||
package Lutim::Command::cron::cleanfiles;
|
||||
use Mojo::Base 'Mojolicious::Command';
|
||||
use LutimModel;
|
||||
use Mojo::Util qw(slurp decode);
|
||||
|
||||
has description => 'Delete expired files.';
|
||||
has usage => sub { shift->extract_usage };
|
||||
|
||||
sub run {
|
||||
my $c = shift;
|
||||
|
||||
my $time = time();
|
||||
my @images = LutimModel::Lutim->select('WHERE enabled = 1 AND (delete_at_day * 86400) < (? - created_at) AND delete_at_day != 0', $time);
|
||||
|
||||
for my $image (@images) {
|
||||
$c->app->delete_image($image);
|
||||
}
|
||||
|
||||
my $config = $c->app->plugin('Config');
|
||||
|
||||
if (defined($config->{delete_no_longer_viewed_files}) && $config->{delete_no_longer_viewed_files} > 0) {
|
||||
$time = time() - $config->{delete_no_longer_viewed_files} * 86400;
|
||||
@images = LutimModel::Lutim->select('WHERE enabled = 1 AND last_access_at < ?', $time);
|
||||
|
||||
for my $image (@images) {
|
||||
$c->app->delete_image($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Lutim::Command::cron::cleanfiles - Delete expired files
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: script/lutim cron cleanfiles
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
66
lib/Lutim/Command/cron/stats.pm
Normal file
@@ -0,0 +1,66 @@
|
||||
package Lutim::Command::cron::stats;
|
||||
use Mojo::Base 'Mojolicious::Command';
|
||||
use LutimModel;
|
||||
use Mojo::DOM;
|
||||
use Mojo::Util qw(slurp spurt decode);
|
||||
use DateTime;
|
||||
|
||||
has description => 'Generate statistics about Lutim.';
|
||||
has usage => sub { shift->extract_usage };
|
||||
|
||||
sub run {
|
||||
my $c = shift;
|
||||
|
||||
my $config = $c->app->plugin('ConfigHashMerge', {
|
||||
default => {
|
||||
stats_day_num => 365
|
||||
}
|
||||
});
|
||||
|
||||
my $text = slurp('templates/data.html.ep.template');
|
||||
my $dom = Mojo::DOM->new($text);
|
||||
my $thead_tr = $dom->at('table thead tr');
|
||||
my $tbody_tr = $dom->at('table tbody tr');
|
||||
my $tbody_t2 = $tbody_tr->next;
|
||||
|
||||
my $separation = time() - $config->{stats_day_num} * 86400;
|
||||
|
||||
my %data;
|
||||
for my $img (LutimModel::Lutim->select('WHERE path IS NOT NULL AND created_at >= ?', $separation)) {
|
||||
my $time = DateTime->from_epoch(epoch => $img->created_at);
|
||||
my ($year, $month, $day) = ($time->year(), $time->month(), $time->day());
|
||||
|
||||
if (defined($data{$year}->{$month}->{$day})) {
|
||||
$data{$year}->{$month}->{$day} += 1;
|
||||
} else {
|
||||
$data{$year}->{$month}->{$day} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
my $total = LutimModel::Lutim->count('WHERE path IS NOT NULL AND created_at < ?', $separation);
|
||||
for my $year (sort {$a <=> $b} keys %data) {
|
||||
for my $month (sort {$a <=> $b} keys %{$data{$year}}) {
|
||||
for my $day (sort {$a <=> $b} keys %{$data{$year}->{$month}}) {
|
||||
$thead_tr->append_content('<th>'."$day/$month/$year".'</th>'."\n");
|
||||
$tbody_tr->append_content('<td>'.$data{$year}->{$month}->{$day}.'</td>'."\n");
|
||||
$total += $data{$year}->{$month}->{$day};
|
||||
$tbody_t2->append_content('<td>'.$total.'</td>'."\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
spurt $dom, 'templates/data.html.ep';
|
||||
}
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Lutim::Command::cron::stats - Stats generator
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: script/lutim cron stats
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
67
lib/Lutim/Command/cron/watch.pm
Normal file
@@ -0,0 +1,67 @@
|
||||
package Lutim::Command::cron::watch;
|
||||
use Mojo::Base 'Mojolicious::Command';
|
||||
use Mojo::Util qw(slurp decode);
|
||||
use Filesys::DiskUsage qw/du/;
|
||||
use LutimModel;
|
||||
use Switch;
|
||||
|
||||
has description => 'Watch the files directory and take action when over quota';
|
||||
has usage => sub { shift->extract_usage };
|
||||
|
||||
sub run {
|
||||
my $c = shift;
|
||||
|
||||
my $config = $c->app->plugin('ConfigHashMerge', {
|
||||
default => {
|
||||
policy_when_full => 'warn'
|
||||
}
|
||||
});
|
||||
|
||||
if (defined($config->{max_total_size})) {
|
||||
my $total = du(qw/files/);
|
||||
|
||||
if ($total > $config->{max_total_size}) {
|
||||
say "[Lutim cron job watch] Files directory is over quota ($total > ".$config->{max_total_size}.")";
|
||||
switch ($config->{policy_when_full}) {
|
||||
case 'warn' {
|
||||
say "[Lutim cron job watch] Please, delete some files or increase quota (".$config->{max_total_size}.")";
|
||||
}
|
||||
case 'stop-upload' {
|
||||
open (my $fh, '>', 'stop-upload') or die ("Couldn't open stop-upload: $!");
|
||||
close($fh);
|
||||
say '[Lutim cron job watch] Uploads are stopped. Delete some images and the stop-upload file to reallow uploads.';
|
||||
}
|
||||
case 'delete' {
|
||||
say '[Lutim cron job watch] Older files are being deleted';
|
||||
do {
|
||||
for my $img (LutimModel::Lutim->select('WHERE path IS NOT NULL AND enabled = 1 ORDER BY created_at ASC LIMIT 50')) {
|
||||
unlink $img->path() or warn "Could not unlink ".$img->path.": $!";
|
||||
$img->update(enabled => 0);
|
||||
}
|
||||
} while (du(qw/files/) > $config->{max_total_size});
|
||||
}
|
||||
else {
|
||||
say '[Lutim cron job watch] Unrecognized policy_when_full option: '.$config->{policy_when_full}.'. Aborting.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unlink 'stop-upload' if (-f 'stop-upload');
|
||||
}
|
||||
} else {
|
||||
say "[Lutim cron job watch] No max_total_size found in the configuration file. Aborting.";
|
||||
}
|
||||
}
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Lutim::Command::cron::watch - Delete IP addresses from database after configured delay
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: script/lutim cron watch
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
486
lib/Lutim/Controller.pm
Normal file
@@ -0,0 +1,486 @@
|
||||
# vim:set sw=4 ts=4 sts=4 expandtab:
|
||||
package Lutim::Controller;
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
use Mojo::Util qw(url_unescape b64_encode);
|
||||
use DateTime;
|
||||
use File::Type;
|
||||
use Digest::file qw(digest_file_hex);
|
||||
use Text::Unidecode;
|
||||
use Data::Validate::URI qw(is_http_uri is_https_uri);
|
||||
|
||||
use vars qw($im_loaded);
|
||||
BEGIN {
|
||||
eval "use Image::Magick";
|
||||
if ($@) {
|
||||
warn "You don't have Image::Magick installed so you won't have thumbnails.";
|
||||
$im_loaded = 0;
|
||||
} else {
|
||||
$im_loaded = 1;
|
||||
}
|
||||
}
|
||||
|
||||
sub home {
|
||||
my $c = shift;
|
||||
|
||||
$c->render(
|
||||
template => 'index',
|
||||
max_file_size => $c->req->max_message_size
|
||||
);
|
||||
|
||||
|
||||
$c->on(finish => sub {
|
||||
my $c = shift;
|
||||
$c->app->log->info('[HIT] someone visited site index');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sub about {
|
||||
shift->render(template => 'about');
|
||||
}
|
||||
|
||||
sub stats {
|
||||
shift->render(
|
||||
template => 'stats',
|
||||
total => LutimModel::Lutim->count('WHERE path IS NOT NULL')
|
||||
);
|
||||
}
|
||||
|
||||
sub webapp {
|
||||
my $c = shift;
|
||||
|
||||
my $headers = Mojo::Headers->new();
|
||||
$headers->add('Content-Type' => 'application/x-web-app-manifest+json');
|
||||
$c->res->content->headers($headers);
|
||||
|
||||
$c->render(
|
||||
template => 'manifest',
|
||||
format => 'webapp'
|
||||
);
|
||||
}
|
||||
|
||||
sub modify {
|
||||
my $c = shift;
|
||||
my $short = $c->param('short');
|
||||
my $token = $c->param('token');
|
||||
my $url = $c->param('url');
|
||||
|
||||
my @images = LutimModel::Lutim->select('WHERE short = ? AND path IS NOT NULL', $short);
|
||||
if (scalar(@images)) {
|
||||
my $image = $images[0];
|
||||
my $msg;
|
||||
if ($image->mod_token() ne $token || $token eq '') {
|
||||
$msg = $c->l('invalid_token');
|
||||
} else {
|
||||
$c->app->log->info('[MODIFICATION] someone modify '.$image->filename.' with token method (path: '.$image->path.')');
|
||||
|
||||
$image->update(
|
||||
delete_at_day => ($c->param('delete-day') && $c->param('delete-day') <= $c->max_delay) ? $c->param('delete-day') : $c->max_delay,
|
||||
delete_at_first_view => ($c->param('first-view')) ? 1 : 0,
|
||||
);
|
||||
$msg = $c->l('image_delay_modified');
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->true,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$msg .= ' (<a href="'.$url.'">'.$url.'</a>)' unless (!defined($url));
|
||||
$c->flash(
|
||||
success => $msg
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(
|
||||
msg => $msg
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
} else {
|
||||
$c->app->log->info('[UNSUCCESSFUL] someone tried to modify '.$short.' but it does\'nt exist.');
|
||||
|
||||
# Image never existed
|
||||
my $msg = $c->l('image_mod_not_found', $short);
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(
|
||||
msg => $msg
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub delete {
|
||||
my $c = shift;
|
||||
my $short = $c->param('short');
|
||||
my $token = $c->param('token');
|
||||
|
||||
my @images = LutimModel::Lutim->select('WHERE short = ? AND path IS NOT NULL', $short);
|
||||
if (scalar(@images)) {
|
||||
my $image = $images[0];
|
||||
my $msg;
|
||||
if ($image->mod_token() ne $token || $token eq '') {
|
||||
$msg = $c->l('invalid_token');
|
||||
} elsif ($image->enabled() == 0) {
|
||||
$msg = $c->l('already_deleted', $image->filename);
|
||||
} else {
|
||||
$c->app->log->info('[DELETION] someone made '.$image->filename.' removed with token method (path: '.$image->path.')');
|
||||
|
||||
$c->delete_image($image);
|
||||
$c->flash(
|
||||
success => $c->l('image_deleted', $image->filename)
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
|
||||
$c->flash(
|
||||
msg => $msg
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
} else {
|
||||
$c->app->log->info('[UNSUCCESSFUL] someone tried to delete '.$short.' but it does\'nt exist.');
|
||||
|
||||
# Image never existed
|
||||
$c->render_not_found;
|
||||
}
|
||||
}
|
||||
|
||||
sub add {
|
||||
my $c = shift;
|
||||
my $upload = $c->param('file');
|
||||
my $file_url = $c->param('lutim-file-url');
|
||||
|
||||
if(!defined($c->stash('stop_upload'))) {
|
||||
if (defined($file_url) && $file_url) {
|
||||
if (is_http_uri($file_url) || is_https_uri($file_url)) {
|
||||
# Anti-flood protection
|
||||
my $ip = $c->ip(1);
|
||||
while (defined($c->app->{wait_for_it}->{$ip}) && (time - $c->app->{wait_for_it}->{$ip}) <= $c->config->{anti_flood_delay} ) {
|
||||
sleep($c->config->{anti_flood_delay});
|
||||
}
|
||||
my $ua = Mojo::UserAgent->new;
|
||||
my $tx = $ua->get($file_url => {DNT => 1});
|
||||
if (my $res = $tx->success) {
|
||||
$file_url = url_unescape $file_url;
|
||||
$file_url =~ m#^.*/([^/]*)$#;
|
||||
my $filename = $1;
|
||||
$filename = 'uploaded.image' unless (defined($filename));
|
||||
$filename .= '.image' if (index($filename, '.') == -1);
|
||||
$upload = Mojo::Upload->new(
|
||||
asset => $res->content->asset,
|
||||
filename => $filename
|
||||
);
|
||||
$c->app->{wait_for_it}->{$ip} = time;
|
||||
} elsif ($tx->res->is_limit_exceeded) {
|
||||
my $msg = $c->l('file_too_big', $tx->res->max_message_size);
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => {
|
||||
filename => $file_url,
|
||||
msg => $msg
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $msg);
|
||||
$c->flash(filename => $upload->filename);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
} else {
|
||||
my $msg = $c->l('download_error');
|
||||
$c->app->log->warn('[DOWNLOAD ERROR]'.$c->dumper($tx->error));
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => {
|
||||
filename => $file_url,
|
||||
msg => $msg
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $msg);
|
||||
$c->flash(filename => $file_url);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
my $msg = $c->l('no_valid_url');
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => {
|
||||
filename => $file_url,
|
||||
msg => $msg
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $msg);
|
||||
$c->flash(filename => $file_url);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my $ft = File::Type->new();
|
||||
my $mediatype = $ft->mime_type($upload->slurp());
|
||||
|
||||
my $ip = $c->ip;
|
||||
|
||||
my ($msg, $short, $real_short, $token, $thumb);
|
||||
# Check file type
|
||||
if (index($mediatype, 'image/') >= 0) {
|
||||
# Create directory if needed
|
||||
mkdir('files', 0700) unless (-d 'files');
|
||||
|
||||
if ($c->req->is_limit_exceeded) {
|
||||
$msg = $c->l('file_too_big', $c->req->max_message_size);
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $msg);
|
||||
$c->flash(filename => $upload->filename);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
if(LutimModel->begin) {
|
||||
my @records = LutimModel::Lutim->select('WHERE path IS NULL LIMIT 1');
|
||||
if (scalar(@records)) {
|
||||
# Save file and create record
|
||||
my $filename = unidecode($upload->filename);
|
||||
my $ext = ($filename =~ m/([^.]+)$/)[0];
|
||||
my $path = 'files/'.$records[0]->short.'.'.$ext;
|
||||
if ($im_loaded) {
|
||||
my $im = Image::Magick->new;
|
||||
$im->BlobToImage($upload->slurp);
|
||||
$im->Resize(geometry=>'x85');
|
||||
|
||||
$thumb = 'data:'.$mediatype.';base64,';
|
||||
$thumb .= b64_encode $im->ImageToBlob();
|
||||
}
|
||||
my $key;
|
||||
if ($c->param('crypt') || $c->config->{always_encrypt}) {
|
||||
($upload, $key) = $c->crypt($upload, $filename);
|
||||
}
|
||||
$upload->move_to($path);
|
||||
$records[0]->update(
|
||||
path => $path,
|
||||
filename => $filename,
|
||||
mediatype => $mediatype,
|
||||
footprint => digest_file_hex($path, 'SHA-512'),
|
||||
enabled => 1,
|
||||
delete_at_day => ($c->param('delete-day') && $c->param('delete-day') <= $c->max_delay) ? $c->param('delete-day') : $c->max_delay,
|
||||
delete_at_first_view => ($c->param('first-view')) ? 1 : 0,
|
||||
created_at => time(),
|
||||
created_by => $ip
|
||||
);
|
||||
|
||||
# Log image creation
|
||||
$c->app->log->info('[CREATION] '.$ip.' pushed '.$filename.' (path: '.$path.')');
|
||||
|
||||
# Give url to user
|
||||
$short = $records[0]->short;
|
||||
$real_short = $short;
|
||||
if (!defined($records[0]->mod_token)) {
|
||||
$records[0]->update(
|
||||
mod_token => $c->shortener($c->config->{token_length})
|
||||
);
|
||||
}
|
||||
$token = $records[0]->mod_token;
|
||||
$short .= '/'.$key if (defined($key));
|
||||
} else {
|
||||
# Houston, we have a problem
|
||||
$msg = $c->l('no_more_short', $c->config->{contact});
|
||||
}
|
||||
}
|
||||
LutimModel->commit;
|
||||
} else {
|
||||
$msg = $c->l('no_valid_file', $upload->filename);
|
||||
}
|
||||
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
if (defined($short)) {
|
||||
$msg = {
|
||||
filename => $upload->filename,
|
||||
short => $short,
|
||||
real_short => $real_short,
|
||||
token => $token,
|
||||
thumb => $thumb
|
||||
};
|
||||
} else {
|
||||
$msg = {
|
||||
filename => $upload->filename,
|
||||
msg => $msg
|
||||
};
|
||||
}
|
||||
return $c->render(
|
||||
json => {
|
||||
success => (defined($short)) ? Mojo::JSON->true : Mojo::JSON->false,
|
||||
msg => $msg
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if ((defined($msg))) {
|
||||
$c->flash(msg => $msg);
|
||||
$c->flash(filename => $upload->filename);
|
||||
return $c->redirect_to('/');
|
||||
} else {
|
||||
$c->stash(short => $short) if (defined($short));
|
||||
$c->stash(real_short => $real_short);
|
||||
$c->stash(token => $token);
|
||||
$c->stash(thumb => $thumb);
|
||||
$c->stash(filename => $upload->filename);
|
||||
return $c->render(
|
||||
template => 'index',
|
||||
max_file_size => $c->req->max_message_size
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (defined($c->param('format')) && $c->param('format') eq 'json') {
|
||||
return $c->render(
|
||||
json => {
|
||||
success => Mojo::JSON->false,
|
||||
msg => {
|
||||
filename => $upload->filename,
|
||||
msg => $c->stash('stop_upload')
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$c->flash(msg => $c->stash('stop_upload'));
|
||||
$c->flash(filename => $upload->filename);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub short {
|
||||
my $c = shift;
|
||||
my $short = $c->param('short');
|
||||
my $touit = $c->param('t');
|
||||
my $key = $c->param('key');
|
||||
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
|
||||
|
||||
my @images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 1 AND path IS NOT NULL', $short);
|
||||
|
||||
if (scalar(@images)) {
|
||||
if($images[0]->delete_at_day && $images[0]->created_at + $images[0]->delete_at_day * 86400 <= time()) {
|
||||
# Log deletion
|
||||
$c->app->log->info('[DELETION] someone tried to view '.$images[0]->filename.' but it has been removed by expiration (path: '.$images[0]->path.')');
|
||||
|
||||
# Delete image
|
||||
$c->delete_image($images[0]);
|
||||
|
||||
# Warn user
|
||||
$c->flash(
|
||||
msg => $c->l('image_not_found')
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
|
||||
my $test;
|
||||
if (defined($touit)) {
|
||||
$test = 1;
|
||||
my $short = $images[0]->short;
|
||||
$short .= '/'.$key if (defined($key));
|
||||
return $c->render(
|
||||
template => 'twitter',
|
||||
layout => undef,
|
||||
short => $short,
|
||||
filename => $images[0]->filename
|
||||
);
|
||||
} else {
|
||||
# Delete image if needed
|
||||
if ($images[0]->delete_at_first_view && $images[0]->counter >= 1) {
|
||||
# Log deletion
|
||||
$c->app->log->info('[DELETION] someone made '.$images[0]->filename.' removed (path: '.$images[0]->path.')');
|
||||
|
||||
# Delete image
|
||||
$c->delete_image($images[0]);
|
||||
|
||||
$c->flash(
|
||||
msg => $c->l('image_not_found')
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
} else {
|
||||
my $expires = ($images[0]->delete_at_day) ? $images[0]->delete_at_day : 360;
|
||||
my $dt = DateTime->from_epoch( epoch => $expires * 86400 + $images[0]->created_at);
|
||||
$dt->set_time_zone('GMT');
|
||||
$expires = $dt->strftime("%a, %d %b %Y %H:%M:%S GMT");
|
||||
|
||||
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl, $expires, $images[0]->delete_at_first_view, $key);
|
||||
}
|
||||
}
|
||||
|
||||
if ($test != 500) {
|
||||
# Update counter
|
||||
$c->on(finish => sub {
|
||||
# Log access
|
||||
$c->app->log->info('[VIEW] someone viewed '.$images[0]->filename.' (path: '.$images[0]->path.')');
|
||||
|
||||
# Update record
|
||||
my $counter = $images[0]->counter + 1;
|
||||
$images[0]->update(counter => $counter);
|
||||
|
||||
$images[0]->update(last_access_at => time());
|
||||
|
||||
# Delete image if needed
|
||||
if ($images[0]->delete_at_first_view) {
|
||||
# Log deletion
|
||||
$c->app->log->info('[DELETION] someone made '.$images[0]->filename.' removed (path: '.$images[0]->path.')');
|
||||
|
||||
# Delete image
|
||||
$c->delete_image($images[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 0 AND path IS NOT NULL', $short);
|
||||
|
||||
if (scalar(@images)) {
|
||||
# Log access try
|
||||
$c->app->log->info('[NOT FOUND] someone tried to view '.$short.' but it does\'nt exist anymore.');
|
||||
|
||||
# Warn user
|
||||
$c->flash(
|
||||
msg => $c->l('image_not_found')
|
||||
);
|
||||
return $c->redirect_to('/');
|
||||
} else {
|
||||
# Image never existed
|
||||
$c->render_not_found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -2,48 +2,92 @@ package Lutim::I18N::en;
|
||||
use Mojo::Base 'Lutim::I18N';
|
||||
|
||||
my $inf_body = <<EOF;
|
||||
<h4>What is LUTIm?</h4>
|
||||
<p>LUTIm is a free (as in free beer) and anonymous image hosting service. It's also the name of the free (as in free speech) software which provides this service.</p>
|
||||
<p>The images you post on LUTIm can be stored indefinitely or be deleted at first view or after 24 hours.</p>
|
||||
<h4>What is Lutim?</h4>
|
||||
<p>Lutim is a free (as in free beer) and anonymous image hosting service. It's also the name of the free (as in free speech) software which provides this service.</p>
|
||||
<p>The images you post on Lutim can be stored indefinitely or be deleted at first view or after a delay selected from those proposed.</p>
|
||||
<h4>How does it work?</h4>
|
||||
<p>Drag and drop an image in the appropriate area or use the traditional way to send files and LUTIm will provide you two URLs. One to view the image, the other to directly download it.</p>
|
||||
<p>You can, optionally, request that the image(s) posted on LUTIm to be deleted at first view (or download) or after 24 hours.</p>
|
||||
<p>Drag and drop an image in the appropriate area or use the traditional way to send files and Lutim will provide you three URLs. One to view the image, an other to directly download it an a last which you can use in Twitter.</p>
|
||||
<p>You can, optionally, request that the image(s) posted on Lutim to be deleted at first view (or download) or after the delay selected from those proposed.</p>
|
||||
<h4>Is it really free (as in free beer)?</h4>
|
||||
<p>Yes, it is! On the other side, if you want to support the developer, you can do it via <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=LUTIm&category=software">Flattr</a> or with <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
|
||||
<p>Yes, it is! On the other side, if you want to support the developer, you can do it via <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=Lutim&category=software">Flattr</a> or with <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
|
||||
<h4>Is it really anonymous?</h4>
|
||||
<p>Yes, it is! On the other side, for legal reasons, your IP address will be stored when you send or view an image. Don't panic, it is the case of all sites on which you go!</p>
|
||||
<p>The log files containing the IP address of image viewers are retained for one year while the IP addresse of the image's sender, as the address of the last viewer are permanently retained.</p>
|
||||
<p>Yes, it is! On the other side, for legal reasons, your IP address will be stored when you send an image. Don't panic, it is normally the case of all sites on which you send files!</p>
|
||||
<p>The IP address of the image's sender is retained for a delay which depends of the administrator's choice (for the official instance, which is located in France, it's one year).</p>
|
||||
<p>If the files are deleted if you ask it while posting it, their SHA512 footprint are retained.</p>
|
||||
<h4>How to report an image?</h4>
|
||||
<p>Please contact the administrator: [_2]</p>
|
||||
<h4>How do you pronounce LUTIm?</h4>
|
||||
<p>Juste like you pronounce the french word <a href="https://fr.wikipedia.org/wiki/Lutin">lutin</a> (/ly.tɛ̃/).</p>
|
||||
<h4>How do you pronounce Lutim?</h4>
|
||||
<p>Juste like you pronounce the French word <a href="https://fr.wikipedia.org/wiki/Lutin">lutin</a> (/ly.tɛ̃/).</p>
|
||||
<h4>What about the software which provides the service?</h4>
|
||||
<p>The LUTIm software is a <a href="http://en.wikipedia.org/wiki/Free_software">free software</a>, which allows you to download and install it on you own server. Have a look at the <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> to see what you can do.</p>
|
||||
<p>The Lutim software is a <a href="http://en.wikipedia.org/wiki/Free_software">free software</a>, which allows you to download and install it on you own server. Have a look at the <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> to see what you can do.</p>
|
||||
<p>For more details, see the <a href="https://github.com/ldidry/lutim">Github</a> page of the project.</p>
|
||||
<h4>Main developers</h4>
|
||||
<ul>
|
||||
<li>Luc Didry, aka Sky (<a href="http://www.fiat-tux.fr">http://www.fiat-tux.fr</a>), core developer, <a href="https://twitter.com/framasky">\@framasky</a></li>
|
||||
<li>Dattaz (<a href="http://dattaz.fr">http://dattaz.fr</a>), webapp developer, <a href="https://twitter.com/dat_taz">\@dat_taz</a></li>
|
||||
</ul>
|
||||
<h4>Contributors</h4>
|
||||
<ul>
|
||||
<li>Jean-Bernard Marcon, aka Goofy (<a href="https://github.com/goofy-bz">https://github.com/goofy-bz</a>)</li>
|
||||
<li>Jean-Christophe Bach (<a href="https://github.com/jcb">https://github.com/jcb</a>)</li>
|
||||
<li>Florian Bigard, aka Chocobozzz (<a href="https://github.com/Chocobozzz">https://github.com/Chocobozzz</a>)</li>
|
||||
</ul>
|
||||
EOF
|
||||
|
||||
our %Lexicon = (
|
||||
'license' => 'License:',
|
||||
'fork-me' => 'Fork me on Github !',
|
||||
'share-twitter' => 'Share on Twitter',
|
||||
'informations' => 'Informations',
|
||||
'informations-body' => $inf_body,
|
||||
'view-link' => 'View link:',
|
||||
'download-link' => 'Download link:',
|
||||
'twitter-link' => 'Link for put in a tweet:',
|
||||
'some-bad' => 'Something bad happened',
|
||||
'delete-first' => 'Delete at first view?',
|
||||
'delete-day' => 'Delete after 24 hours?',
|
||||
'upload_image' => 'Send an image',
|
||||
'image-only' => 'Only images are allowed',
|
||||
'go' => 'Let\'s go!',
|
||||
'drag-n-drop' => 'Drag & drop images here',
|
||||
'or' => '-or-',
|
||||
'file-browser' => 'Click to open the file browser',
|
||||
'image_not_found' => 'Unable to find the image',
|
||||
'no_more_short' => 'There is no more available URL. Retry or contact the administrator. [_1]',
|
||||
'no_valid_file' => 'The file [_1] is not an image.',
|
||||
'homepage' => 'Homepage',
|
||||
'license' => 'License:',
|
||||
'fork-me' => 'Fork me on Github !',
|
||||
'share-twitter' => 'Share on Twitter',
|
||||
'informations' => 'Informations',
|
||||
'informations-body' => $inf_body,
|
||||
'view-link' => 'View link',
|
||||
'download-link' => 'Download link',
|
||||
'twitter-link' => 'Link for put in a tweet',
|
||||
'tweet_it' => 'Tweet it!',
|
||||
'share_it' => 'Share it!',
|
||||
'delete-link' => 'Deletion link',
|
||||
'some-bad' => 'Something bad happened',
|
||||
'delete-first' => 'Delete at first view?',
|
||||
'delete-day' => 'Delete after 24 hours?',
|
||||
'upload_image' => 'Send an image',
|
||||
'image-only' => 'Only images are allowed',
|
||||
'go' => 'Let\'s go!',
|
||||
'drag-n-drop' => 'Drag & drop images here',
|
||||
'or' => '-or-',
|
||||
'file-browser' => 'Click to open the file browser',
|
||||
'image_not_found' => 'Unable to find the image: it has been deleted.',
|
||||
'no_more_short' => 'There is no more available URL. Retry or contact the administrator. [_1]',
|
||||
'no_valid_file' => 'The file [_1] is not an image.',
|
||||
'file_too_big' => 'The file exceed the size limit ([_1])',
|
||||
'no_time_limit' => 'No time limit',
|
||||
'24_hours' => '24 hours',
|
||||
'7_days' => '7 days',
|
||||
'30_days' => '30 days',
|
||||
'1_year' => 'One year',
|
||||
'pushed-images' => ' sent images on this instance from beginning.',
|
||||
'graph-data-once-a-day' => 'The graph\'s datas are not updated in real-time.',
|
||||
'lutim-stats' => 'Lutim\'s statistics',
|
||||
'back-to-index' => 'Back to homepage',
|
||||
'stop_upload' => 'Uploading is currently disabled, please try later or contact the administrator ([_1]).',
|
||||
'download_error' => 'An error occured while downloading the image.',
|
||||
'no_valid_url' => 'The URL is not valid.',
|
||||
'image_url' => 'Image URL',
|
||||
'upload_image_url' => 'Upload an image with its URL',
|
||||
'delay_0' => 'no time limit',
|
||||
'delay_1' => '24 hours',
|
||||
'delay_days' => '[_1] days',
|
||||
'delay_365' => '1 year',
|
||||
'max_delay' => 'Warning! The maximum time limit for an image is [_1] day(s), even if you choose "no time limit".',
|
||||
'crypt_image' => 'Encrypt the image (Lutim does not keep the key).',
|
||||
'always_encrypt' => 'The images are encrypted on the server (Lutim does not keep the key).',
|
||||
'image_deleted' => 'The image [_1] has been successfully deleted',
|
||||
'invalid_token' => 'The delete token is invalid.',
|
||||
'already_deleted' => 'The image [_1] has already been deleted.',
|
||||
'install_as_webapp' => 'Install webapp',
|
||||
'image_delay_modified' => 'The image\'s delay has been successfully modified',
|
||||
'image_mod_not_found' => 'Unable to find the image [_1].',
|
||||
'modify_image_error' => 'Error while trying to modify the image.',
|
||||
);
|
||||
|
||||
1;
|
||||
|
||||
@@ -2,48 +2,92 @@ package Lutim::I18N::fr;
|
||||
use Mojo::Base 'Lutim::I18N';
|
||||
|
||||
my $inf_body = <<EOF;
|
||||
<h4>Qu’est-ce que LUTIm ?</h4>
|
||||
<p>LUTIm est un service gratuit et anonyme d’hébergement d’images. Il s’agit aussi du nom du logiciel (libre) qui fournit ce service.</p>
|
||||
<p>Les images déposées sur LUTIm peuvent être stockées indéfiniment, ou s’effacer dès le premier affichage ou au bout de 24h.</p>
|
||||
<h4>Qu’est-ce que Lutim ?</h4>
|
||||
<p>Lutim est un service gratuit et anonyme d’hébergement d’images. Il s’agit aussi du nom du logiciel (libre) qui fournit ce service.</p>
|
||||
<p>Les images déposées sur Lutim peuvent être stockées indéfiniment, ou s’effacer dès le premier affichage ou au bout du délai choisi parmi ceux proposés.</p>
|
||||
<h4>Comment ça marche ?</h4>
|
||||
<p>Faites glisser des images dans la zone prévue à cette effet ou sélectionnez un fichier de façon classique et LUTIm vous fournira deux URLs en retour. Une pour afficher l’image, l’autre pour la télécharger directement.</p>
|
||||
<p>Vous pouvez, de façon optionnelle, demander à ce que la ou les images déposées sur LUTIm soient supprimées après leur premier affichage (ou téléchargement) ou au bout de 24 heures.</p>
|
||||
<p>Faites glisser des images dans la zone prévue à cet effet ou sélectionnez un fichier de façon classique et Lutim vous fournira troie URLs en retour. Une pour afficher l’image, une autre pour la télécharger directement et une dernière utilisable sur Twitter.</p>
|
||||
<p>Vous pouvez, de façon facultative, demander à ce que la ou les images déposées sur Lutim soient supprimées après leur premier affichage (ou téléchargement) ou au bout d'un délai choisi parmi ceux proposés.</p>
|
||||
<h4>C’est vraiment gratuit ?</h4>
|
||||
<p>Oui, ça l’est ! Par contre, si vous avez envie de soutenir le développeur, vous pouvez faire un microdon avec <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=LUTIm&category=software">Flattr</a> ou en <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
|
||||
<p>Oui, ça l’est ! Par contre, si vous avez envie de soutenir le développeur, vous pouvez faire un microdon avec <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=Lutim&category=software">Flattr</a> ou en <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
|
||||
<h4>C’est vraiment anonyme ?</h4>
|
||||
<p>Oui, ça l’est ! Par contre, pour des raisons légales, votre adresse IP sera enregistrée lorsque vous enverrez ou consulterez une image.Ne vous affolez pas, c’est de toute façon le cas sur tous les sites sur lesquels vous surfez !</p>
|
||||
<p>Les journaux systèmes contenant l’adresse IP des visiteurs d’une image sont conservés un an, tandis que l’IP de la personne ayant déposé l’image et celle du dernier visiteur de l’image sont stockées de manière définitive.</p>
|
||||
<p>Oui, ça l’est ! Par contre, pour des raisons légales, votre adresse IP sera enregistrée lorsque vous enverrez une image. Ne vous affolez pas, c’est de toute façon normalement le cas de tous les sites sur lesquels vous envoyez des fichiers !</p>
|
||||
<p>L’IP de la personne ayant déposé l’image est stockée pendant un délai dépendant de l'administrateur de l'instance (pour l'instance officielle, dont le serveur est en France, c'est un délai d'un an).</p>
|
||||
<p>Si les fichiers sont bien supprimés si vous en avez exprimé le choix, leur empreinte SHA512 est toutefois conservée.</p>
|
||||
<h4>Comment peut-on faire pour signaler une image ?</h4>
|
||||
<p>Veuillez contacter l’administrateur : [_2]</p>
|
||||
<h4>Comment doit-on prononcer LUTIm ?</h4>
|
||||
<h4>Comment doit-on prononcer Lutim ?</h4>
|
||||
<p>Comme on prononce <a href="https://fr.wikipedia.org/wiki/Lutin">lutin</a> !</p>
|
||||
<h4>Et à propos du logiciel qui fournit le service ?</h4>
|
||||
<p>Le logiciel LUTIm est un <a href="https://fr.wikipedia.org/wiki/Logiciel_libre">logiciel libre</a>, ce qui vous permet de le télécharger et de l’installer sur votre propre serveur. Jetez un coup d’œil à l’<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> pour voir quels sont vos droits.</p>
|
||||
<p>Le logiciel Lutim est un <a href="https://fr.wikipedia.org/wiki/Logiciel_libre">logiciel libre</a>, ce qui vous permet de le télécharger et de l’installer sur votre propre serveur. Jetez un coup d’œil à l’<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> pour voir quels sont vos droits.</p>
|
||||
<p>Pour plus de détails, consultez la page <a href="https://github.com/ldidry/lutim">Github</a> du projet.</p>
|
||||
<h4>Développeurs de l'application</h4>
|
||||
<ul>
|
||||
<li>Luc Didry, aka Sky (<a href="http://www.fiat-tux.fr">http://www.fiat-tux.fr</a>), développeur principal, <a href="https://twitter.com/framasky">\@framasky</a></li>
|
||||
<li>Dattaz (<a href="http://dattaz.fr">http://dattaz.fr</a>), développeur de la webapp, <a href="https://twitter.com/dat_taz">\@dat_taz</a></li>
|
||||
</ul>
|
||||
<h4>Contributeurs</h4>
|
||||
<ul>
|
||||
<li>Jean-Bernard Marcon, aka Goofy (<a href="https://github.com/goofy-bz">https://github.com/goofy-bz</a>)</li>
|
||||
<li>Jean-Christophe Bach (<a href="https://github.com/jcb">https://github.com/jcb</a>)</li>
|
||||
<li>Florian Bigard, aka Chocobozzz (<a href="https://github.com/Chocobozzz">https://github.com/Chocobozzz</a>)</li>
|
||||
</ul>
|
||||
EOF
|
||||
|
||||
our %Lexicon = (
|
||||
'license' => 'Licence :',
|
||||
'fork-me' => 'Fork me on Github',
|
||||
'share-twitter' => 'Partager sur Twitter',
|
||||
'informations' => 'Informations',
|
||||
'informations-body' => $inf_body,
|
||||
'view-link' => 'Lien d\'affichage :',
|
||||
'download-link' => 'Lien de téléchargement :',
|
||||
'twitter-link' => 'Lien pour mettre dans un tweet :',
|
||||
'some-bad' => 'Un problème est survenu',
|
||||
'delete-first' => 'Supprimer au premier accès ?',
|
||||
'delete-day' => 'Supprimer après 24 heures ?',
|
||||
'upload_image' => 'Envoyez une image',
|
||||
'image-only' => 'Seules les images sont acceptées',
|
||||
'go' => 'Allons-y !',
|
||||
'drag-n-drop' => 'Déposez vos images ici',
|
||||
'or' => '-ou-',
|
||||
'file-browser' => 'Cliquez pour utiliser le navigateur de fichier',
|
||||
'image_not_found' => 'Impossible de trouver l\'image',
|
||||
'no_more_short' => 'Il n\'y a plus d\'URL disponible. Veuillez réessayer ou contactez l\'administrateur. [_1].',
|
||||
'no_valid_file' => 'Le fichier [_1] n\'est pas une image.'
|
||||
'homepage' => 'Accueil',
|
||||
'license' => 'Licence :',
|
||||
'fork-me' => 'Fork me on Github',
|
||||
'share-twitter' => 'Partager sur Twitter',
|
||||
'informations' => 'Informations',
|
||||
'informations-body' => $inf_body,
|
||||
'view-link' => 'Lien d\'affichage',
|
||||
'download-link' => 'Lien de téléchargement',
|
||||
'twitter-link' => 'Lien pour mettre dans un tweet',
|
||||
'tweet_it' => 'Tweetez !',
|
||||
'share_it' => 'Partagez !',
|
||||
'delete-link' => 'Lien de suppression',
|
||||
'some-bad' => 'Un problème est survenu',
|
||||
'delete-first' => 'Supprimer au premier accès ?',
|
||||
'delete-day' => 'Supprimer après 24 heures ?',
|
||||
'upload_image' => 'Envoyez une image',
|
||||
'image-only' => 'Seules les images sont acceptées',
|
||||
'go' => 'Allons-y !',
|
||||
'drag-n-drop' => 'Déposez vos images ici',
|
||||
'or' => '-ou-',
|
||||
'file-browser' => 'Cliquez pour utiliser le navigateur de fichier',
|
||||
'image_not_found' => 'Impossible de trouver l\'image : elle a été supprimée.',
|
||||
'no_more_short' => 'Il n\'y a plus d\'URL disponible. Veuillez réessayer ou contactez l\'administrateur. [_1].',
|
||||
'no_valid_file' => 'Le fichier [_1] n\'est pas une image.',
|
||||
'file_too_big' => 'Le fichier dépasse la limite de taille ([_1])',
|
||||
'no_time_limit' => 'Pas de limitation de durée',
|
||||
'24_hours' => '24 heures',
|
||||
'7_days' => '7 jours',
|
||||
'30_days' => '30 jours',
|
||||
'1_year' => 'Un an',
|
||||
'pushed-images' => ' images envoyées sur cette instance depuis le début.',
|
||||
'graph-data-once-a-day' => 'Les données du graphique ne sont pas mises à jour en temps réél.',
|
||||
'lutim-stats' => 'Statistiques de Lutim',
|
||||
'back-to-index' => 'Retour à la page d\'accueil',
|
||||
'stop_upload' => 'L\'envoi d\'images est actuellement désactivé, veuillez réessayer plus tard ou contacter l\'administrateur ([_1]).',
|
||||
'download_error' => 'Une erreur est survenue lors du téléchargement de l\'image.',
|
||||
'no_valid_url' => 'l\'URL n\'est pas valide.',
|
||||
'image_url' => 'URL de l\'image',
|
||||
'upload_image_url' => 'Déposer une image par son URL',
|
||||
'delay_0' => 'pas de limitation de durée',
|
||||
'delay_1' => '24 heures',
|
||||
'delay_days' => '[_1] jours',
|
||||
'delay_365' => '1 an',
|
||||
'max_delay' => 'Attention ! Le délai maximal de rétention d\'une image est de [_1] jour(s), même si vous choisissez « pas de limitation de durée ».',
|
||||
'crypt_image' => 'Chiffrer l\'image (Lutim ne stocke pas la clé).',
|
||||
'always_encrypt' => 'Les images sont chiffrées sur le serveur (Lutim ne stocke pas la clé).',
|
||||
'image_deleted' => 'L\'image [_1] a été supprimée avec succès.',
|
||||
'invalid_token' => 'Le jeton de suppression est invalide.',
|
||||
'already_deleted' => 'L\'image [_1] a déjà été supprimée.',
|
||||
'install_as_webapp' => 'Installer la webapp',
|
||||
'image_delay_modified' => 'Le délai de l\'image a été modifié avec succès.',
|
||||
'image_mod_not_found' => 'Impossible de trouver l\'image [_1].',
|
||||
'modify_image_error' => 'Une erreur est survenue lors de la tentative de modification de l\'image.',
|
||||
);
|
||||
|
||||
1;
|
||||
|
||||
@@ -20,7 +20,7 @@ use ORLite {
|
||||
created_at INTEGER,
|
||||
created_by TEXT,
|
||||
last_access_at INTEGER,
|
||||
last_access_by INTEGER)'
|
||||
mod_token TEXT)'
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,131 @@
|
||||
# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
|
||||
{
|
||||
####################
|
||||
# Hypnotoad settings
|
||||
####################
|
||||
# see http://mojolicio.us/perldoc/Mojo/Server/Hypnotoad for a full list of settings
|
||||
hypnotoad => {
|
||||
# array of IP addresses and ports you want to listen to
|
||||
listen => ['http://127.0.0.1:8080'],
|
||||
# user and group you want for Lutim to run with
|
||||
# be sure that this user/group have rights on the lutim directory
|
||||
# if you launch lutim from a different user, be sure that this user have the right to su this user/group
|
||||
# => if current_user is not the user that you sets here and is not root, there's chances that it will fail (see https://github.com/ldidry/lutim/issues/25)
|
||||
user => 'www-data',
|
||||
group => 'www-data'
|
||||
},
|
||||
contact => 'John Doe, admin[at]example.com',
|
||||
secrets => ['fdjsofjoihrei'], # please provide a random string
|
||||
piwik_img => 'https://piwik.example.org/piwik.php?idsite=1&rec=1', # optional, only the piwik image tracker is allowed, no javascript
|
||||
length => 8, # optional
|
||||
provis_step => 5, # optional
|
||||
provisionning => 100, # optional
|
||||
hosted_by => 'My super hoster <img src="http://hoster.example.com" alt="Hoster logo">', # optional
|
||||
tweet_card_via => '@framasky' # optional
|
||||
|
||||
################
|
||||
# Lutim settings
|
||||
################
|
||||
|
||||
# put a way to contact you here and uncomment it
|
||||
# mandatory
|
||||
#contact => 'John Doe, admin[at]example.com',
|
||||
|
||||
# random string used to encrypt cookies
|
||||
# mandatory
|
||||
secrets => ['fdjsofjoihrei'],
|
||||
|
||||
# length of the images random URL
|
||||
# optional, default is 8
|
||||
#length => 8,
|
||||
|
||||
# how many URLs will be provisioned in a batch ?
|
||||
# optional, default is 5
|
||||
#provis_step => 5,
|
||||
|
||||
# max number of URLs to be provisioned
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# in the first version, this option was provisionning with two 'n'. While the option with the typo is still valid, it is deprecated.
|
||||
# in the next version (0.4), only provisioning with ine 'n' will be accepted
|
||||
# optional, default is 100
|
||||
#provisioning => 100,
|
||||
|
||||
# anti-flood protection delay, in seconds
|
||||
# users won't be able to ask Lutim to download images more than one per anti_flood_delay seconds
|
||||
# optional, default is 5
|
||||
#anti_flood_delay => 5,
|
||||
|
||||
# twitter account which will appear on twitter cards
|
||||
# see https://dev.twitter.com/docs/cards/validation/validator to register your Lutim instance on twitter
|
||||
# optional, default is @framasky
|
||||
#tweet_card_via => '@framasky',
|
||||
|
||||
# max image size, in octets
|
||||
# you can write it 10*1024*1024
|
||||
# optional, default is 10485760
|
||||
#max_file_size => 10485760,
|
||||
|
||||
# if you want to have piwik statistics, provide a piwik image tracker
|
||||
# only the image tracker is allowed, no javascript
|
||||
# optional, no default
|
||||
#piwik_img => 'https://piwik.example.org/piwik.php?idsite=1&rec=1',
|
||||
|
||||
# if you want to include something in the right of the screen, put it here
|
||||
# here's an exemple to put the logo of your hoster
|
||||
# optional, no default
|
||||
#hosted_by => 'My super hoster <img src="http://hoster.example.com" alt="Hoster logo">',
|
||||
|
||||
# DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED
|
||||
# Lutim now checks if the X-Forwarded-Proto header is present and equal to https.
|
||||
# set to 1 if you use Lutim behind a secure web server
|
||||
# optional, default is 0
|
||||
#https => 0,
|
||||
|
||||
# broadcast_message which will displayed on all pages of Lutim (but no in json response)
|
||||
# optional, no default
|
||||
#broadcast_message => 'Maintenance',
|
||||
|
||||
# array of authorized domains for API calls.
|
||||
# if you want to authorize everyone to use the API: ['*']
|
||||
# optional, no domains allowed by default
|
||||
#allowed_domains => ['http://1.example.com', 'http://2.example.com'],
|
||||
|
||||
# default time limit for files
|
||||
# valid values are 0, 1, 7, 30 and 365
|
||||
# optional, default is 0 (no limit)
|
||||
#default_delay => 0,
|
||||
|
||||
# number of days after which the images will be deleted, even if they were uploaded with "no delay" (or value superior to max_delay)
|
||||
# a warning message will be displayed on homepage
|
||||
# optional, default is 0 (no limit)
|
||||
#max_delay => 0,
|
||||
|
||||
# if set to 1, all the images will be encrypted and the encryption option will no be displayed
|
||||
# optional, default is 0
|
||||
#always_encrypt => 0,
|
||||
|
||||
# length of the image's delete token
|
||||
# optional, default is 24
|
||||
#token_length => 24,
|
||||
|
||||
##########################
|
||||
# Lutim cron jobs settings
|
||||
##########################
|
||||
|
||||
# number of days shown in /stats page (used with script/lutim cron stats)
|
||||
# optional, default is 365
|
||||
#stats_day_num => 365,
|
||||
|
||||
# number of days senders' IP addresses are kept in database
|
||||
# after that delay, they will be deleted from database (used with script/lutim cron cleanbdd)
|
||||
# optional, default is 365
|
||||
#keep_ip_during => 365,
|
||||
|
||||
# max size of the files directory, in octets
|
||||
# used by script/lutim cron watch to trigger an action
|
||||
# optional, no default
|
||||
#max_total_size => 10*1024*1024*1024,
|
||||
|
||||
# default action when files directory is over max_total_size (used with script/lutim cron watch)
|
||||
# valid values are 'warn', 'stop-upload' and 'delete'
|
||||
# please, see readme
|
||||
# optional, default is 'warn'
|
||||
#policy_when_full => 'warn',
|
||||
|
||||
# images which are not viewed since delete_no_longer_viewed_files days will be deleted by the cron cleanfiles task
|
||||
# if delete_no_longer_viewed_files is not set, the no longer viewed files will NOT be deleted
|
||||
# optional, no default
|
||||
#delete_no_longer_viewed_files => 90
|
||||
};
|
||||
|
||||
425
public/css/bootstrap.css
vendored
@@ -1347,6 +1347,235 @@ address {
|
||||
margin-left: 0%;
|
||||
}
|
||||
}
|
||||
table {
|
||||
max-width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.table > thead > tr > th,
|
||||
.table > tbody > tr > th,
|
||||
.table > tfoot > tr > th,
|
||||
.table > thead > tr > td,
|
||||
.table > tbody > tr > td,
|
||||
.table > tfoot > tr > td {
|
||||
padding: 8px;
|
||||
line-height: 1.42857143;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #dddddd;
|
||||
}
|
||||
.table > thead > tr > th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #dddddd;
|
||||
}
|
||||
.table > caption + thead > tr:first-child > th,
|
||||
.table > colgroup + thead > tr:first-child > th,
|
||||
.table > thead:first-child > tr:first-child > th,
|
||||
.table > caption + thead > tr:first-child > td,
|
||||
.table > colgroup + thead > tr:first-child > td,
|
||||
.table > thead:first-child > tr:first-child > td {
|
||||
border-top: 0;
|
||||
}
|
||||
.table > tbody + tbody {
|
||||
border-top: 2px solid #dddddd;
|
||||
}
|
||||
.table .table {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.table-condensed > thead > tr > th,
|
||||
.table-condensed > tbody > tr > th,
|
||||
.table-condensed > tfoot > tr > th,
|
||||
.table-condensed > thead > tr > td,
|
||||
.table-condensed > tbody > tr > td,
|
||||
.table-condensed > tfoot > tr > td {
|
||||
padding: 5px;
|
||||
}
|
||||
.table-bordered {
|
||||
border: 1px solid #dddddd;
|
||||
}
|
||||
.table-bordered > thead > tr > th,
|
||||
.table-bordered > tbody > tr > th,
|
||||
.table-bordered > tfoot > tr > th,
|
||||
.table-bordered > thead > tr > td,
|
||||
.table-bordered > tbody > tr > td,
|
||||
.table-bordered > tfoot > tr > td {
|
||||
border: 1px solid #dddddd;
|
||||
}
|
||||
.table-bordered > thead > tr > th,
|
||||
.table-bordered > thead > tr > td {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
.table-striped > tbody > tr:nth-child(odd) > td,
|
||||
.table-striped > tbody > tr:nth-child(odd) > th {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.table-hover > tbody > tr:hover > td,
|
||||
.table-hover > tbody > tr:hover > th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
table col[class*="col-"] {
|
||||
position: static;
|
||||
float: none;
|
||||
display: table-column;
|
||||
}
|
||||
table td[class*="col-"],
|
||||
table th[class*="col-"] {
|
||||
position: static;
|
||||
float: none;
|
||||
display: table-cell;
|
||||
}
|
||||
.table > thead > tr > td.active,
|
||||
.table > tbody > tr > td.active,
|
||||
.table > tfoot > tr > td.active,
|
||||
.table > thead > tr > th.active,
|
||||
.table > tbody > tr > th.active,
|
||||
.table > tfoot > tr > th.active,
|
||||
.table > thead > tr.active > td,
|
||||
.table > tbody > tr.active > td,
|
||||
.table > tfoot > tr.active > td,
|
||||
.table > thead > tr.active > th,
|
||||
.table > tbody > tr.active > th,
|
||||
.table > tfoot > tr.active > th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.table-hover > tbody > tr > td.active:hover,
|
||||
.table-hover > tbody > tr > th.active:hover,
|
||||
.table-hover > tbody > tr.active:hover > td,
|
||||
.table-hover > tbody > tr.active:hover > th {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
.table > thead > tr > td.success,
|
||||
.table > tbody > tr > td.success,
|
||||
.table > tfoot > tr > td.success,
|
||||
.table > thead > tr > th.success,
|
||||
.table > tbody > tr > th.success,
|
||||
.table > tfoot > tr > th.success,
|
||||
.table > thead > tr.success > td,
|
||||
.table > tbody > tr.success > td,
|
||||
.table > tfoot > tr.success > td,
|
||||
.table > thead > tr.success > th,
|
||||
.table > tbody > tr.success > th,
|
||||
.table > tfoot > tr.success > th {
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
.table-hover > tbody > tr > td.success:hover,
|
||||
.table-hover > tbody > tr > th.success:hover,
|
||||
.table-hover > tbody > tr.success:hover > td,
|
||||
.table-hover > tbody > tr.success:hover > th {
|
||||
background-color: #d0e9c6;
|
||||
}
|
||||
.table > thead > tr > td.info,
|
||||
.table > tbody > tr > td.info,
|
||||
.table > tfoot > tr > td.info,
|
||||
.table > thead > tr > th.info,
|
||||
.table > tbody > tr > th.info,
|
||||
.table > tfoot > tr > th.info,
|
||||
.table > thead > tr.info > td,
|
||||
.table > tbody > tr.info > td,
|
||||
.table > tfoot > tr.info > td,
|
||||
.table > thead > tr.info > th,
|
||||
.table > tbody > tr.info > th,
|
||||
.table > tfoot > tr.info > th {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
.table-hover > tbody > tr > td.info:hover,
|
||||
.table-hover > tbody > tr > th.info:hover,
|
||||
.table-hover > tbody > tr.info:hover > td,
|
||||
.table-hover > tbody > tr.info:hover > th {
|
||||
background-color: #c4e3f3;
|
||||
}
|
||||
.table > thead > tr > td.warning,
|
||||
.table > tbody > tr > td.warning,
|
||||
.table > tfoot > tr > td.warning,
|
||||
.table > thead > tr > th.warning,
|
||||
.table > tbody > tr > th.warning,
|
||||
.table > tfoot > tr > th.warning,
|
||||
.table > thead > tr.warning > td,
|
||||
.table > tbody > tr.warning > td,
|
||||
.table > tfoot > tr.warning > td,
|
||||
.table > thead > tr.warning > th,
|
||||
.table > tbody > tr.warning > th,
|
||||
.table > tfoot > tr.warning > th {
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
.table-hover > tbody > tr > td.warning:hover,
|
||||
.table-hover > tbody > tr > th.warning:hover,
|
||||
.table-hover > tbody > tr.warning:hover > td,
|
||||
.table-hover > tbody > tr.warning:hover > th {
|
||||
background-color: #faf2cc;
|
||||
}
|
||||
.table > thead > tr > td.danger,
|
||||
.table > tbody > tr > td.danger,
|
||||
.table > tfoot > tr > td.danger,
|
||||
.table > thead > tr > th.danger,
|
||||
.table > tbody > tr > th.danger,
|
||||
.table > tfoot > tr > th.danger,
|
||||
.table > thead > tr.danger > td,
|
||||
.table > tbody > tr.danger > td,
|
||||
.table > tfoot > tr.danger > td,
|
||||
.table > thead > tr.danger > th,
|
||||
.table > tbody > tr.danger > th,
|
||||
.table > tfoot > tr.danger > th {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
.table-hover > tbody > tr > td.danger:hover,
|
||||
.table-hover > tbody > tr > th.danger:hover,
|
||||
.table-hover > tbody > tr.danger:hover > td,
|
||||
.table-hover > tbody > tr.danger:hover > th {
|
||||
background-color: #ebcccc;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
border: 1px solid #dddddd;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.table-responsive > .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-responsive > .table > thead > tr > th,
|
||||
.table-responsive > .table > tbody > tr > th,
|
||||
.table-responsive > .table > tfoot > tr > th,
|
||||
.table-responsive > .table > thead > tr > td,
|
||||
.table-responsive > .table > tbody > tr > td,
|
||||
.table-responsive > .table > tfoot > tr > td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table-responsive > .table-bordered {
|
||||
border: 0;
|
||||
}
|
||||
.table-responsive > .table-bordered > thead > tr > th:first-child,
|
||||
.table-responsive > .table-bordered > tbody > tr > th:first-child,
|
||||
.table-responsive > .table-bordered > tfoot > tr > th:first-child,
|
||||
.table-responsive > .table-bordered > thead > tr > td:first-child,
|
||||
.table-responsive > .table-bordered > tbody > tr > td:first-child,
|
||||
.table-responsive > .table-bordered > tfoot > tr > td:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
.table-responsive > .table-bordered > thead > tr > th:last-child,
|
||||
.table-responsive > .table-bordered > tbody > tr > th:last-child,
|
||||
.table-responsive > .table-bordered > tfoot > tr > th:last-child,
|
||||
.table-responsive > .table-bordered > thead > tr > td:last-child,
|
||||
.table-responsive > .table-bordered > tbody > tr > td:last-child,
|
||||
.table-responsive > .table-bordered > tfoot > tr > td:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
.table-responsive > .table-bordered > tbody > tr:last-child > th,
|
||||
.table-responsive > .table-bordered > tfoot > tr:last-child > th,
|
||||
.table-responsive > .table-bordered > tbody > tr:last-child > td,
|
||||
.table-responsive > .table-bordered > tfoot > tr:last-child > td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
fieldset {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -2315,6 +2544,82 @@ select[multiple].input-group-sm > .input-group-btn > .btn {
|
||||
.alert-danger .alert-link {
|
||||
color: #843534;
|
||||
}
|
||||
@-webkit-keyframes progress-bar-stripes {
|
||||
from {
|
||||
background-position: 40px 0;
|
||||
}
|
||||
to {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes progress-bar-stripes {
|
||||
from {
|
||||
background-position: 40px 0;
|
||||
}
|
||||
to {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
margin-bottom: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.progress-bar {
|
||||
float: left;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
background-color: #428bca;
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-transition: width 0.6s ease;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
.progress-striped .progress-bar {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
.progress.active .progress-bar {
|
||||
-webkit-animation: progress-bar-stripes 2s linear infinite;
|
||||
animation: progress-bar-stripes 2s linear infinite;
|
||||
}
|
||||
.progress-bar-success {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
.progress-striped .progress-bar-success {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
}
|
||||
.progress-bar-info {
|
||||
background-color: #5bc0de;
|
||||
}
|
||||
.progress-striped .progress-bar-info {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
}
|
||||
.progress-bar-warning {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
.progress-striped .progress-bar-warning {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
}
|
||||
.progress-bar-danger {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
.progress-striped .progress-bar-danger {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
}
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 21px;
|
||||
@@ -2340,119 +2645,6 @@ button.close {
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal {
|
||||
display: none;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
outline: 0;
|
||||
}
|
||||
.modal.fade .modal-dialog {
|
||||
-webkit-transform: translate(0, -25%);
|
||||
-ms-transform: translate(0, -25%);
|
||||
transform: translate(0, -25%);
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out;
|
||||
-moz-transition: -moz-transform 0.3s ease-out;
|
||||
-o-transition: -o-transform 0.3s ease-out;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
-webkit-transform: translate(0, 0);
|
||||
-ms-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin: 10px;
|
||||
}
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #999999;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
|
||||
background-clip: padding-box;
|
||||
outline: none;
|
||||
}
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1040;
|
||||
background-color: #000000;
|
||||
}
|
||||
.modal-backdrop.fade {
|
||||
opacity: 0;
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
.modal-backdrop.in {
|
||||
opacity: 0.5;
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
.modal-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
min-height: 16.42857143px;
|
||||
}
|
||||
.modal-header .close {
|
||||
margin-top: -2px;
|
||||
}
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
.modal-body {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal-footer {
|
||||
margin-top: 15px;
|
||||
padding: 19px 20px 20px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.modal-footer .btn + .btn {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.modal-footer .btn-group .btn + .btn {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.modal-footer .btn-block + .btn-block {
|
||||
margin-left: 0;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.modal-dialog {
|
||||
width: 600px;
|
||||
margin: 30px auto;
|
||||
}
|
||||
.modal-content {
|
||||
-webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.modal-sm {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.modal-lg {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after,
|
||||
.container:before,
|
||||
@@ -2462,9 +2654,7 @@ button.close {
|
||||
.row:before,
|
||||
.row:after,
|
||||
.form-horizontal .form-group:before,
|
||||
.form-horizontal .form-group:after,
|
||||
.modal-footer:before,
|
||||
.modal-footer:after {
|
||||
.form-horizontal .form-group:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
@@ -2472,8 +2662,7 @@ button.close {
|
||||
.container:after,
|
||||
.container-fluid:after,
|
||||
.row:after,
|
||||
.form-horizontal .form-group:after,
|
||||
.modal-footer:after {
|
||||
.form-horizontal .form-group:after {
|
||||
clear: both;
|
||||
}
|
||||
.center-block {
|
||||
|
||||
2
public/css/bootstrap.min.css
vendored
12
public/css/fontello-codes.css
vendored
@@ -1,5 +1,9 @@
|
||||
|
||||
.icon-bitcoin:before { content: '\e802'; } /* '' */
|
||||
.icon-github-circled:before { content: '\e800'; } /* '' */
|
||||
.icon-touiteur:before { content: '\e801'; } /* '' */
|
||||
.icon-flattr:before { content: '\e803'; } /* '' */
|
||||
.icon-twitter:before { content: '\e800'; } /* '' */
|
||||
.icon-github-circled:before { content: '\e801'; } /* '' */
|
||||
.icon-flattr:before { content: '\e802'; } /* '' */
|
||||
.icon-bitcoin:before { content: '\e803'; } /* '' */
|
||||
.icon-trash:before { content: '\e804'; } /* '' */
|
||||
.icon-download:before { content: '\e805'; } /* '' */
|
||||
.icon-spinner:before { content: '\e806'; } /* '' */
|
||||
.icon-eye:before { content: '\e807'; } /* '' */
|
||||
24
public/css/fontello-embedded.css
vendored
12
public/css/fontello-ie7-codes.css
vendored
@@ -1,5 +1,9 @@
|
||||
|
||||
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-touiteur { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
12
public/css/fontello-ie7.css
vendored
@@ -10,7 +10,11 @@
|
||||
/* font-size: 120%; */
|
||||
}
|
||||
|
||||
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-touiteur { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
24
public/css/fontello.css
vendored
@@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?4658339');
|
||||
src: url('../font/fontello.eot?4658339#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff?4658339') format('woff'),
|
||||
url('../font/fontello.ttf?4658339') format('truetype'),
|
||||
url('../font/fontello.svg?4658339#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?47445522');
|
||||
src: url('../font/fontello.eot?47445522#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff?47445522') format('woff'),
|
||||
url('../font/fontello.ttf?47445522') format('truetype'),
|
||||
url('../font/fontello.svg?47445522#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?4658339#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?47445522#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -50,7 +50,11 @@
|
||||
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
|
||||
}
|
||||
|
||||
.icon-bitcoin:before { content: '\e802'; } /* '' */
|
||||
.icon-github-circled:before { content: '\e800'; } /* '' */
|
||||
.icon-touiteur:before { content: '\e801'; } /* '' */
|
||||
.icon-flattr:before { content: '\e803'; } /* '' */
|
||||
.icon-twitter:before { content: '\e800'; } /* '' */
|
||||
.icon-github-circled:before { content: '\e801'; } /* '' */
|
||||
.icon-flattr:before { content: '\e802'; } /* '' */
|
||||
.icon-bitcoin:before { content: '\e803'; } /* '' */
|
||||
.icon-trash:before { content: '\e804'; } /* '' */
|
||||
.icon-download:before { content: '\e805'; } /* '' */
|
||||
.icon-spinner:before { content: '\e806'; } /* '' */
|
||||
.icon-eye:before { content: '\e807'; } /* '' */
|
||||
6
public/css/hennypenny.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Henny_Penny';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Henny Penny'), local('HennyPenny-Regular'), url(../font/hennypenny.ttf) format('truetype');
|
||||
}
|
||||
50
public/css/lutim.css
Normal file
@@ -0,0 +1,50 @@
|
||||
@media (max-width: 767px) {
|
||||
body {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.jsonly {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.hennypenny {
|
||||
font-family: 'Henny_Penny', cursive;
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: 10px;
|
||||
}
|
||||
label.always-encrypt {
|
||||
display: none;
|
||||
}
|
||||
.link_nocol,
|
||||
.link_nocol:hover{
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#install-app img {
|
||||
height: 22px;
|
||||
}
|
||||
#install-app {
|
||||
display: none;
|
||||
}
|
||||
2
public/css/morris-0.4.3.min.css
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.morris-hover{position:absolute;z-index:1000;}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255, 255, 255, 0.8);border:solid 2px rgba(230, 230, 230, 0.8);font-family:sans-serif;font-size:12px;text-align:center;}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0;}
|
||||
.morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0;}
|
||||
@@ -26,6 +26,18 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
.uploader div.browser label {
|
||||
max-width:95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:768px){
|
||||
.uploader div.browser label {
|
||||
width:300px;
|
||||
}
|
||||
}
|
||||
|
||||
.uploader div.browser label {
|
||||
background-color: #5a7bc2;
|
||||
padding: 5px 15px;
|
||||
@@ -38,7 +50,6 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
width: 300px;
|
||||
margin: 20px auto 0px auto;
|
||||
|
||||
box-shadow: 2px 2px 2px #888888;
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
<font id="fontello" horiz-adv-x="1000" >
|
||||
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
|
||||
<missing-glyph horiz-adv-x="1000" />
|
||||
<glyph glyph-name="bitcoin" unicode="" d="m651 493q10-102-73-144q65-16 98-58t25-119q-4-40-18-70t-36-49t-54-33t-68-19t-81-9v-142h-86v140q-45 0-68 1v-141h-86v142q-10 0-30 1t-31 0h-112l18 102h62q27 0 32 28v225h9q-4 0-9 0v160q-7 38-50 38h-62v92l119-1q35 0 54 1v141h86v-138q45 1 68 1v137h86v-141q44-4 78-13t63-25t46-43t20-64z m-120-304q0 20-8 35t-21 26t-32 17t-36 10t-42 5t-38 2t-36 0t-27-1v-189q5 0 21 0t27 0t29 1t33 2t32 5t31 8t26 11t22 17t14 22t5 29z m-39 265q0 19-7 33t-17 23t-27 16t-31 9t-34 5t-33 1t-30 0t-22-1v-171q3 0 20 0t26 0t27 1t31 3t29 6t27 10t21 15t15 22t5 28z" horiz-adv-x="714.3" />
|
||||
<glyph glyph-name="github-circled" unicode="" d="m857 350q0-140-82-252t-211-155q-15-3-22 4t-7 17v118q0 54-29 79q32 3 57 10t53 22t45 37t30 58t11 84q0 68-44 115q21 51-5 114q-15 5-45-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22t-48 7q-24-63-4-114q-44-47-44-115q0-47 12-83t29-59t45-37t52-22t57-10q-22-20-27-58q-12-5-25-8t-32-3t-36 12t-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7t5-8t7-6l4-3q12-6 24-21t18-29l5-13q8-21 25-34t37-17t39-4t31 2l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156t216 58t215-58t156-156t57-215z" horiz-adv-x="857.1" />
|
||||
<glyph glyph-name="touiteur" unicode="" d="m904 622q-37-54-90-93q0-8 0-23q0-73-21-145t-64-139t-103-117t-144-82t-181-30q-151 0-276 81q19-3 43-3q126 0 224 77q-59 2-105 36t-64 89q19-2 34-2q24 0 48 6q-63 13-104 62t-41 115v2q38-21 82-23q-37 25-59 64t-22 86q0 49 25 91q68-83 164-133t208-55q-5 21-5 41q0 75 53 127t127 53q79 0 132-57q61 12 114 44q-20-64-79-100q52 6 104 28z" horiz-adv-x="928.6" />
|
||||
<glyph glyph-name="flattr" unicode="" d="m180 424l0-210l-180-180l0 414q0 302 278 302l442 0l-312-310q-6-6-10-6q-8 0-12 10l0 128q-98 0-112-2q-94-16-94-146z m440 64l180 180l0-414q0-304-278-304l-440 0l312 312q2 6 8 6q8 0 12-10l0-128q98 0 112 2q94 16 94 146l0 210z" horiz-adv-x="800" />
|
||||
<glyph glyph-name="twitter" unicode="" d="m904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-3 43-3 126 0 224 77-59 2-105 36t-64 89q19-2 34-2 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 114 44-20-64-79-100 52 6 104 28z" horiz-adv-x="928.6" />
|
||||
<glyph glyph-name="github-circled" unicode="" d="m857 350q0-140-82-252t-211-155q-15-3-22 4t-7 17v118q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 68-44 115 21 51-5 114-15 5-45-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-48 7q-24-63-4-114-44-47-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-22-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l5-13q8-21 25-34t37-17 39-4 31 2l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
|
||||
<glyph glyph-name="flattr" unicode="" d="m0-37l0 514q0 179 85 278t259 99l548 0q-5-5-52-53t-100-101-109-109-95-93-42-37q-15 0-15 16l0 156-48 0q-59 0-94-6t-63-26-39-57-12-96l0-262z m67-117q5 5 53 53t100 101 109 110 95 93 41 36q15 0 15-16l0-156 48 0q116 0 162 36t45 149l0 262 224 223 0-514q0-179-84-278t-260-99l-548 0z" horiz-adv-x="959" />
|
||||
<glyph glyph-name="bitcoin" unicode="" d="m651 493q10-102-73-144 65-16 98-58t25-119q-4-40-18-70t-36-49-54-33-68-19-81-9v-142h-86v140q-45 0-68 1v-141h-86v142q-10 0-30 1t-31 0h-112l18 102h62q27 0 32 28v225h9q-4 0-9 0v160q-7 38-50 38h-62v92l119-1q35 0 54 1v141h86v-138q45 1 68 1v137h86v-141q44-4 78-13t63-25 46-43 20-64z m-120-304q0 20-8 35t-21 26-32 17-36 10-42 5-38 2-36 0-27-1v-189q5 0 21 0t27 0 29 1 33 2 32 5 31 8 26 11 22 17 14 22 5 29z m-39 265q0 19-7 33t-17 23-27 16-31 9-34 5-33 1-30 0-22-1v-171q3 0 20 0t26 0 27 1 31 3 29 6 27 10 21 15 15 22 5 28z" horiz-adv-x="714.3" />
|
||||
<glyph glyph-name="trash" unicode="" d="m286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q22 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
|
||||
<glyph glyph-name="download" unicode="" d="m714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
|
||||
<glyph glyph-name="spinner" unicode="" d="m469 614v204q129 0 237-61t169-170 62-237h-204q0 72-36 133t-95 96-133 35z" horiz-adv-x="937.5" />
|
||||
<glyph glyph-name="eye" unicode="" d="m929 314q-85 132-213 197 34-58 34-125 0-104-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 242 68 187 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-12 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
public/font/hennypenny.ttf
Normal file
BIN
public/img/Lutim.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
public/img/Lutim.xcf
Normal file
BIN
public/img/Lutim_flou.xcf
Normal file
BIN
public/img/Lutim_small.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/img/Lutim_sticker.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
public/img/Lutim_sticker.xcf
Normal file
BIN
public/img/favicon.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/img/lutim120.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/img/lutim128.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/img/lutim152.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/img/lutim196.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/img/lutim256.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/img/lutim32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/img/lutim60.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/img/lutim76.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/img/lutim90.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/img/rocket.png
Normal file
|
After Width: | Height: | Size: 509 B |
356
public/js/bootstrap.js
vendored
@@ -1,5 +1,5 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: alert.js v3.1.0
|
||||
* Bootstrap: alert.js v3.1.1
|
||||
* http://getbootstrap.com/javascript/#alerts
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
@@ -88,359 +88,7 @@
|
||||
}(jQuery);
|
||||
|
||||
/* ========================================================================
|
||||
* Bootstrap: button.js v3.1.0
|
||||
* http://getbootstrap.com/javascript/#buttons
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// BUTTON PUBLIC CLASS DEFINITION
|
||||
// ==============================
|
||||
|
||||
var Button = function (element, options) {
|
||||
this.$element = $(element)
|
||||
this.options = $.extend({}, Button.DEFAULTS, options)
|
||||
this.isLoading = false
|
||||
}
|
||||
|
||||
Button.DEFAULTS = {
|
||||
loadingText: 'loading...'
|
||||
}
|
||||
|
||||
Button.prototype.setState = function (state) {
|
||||
var d = 'disabled'
|
||||
var $el = this.$element
|
||||
var val = $el.is('input') ? 'val' : 'html'
|
||||
var data = $el.data()
|
||||
|
||||
state = state + 'Text'
|
||||
|
||||
if (!data.resetText) $el.data('resetText', $el[val]())
|
||||
|
||||
$el[val](data[state] || this.options[state])
|
||||
|
||||
// push to event loop to allow forms to submit
|
||||
setTimeout($.proxy(function () {
|
||||
if (state == 'loadingText') {
|
||||
this.isLoading = true
|
||||
$el.addClass(d).attr(d, d)
|
||||
} else if (this.isLoading) {
|
||||
this.isLoading = false
|
||||
$el.removeClass(d).removeAttr(d)
|
||||
}
|
||||
}, this), 0)
|
||||
}
|
||||
|
||||
Button.prototype.toggle = function () {
|
||||
var changed = true
|
||||
var $parent = this.$element.closest('[data-toggle="buttons"]')
|
||||
|
||||
if ($parent.length) {
|
||||
var $input = this.$element.find('input')
|
||||
if ($input.prop('type') == 'radio') {
|
||||
if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
|
||||
else $parent.find('.active').removeClass('active')
|
||||
}
|
||||
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
|
||||
}
|
||||
|
||||
if (changed) this.$element.toggleClass('active')
|
||||
}
|
||||
|
||||
|
||||
// BUTTON PLUGIN DEFINITION
|
||||
// ========================
|
||||
|
||||
var old = $.fn.button
|
||||
|
||||
$.fn.button = function (option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.button')
|
||||
var options = typeof option == 'object' && option
|
||||
|
||||
if (!data) $this.data('bs.button', (data = new Button(this, options)))
|
||||
|
||||
if (option == 'toggle') data.toggle()
|
||||
else if (option) data.setState(option)
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.button.Constructor = Button
|
||||
|
||||
|
||||
// BUTTON NO CONFLICT
|
||||
// ==================
|
||||
|
||||
$.fn.button.noConflict = function () {
|
||||
$.fn.button = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// BUTTON DATA-API
|
||||
// ===============
|
||||
|
||||
$(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
|
||||
var $btn = $(e.target)
|
||||
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
|
||||
$btn.button('toggle')
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
}(jQuery);
|
||||
|
||||
/* ========================================================================
|
||||
* Bootstrap: modal.js v3.1.0
|
||||
* http://getbootstrap.com/javascript/#modals
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// MODAL CLASS DEFINITION
|
||||
// ======================
|
||||
|
||||
var Modal = function (element, options) {
|
||||
this.options = options
|
||||
this.$element = $(element)
|
||||
this.$backdrop =
|
||||
this.isShown = null
|
||||
|
||||
if (this.options.remote) {
|
||||
this.$element
|
||||
.find('.modal-content')
|
||||
.load(this.options.remote, $.proxy(function () {
|
||||
this.$element.trigger('loaded.bs.modal')
|
||||
}, this))
|
||||
}
|
||||
}
|
||||
|
||||
Modal.DEFAULTS = {
|
||||
backdrop: true,
|
||||
keyboard: true,
|
||||
show: true
|
||||
}
|
||||
|
||||
Modal.prototype.toggle = function (_relatedTarget) {
|
||||
return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
|
||||
}
|
||||
|
||||
Modal.prototype.show = function (_relatedTarget) {
|
||||
var that = this
|
||||
var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
|
||||
|
||||
this.$element.trigger(e)
|
||||
|
||||
if (this.isShown || e.isDefaultPrevented()) return
|
||||
|
||||
this.isShown = true
|
||||
|
||||
this.escape()
|
||||
|
||||
this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
|
||||
|
||||
this.backdrop(function () {
|
||||
var transition = $.support.transition && that.$element.hasClass('fade')
|
||||
|
||||
if (!that.$element.parent().length) {
|
||||
that.$element.appendTo(document.body) // don't move modals dom position
|
||||
}
|
||||
|
||||
that.$element
|
||||
.show()
|
||||
.scrollTop(0)
|
||||
|
||||
if (transition) {
|
||||
that.$element[0].offsetWidth // force reflow
|
||||
}
|
||||
|
||||
that.$element
|
||||
.addClass('in')
|
||||
.attr('aria-hidden', false)
|
||||
|
||||
that.enforceFocus()
|
||||
|
||||
var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
|
||||
|
||||
transition ?
|
||||
that.$element.find('.modal-dialog') // wait for modal to slide in
|
||||
.one($.support.transition.end, function () {
|
||||
that.$element.focus().trigger(e)
|
||||
})
|
||||
.emulateTransitionEnd(300) :
|
||||
that.$element.focus().trigger(e)
|
||||
})
|
||||
}
|
||||
|
||||
Modal.prototype.hide = function (e) {
|
||||
if (e) e.preventDefault()
|
||||
|
||||
e = $.Event('hide.bs.modal')
|
||||
|
||||
this.$element.trigger(e)
|
||||
|
||||
if (!this.isShown || e.isDefaultPrevented()) return
|
||||
|
||||
this.isShown = false
|
||||
|
||||
this.escape()
|
||||
|
||||
$(document).off('focusin.bs.modal')
|
||||
|
||||
this.$element
|
||||
.removeClass('in')
|
||||
.attr('aria-hidden', true)
|
||||
.off('click.dismiss.bs.modal')
|
||||
|
||||
$.support.transition && this.$element.hasClass('fade') ?
|
||||
this.$element
|
||||
.one($.support.transition.end, $.proxy(this.hideModal, this))
|
||||
.emulateTransitionEnd(300) :
|
||||
this.hideModal()
|
||||
}
|
||||
|
||||
Modal.prototype.enforceFocus = function () {
|
||||
$(document)
|
||||
.off('focusin.bs.modal') // guard against infinite focus loop
|
||||
.on('focusin.bs.modal', $.proxy(function (e) {
|
||||
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
|
||||
this.$element.focus()
|
||||
}
|
||||
}, this))
|
||||
}
|
||||
|
||||
Modal.prototype.escape = function () {
|
||||
if (this.isShown && this.options.keyboard) {
|
||||
this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
|
||||
e.which == 27 && this.hide()
|
||||
}, this))
|
||||
} else if (!this.isShown) {
|
||||
this.$element.off('keyup.dismiss.bs.modal')
|
||||
}
|
||||
}
|
||||
|
||||
Modal.prototype.hideModal = function () {
|
||||
var that = this
|
||||
this.$element.hide()
|
||||
this.backdrop(function () {
|
||||
that.removeBackdrop()
|
||||
that.$element.trigger('hidden.bs.modal')
|
||||
})
|
||||
}
|
||||
|
||||
Modal.prototype.removeBackdrop = function () {
|
||||
this.$backdrop && this.$backdrop.remove()
|
||||
this.$backdrop = null
|
||||
}
|
||||
|
||||
Modal.prototype.backdrop = function (callback) {
|
||||
var animate = this.$element.hasClass('fade') ? 'fade' : ''
|
||||
|
||||
if (this.isShown && this.options.backdrop) {
|
||||
var doAnimate = $.support.transition && animate
|
||||
|
||||
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
|
||||
.appendTo(document.body)
|
||||
|
||||
this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
|
||||
if (e.target !== e.currentTarget) return
|
||||
this.options.backdrop == 'static'
|
||||
? this.$element[0].focus.call(this.$element[0])
|
||||
: this.hide.call(this)
|
||||
}, this))
|
||||
|
||||
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
|
||||
|
||||
this.$backdrop.addClass('in')
|
||||
|
||||
if (!callback) return
|
||||
|
||||
doAnimate ?
|
||||
this.$backdrop
|
||||
.one($.support.transition.end, callback)
|
||||
.emulateTransitionEnd(150) :
|
||||
callback()
|
||||
|
||||
} else if (!this.isShown && this.$backdrop) {
|
||||
this.$backdrop.removeClass('in')
|
||||
|
||||
$.support.transition && this.$element.hasClass('fade') ?
|
||||
this.$backdrop
|
||||
.one($.support.transition.end, callback)
|
||||
.emulateTransitionEnd(150) :
|
||||
callback()
|
||||
|
||||
} else if (callback) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MODAL PLUGIN DEFINITION
|
||||
// =======================
|
||||
|
||||
var old = $.fn.modal
|
||||
|
||||
$.fn.modal = function (option, _relatedTarget) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.modal')
|
||||
var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
|
||||
if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
|
||||
if (typeof option == 'string') data[option](_relatedTarget)
|
||||
else if (options.show) data.show(_relatedTarget)
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.modal.Constructor = Modal
|
||||
|
||||
|
||||
// MODAL NO CONFLICT
|
||||
// =================
|
||||
|
||||
$.fn.modal.noConflict = function () {
|
||||
$.fn.modal = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// MODAL DATA-API
|
||||
// ==============
|
||||
|
||||
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
|
||||
var $this = $(this)
|
||||
var href = $this.attr('href')
|
||||
var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
|
||||
var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
|
||||
|
||||
if ($this.is('a')) e.preventDefault()
|
||||
|
||||
$target
|
||||
.modal(option, this)
|
||||
.one('hide', function () {
|
||||
$this.is(':visible') && $this.focus()
|
||||
})
|
||||
})
|
||||
|
||||
$(document)
|
||||
.on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
|
||||
.on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
|
||||
|
||||
}(jQuery);
|
||||
|
||||
/* ========================================================================
|
||||
* Bootstrap: transition.js v3.1.0
|
||||
* Bootstrap: transition.js v3.1.1
|
||||
* http://getbootstrap.com/javascript/#transitions
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
|
||||
2
public/js/bootstrap.min.js
vendored
@@ -200,7 +200,8 @@
|
||||
//});
|
||||
fd.append('format', 'json');
|
||||
fd.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
|
||||
fd.append('delete-day', ($("#delete-day").prop('checked')) ? 1 : 0);
|
||||
fd.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
|
||||
fd.append('delete-day', ($("#delete-day").val()));
|
||||
|
||||
widget.settings.onBeforeUpload.call(widget.element, widget.queuePos);
|
||||
|
||||
|
||||
3
public/js/dmuploader.min.js
vendored
@@ -10,5 +10,6 @@
|
||||
// c.each(h.settings.extraData,function(i,j){f.append(i,j)});
|
||||
f.append('format', 'json');
|
||||
f.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
|
||||
f.append('delete-day', ($("#delete-day").prop('checked')) ? 1 : 0);
|
||||
f.append('delete-day', ($("#delete-day").val()));
|
||||
f.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
|
||||
h.settings.onBeforeUpload.call(h.element,h.queuePos);h.queueRunning=true;c.ajax({url:h.settings.url,type:h.settings.method,dataType:h.settings.dataType,data:f,cache:false,contentType:false,processData:false,forceSync:false,xhr:function(){var i=c.ajaxSettings.xhr();if(i.upload){i.upload.addEventListener("progress",function(m){var l=0;var j=m.loaded||m.position;var k=m.total||e.totalSize;if(m.lengthComputable){l=Math.ceil(j/k*100)}h.settings.onUploadProgress.call(h.element,h.queuePos,l)},false)}return i},success:function(j,i,k){h.settings.onUploadSuccess.call(h.element,h.queuePos,j)},error:function(k,i,j){h.settings.onUploadError.call(h.element,h.queuePos,j)},complete:function(i,j){h.processQueue()}})};c.fn.dmUploader=function(f){return this.each(function(){if(!c.data(this,b)){c.data(this,b,new a(this,f))}})};c(document).on("dragenter",function(f){f.stopPropagation();f.preventDefault()});c(document).on("dragover",function(f){f.stopPropagation();f.preventDefault()});c(document).on("drop",function(f){f.stopPropagation();f.preventDefault()})})(jQuery);
|
||||
|
||||
346
public/js/jquery.simplegraph.js
Normal file
@@ -0,0 +1,346 @@
|
||||
function SimpleGraph(data, labels, canvas, settings) {
|
||||
|
||||
this.settings = settings;
|
||||
|
||||
setStyleDefaults(settings);
|
||||
|
||||
this.dataSet = new DataSet(data, labels, this.settings);
|
||||
this.grid = new Grid(this.dataSet, this.settings);
|
||||
this.canvas = canvas;
|
||||
|
||||
this.draw = function() {
|
||||
if (this.settings.drawGrid) {
|
||||
this.grid.draw(this.canvas);
|
||||
}
|
||||
if (this.settings.yAxisCaption) {
|
||||
this.dataSet.labelYAxis(this.grid, this.canvas);
|
||||
}
|
||||
this.dataSet.labelXAxis(this.grid, this.canvas);
|
||||
this.dataSet.plot(this.grid, this.canvas);
|
||||
};
|
||||
|
||||
this.replaceDataSet = function(dataSet) {
|
||||
this.dataSet = new DataSet(dataSet, dataSet.labels, this.settings);
|
||||
this.grid = new Grid(this.dataSet, this.settings);
|
||||
};
|
||||
|
||||
this.plotCurrentDataSet = function() {
|
||||
this.dataSet.plot(this.grid, this.canvas);
|
||||
};
|
||||
|
||||
function setStyleDefaults(settings) {
|
||||
var targets = ["xAxisLabel", "yAxisLabel", "yAxisCaption", "hoverLabel", "hoverValue"];
|
||||
var types = ["Color", "Font", "FontSize", "FontWeight"];
|
||||
jQuery.each(targets, function(index, target) {
|
||||
jQuery.each(types, function(index, type) {
|
||||
if (!settings[target + type]) {
|
||||
settings[target + type] = settings["label" + type];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
settings.labelStyle = {
|
||||
font: settings.labelFontSize + '"' + settings.labelFont + '"',
|
||||
fill: settings.labelColor
|
||||
};
|
||||
|
||||
jQuery.each(targets, function(index, target) {
|
||||
settings[target + "Style"] = {
|
||||
font: settings[target + "FontSize"] + ' ' + settings[target + "Font"],
|
||||
fill: settings[target + "Color"],
|
||||
"font-weight": settings[target + "FontWeight"]
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Holds the data and labels to be plotted, provides methods for labelling the x and y axes,
|
||||
// and for plotting it's own points. Each method requires a grid object for translating values to
|
||||
// x,y pixel coordinates and a canvas object on which to draw.
|
||||
function DataSet(data, labels, settings) {
|
||||
this.data = data;
|
||||
this.labels = labels;
|
||||
this.settings = settings;
|
||||
|
||||
this.labelXAxis = function(grid, canvas) {
|
||||
(function(ds) {
|
||||
jQuery.each(ds.labels, function(i, label) {
|
||||
var x = grid.x(i);
|
||||
canvas.text(x + ds.settings.xAxisLabelOffset, ds.settings.height - 6, label).attr(ds.settings.xAxisLabelStyle);
|
||||
});
|
||||
})(this);
|
||||
};
|
||||
|
||||
this.labelYAxis = function(grid, canvas) {
|
||||
// Legend
|
||||
canvas.rect(
|
||||
grid.leftEdge - (30 + this.settings.yAxisOffset), //TODO PARAM - Label Colum Width
|
||||
grid.topEdge,
|
||||
30, //TODO PARAM - Label Column Width
|
||||
grid.height
|
||||
).attr({stroke: this.settings.lineColor, fill: this.settings.lineColor, opacity: 0.3}); //TODO PARAMS - legend border and fill style
|
||||
|
||||
for (var i = 1, ii = (grid.rows); i < (ii - this.settings.lowerBound/2); i = i + 2) {
|
||||
var value = (ii - i)*2,
|
||||
y = grid.y(value) + 4, // TODO: Value of 4 works for default dimensions, expect will need to scale
|
||||
x = grid.leftEdge - (6 + this.settings.yAxisOffset);
|
||||
canvas.text(x, y, value).attr(this.settings.yAxisLabelStyle);
|
||||
}
|
||||
var caption = canvas.text(
|
||||
grid.leftEdge - (20 + this.settings.yAxisOffset),
|
||||
(grid.height/2) + (this.settings.yAxisCaption.length / 2),
|
||||
this.settings.yAxisCaption + " (" + this.settings.units + ")").attr(this.settings.yAxisCaptionStyle).rotate(270);
|
||||
// Increase the offset for the next caption (if any)
|
||||
this.settings.yAxisOffset = this.settings.yAxisOffset + 30;
|
||||
};
|
||||
|
||||
this.plot = function(grid, canvas) {
|
||||
var line_path = canvas.path({
|
||||
stroke: this.settings.lineColor,
|
||||
"stroke-width": this.settings.lineWidth,
|
||||
"stroke-linejoin": this.settings.lineJoin
|
||||
});
|
||||
|
||||
var fill_path = canvas.path({
|
||||
stroke: "none",
|
||||
fill: this.settings.fillColor,
|
||||
opacity: this.settings.fillOpacity
|
||||
}).moveTo(this.settings.leftGutter, this.settings.height - this.settings.bottomGutter);
|
||||
|
||||
var bars = canvas.group(),
|
||||
dots = canvas.group(),
|
||||
cover = canvas.group();
|
||||
|
||||
var hoverFrame = dots.rect(10, 10, 100, 40, 5).attr({
|
||||
fill: "#fff", stroke: "#474747", "stroke-width": 2}).hide(); //TODO PARAM - fill colour, border colour, border width
|
||||
var hoverText = [];
|
||||
hoverText[0] = canvas.text(60, 25, "").attr(this.settings.hoverValueStyle).hide();
|
||||
hoverText[1] = canvas.text(60, 40, "").attr(this.settings.hoverLabelStyle).hide();
|
||||
|
||||
// Plot the points
|
||||
(function(dataSet) {
|
||||
jQuery.each(dataSet.data, function(i, value) {
|
||||
var y = grid.y(value),
|
||||
x = grid.x(i),
|
||||
label = dataSet.labels ? dataSet.labels[i] : " ";
|
||||
|
||||
if (dataSet.settings.drawPoints) {
|
||||
var dot = dots.circle(x, y, dataSet.settings.pointRadius).attr({fill: dataSet.settings.pointColor, stroke: dataSet.settings.pointColor});
|
||||
}
|
||||
if (dataSet.settings.drawBars) {
|
||||
bars.rect(x + dataSet.settings.barOffset, y, dataSet.settings.barWidth, (dataSet.settings.height - dataSet.settings.bottomGutter) - y).attr({fill: dataSet.settings.barColor, stroke: "none"});
|
||||
}
|
||||
if (dataSet.settings.drawLine) {
|
||||
line_path[i == 0 ? "moveTo" : "cplineTo"](x, y, 5);
|
||||
}
|
||||
if (dataSet.settings.fillUnderLine) {
|
||||
fill_path[i == 0 ? "lineTo" : "cplineTo"](x, y, 5);
|
||||
}
|
||||
if (dataSet.settings.addHover) {
|
||||
var rect = canvas.rect(x - 50, y - 50, 100, 100).attr({stroke: "none", fill: "#fff", opacity: 0}); //TODO PARAM - hover target width / height
|
||||
jQuery(rect[0]).hover( function() {
|
||||
jQuery.fn.simplegraph.hoverIn(canvas, value, label, x, y, hoverFrame, hoverText, dot, dataSet.settings);
|
||||
},
|
||||
function() {
|
||||
jQuery.fn.simplegraph.hoverOut(canvas, hoverFrame, hoverText, dot, dataSet.settings);
|
||||
});
|
||||
}
|
||||
});
|
||||
})(this);
|
||||
|
||||
if (this.settings.fillUnderLine) {
|
||||
fill_path.lineTo(grid.x(this.data.length - 1), this.settings.height - this.settings.bottomGutter).andClose();
|
||||
}
|
||||
hoverFrame.toFront();
|
||||
};
|
||||
}
|
||||
|
||||
// Holds the dimensions of the grid, and provides methods to convert values into x,y
|
||||
// pixel coordinates. Also, provides a method to draw a grid on a supplied canvas.
|
||||
function Grid(dataSet, settings) {
|
||||
this.dataSet = dataSet;
|
||||
this.settings = settings;
|
||||
|
||||
this.calculateMaxYAxis = function() {
|
||||
var max = Math.max.apply(Math, this.dataSet.data),
|
||||
maxOveride = this.settings.minYAxisValue;
|
||||
if (maxOveride && maxOveride > max) {
|
||||
max = maxOveride;
|
||||
}
|
||||
return max;
|
||||
};
|
||||
|
||||
this.setYAxis = function() {
|
||||
this.height = this.settings.height - this.settings.topGutter - this.settings.bottomGutter;
|
||||
this.maxValueYAxis = this.calculateMaxYAxis();
|
||||
this.Y = this.height / (this.maxValueYAxis - this.settings.lowerBound);
|
||||
};
|
||||
|
||||
this.setXAxis = function() {
|
||||
this.X = (this.settings.width - this.settings.leftGutter) / (this.dataSet.data.length - 0.4);
|
||||
};
|
||||
|
||||
this.setDimensions = function() {
|
||||
this.leftEdge = this.settings.leftGutter;
|
||||
this.topEdge = this.settings.topGutter;
|
||||
this.width = this.settings.width - this.settings.leftGutter - this.X;
|
||||
this.columns = this.dataSet.data.length - 1;
|
||||
this.rows = (this.maxValueYAxis - this.settings.lowerBound) / 2; //TODO PARAM - steps per row
|
||||
};
|
||||
|
||||
this.draw = function(canvas) {
|
||||
canvas.drawGrid(
|
||||
this.leftEdge,
|
||||
this.topEdge,
|
||||
this.width,
|
||||
this.height,
|
||||
this.columns,
|
||||
this.rows,
|
||||
this.settings.gridBorderColor
|
||||
);
|
||||
};
|
||||
|
||||
this.x = function(value) {
|
||||
return this.settings.leftGutter + this.X * value;
|
||||
};
|
||||
|
||||
this.y = function(value) {
|
||||
return this.settings.height - this.settings.bottomGutter - this.Y * (value - this.settings.lowerBound);
|
||||
};
|
||||
|
||||
this.setYAxis();
|
||||
this.setXAxis();
|
||||
this.setDimensions();
|
||||
|
||||
};
|
||||
|
||||
(function($) {
|
||||
|
||||
//- required to implement hover function
|
||||
var isLabelVisible;
|
||||
var leaveTimer;
|
||||
|
||||
$.fn.simplegraph = function(data, labels, options) {
|
||||
var settings = $.extend({}, $.fn.simplegraph.defaults, options);
|
||||
setPenColor(settings);
|
||||
|
||||
return this.each( function() {
|
||||
var canvas = Raphael(this, settings.width, settings.height);
|
||||
var simplegraph = new SimpleGraph(data, labels, canvas, settings);
|
||||
|
||||
simplegraph.draw();
|
||||
|
||||
// Stash simplegraph object away for future reference
|
||||
$.data(this, "simplegraph", simplegraph);
|
||||
});
|
||||
};
|
||||
|
||||
// Plot another set of values on an existing graph, use it like this:
|
||||
// $("#target").simplegraph(data, labels).simplegraph_more(moreData);
|
||||
$.fn.simplegraph_more = function(data, options) {
|
||||
return this.each( function() {
|
||||
var sg = $.data(this, "simplegraph");
|
||||
sg.dataSet = new DataSet(data, sg.dataSet.labels, sg.settings);
|
||||
sg.settings.penColor = options.penColor;
|
||||
setPenColor(sg.settings);
|
||||
sg.settings = $.extend(sg.settings, options);
|
||||
sg.grid = new Grid(sg.dataSet, sg.settings);
|
||||
sg.dataSet.labelYAxis(sg.grid, sg.canvas);
|
||||
sg.dataSet.plot(sg.grid, sg.canvas);
|
||||
});
|
||||
};
|
||||
|
||||
// Public
|
||||
|
||||
$.fn.simplegraph.defaults = {
|
||||
drawGrid: false,
|
||||
units: "",
|
||||
// Dimensions
|
||||
width: 600,
|
||||
height: 250,
|
||||
leftGutter: 30,
|
||||
bottomGutter: 20,
|
||||
topGutter: 20,
|
||||
// Label Style
|
||||
labelColor: "#000",
|
||||
labelFont: "Helvetica",
|
||||
labelFontSize: "10px",
|
||||
labelFontWeight: "normal",
|
||||
// Grid Style
|
||||
gridBorderColor: "#ccc",
|
||||
// -- Y Axis Captions
|
||||
yAxisOffset: 0,
|
||||
// -- Y Axis Captions
|
||||
xAxisLabelOffset: 0,
|
||||
// Graph Style
|
||||
// -- Points
|
||||
drawPoints: false,
|
||||
pointColor: "#000",
|
||||
pointRadius: 3,
|
||||
activePointRadius: 5,
|
||||
// -- Line
|
||||
drawLine: true,
|
||||
lineColor: "#000",
|
||||
lineWidth: 3,
|
||||
lineJoin: "round",
|
||||
// -- Bars
|
||||
drawBars: false,
|
||||
barColor: "#000",
|
||||
barWidth: 10,
|
||||
barOffset: 0,
|
||||
// -- Fill
|
||||
fillUnderLine: false,
|
||||
fillColor: "#000",
|
||||
fillOpacity: 0.2,
|
||||
// -- Hover
|
||||
addHover: true,
|
||||
// Calculations
|
||||
lowerBound: 0
|
||||
};
|
||||
|
||||
// Default hoverIn callback, this is public and as such can be overwritten. You can write your
|
||||
// own call back with the same signature if you want different behaviour.
|
||||
$.fn.simplegraph.hoverIn = function(canvas, value, label, x, y, frame, hoverLabel, dot, settings) {
|
||||
clearTimeout(leaveTimer);
|
||||
var newcoord = {x: x * 1 + 7.5, y: y - 19};
|
||||
if (newcoord.x + 100 > settings.width) {
|
||||
newcoord.x -= 114;
|
||||
}
|
||||
hoverLabel[0].attr({text: value}).show().animate({x : newcoord.x + 50, y : newcoord.y + 15}, (isLabelVisible ? 100 : 0));
|
||||
hoverLabel[1].attr({text: label}).show().animate({x : newcoord.x + 50, y : newcoord.y + 30}, (isLabelVisible ? 100 : 0));
|
||||
frame.show().animate({x: newcoord.x, y: newcoord.y}, (isLabelVisible ? 100 : 0));
|
||||
if (settings.drawPoints) {
|
||||
dot.attr("r", settings.activePointRadius);
|
||||
}
|
||||
isLabelVisible = true;
|
||||
canvas.safari();
|
||||
};
|
||||
|
||||
// Default hoverOut callback, this is public and as such can be overwritten. You can write your
|
||||
// own call back with the same signature if you want different behaviour.
|
||||
$.fn.simplegraph.hoverOut = function(canvas, frame, label, dot, settings) {
|
||||
if (settings.drawPoints) {
|
||||
dot.attr("r", settings.pointRadius);
|
||||
}
|
||||
canvas.safari();
|
||||
leaveTimer = setTimeout(function () {
|
||||
isLabelVisible = false;
|
||||
frame.hide();
|
||||
label[0].hide();
|
||||
label[1].hide();
|
||||
canvas.safari();
|
||||
}, 1);
|
||||
};
|
||||
|
||||
// Private
|
||||
|
||||
function setPenColor(settings) {
|
||||
if (settings.penColor) {
|
||||
settings.lineColor = settings.penColor;
|
||||
settings.pointColor = settings.penColor;
|
||||
settings.fillColor = settings.penColor;
|
||||
settings.barColor = settings.penColor;
|
||||
}
|
||||
}
|
||||
|
||||
})(jQuery);
|
||||
26
public/js/lutim.js
Normal file
@@ -0,0 +1,26 @@
|
||||
$('document').ready(function() {
|
||||
$('.jsonly').show();
|
||||
// Are we in a mozilla navigator? (well, are we in a navigator which can handle webapps?)
|
||||
if (navigator.mozApps !== undefined) {
|
||||
var installCheck = navigator.mozApps.checkInstalled(manifestUrl);
|
||||
installCheck.onsuccess = function() {
|
||||
if(installCheck.result === null) {
|
||||
var button = $('#install-app');
|
||||
// Show app install button when app is not installed
|
||||
button.css('display','inline-block');
|
||||
button.click(function() {
|
||||
var request = window.navigator.mozApps.install(manifestUrl);
|
||||
request.onsuccess = function () {
|
||||
// Save the App object that is returned
|
||||
var appRecord = this.result;
|
||||
button.css('display','none');
|
||||
};
|
||||
request.onerror = function () {
|
||||
// Display the error information from the DOMError object
|
||||
alert('Install failed, error: ' + this.error.name);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
1
public/js/morris-0.4.3.min.js
vendored
Normal file
10
public/js/raphael-min.js
vendored
Normal file
2215
public/js/raphael.js
Normal file
53
public/js/raphael.path.methods.js
Normal file
@@ -0,0 +1,53 @@
|
||||
Raphael.el.isAbsolute = true;
|
||||
Raphael.el.absolutely = function () {
|
||||
this.isAbsolute = 1;
|
||||
return this;
|
||||
};
|
||||
Raphael.el.relatively = function () {
|
||||
this.isAbsolute = 0;
|
||||
return this;
|
||||
};
|
||||
Raphael.el.moveTo = function (x, y) {
|
||||
this._last = {x: x, y: y};
|
||||
return this.attr({path: this.attrs.path + ["m", "M"][+this.isAbsolute] + parseFloat(x) + " " + parseFloat(y)});
|
||||
};
|
||||
Raphael.el.lineTo = function (x, y) {
|
||||
this._last = {x: x, y: y};
|
||||
return this.attr({path: this.attrs.path + ["l", "L"][+this.isAbsolute] + parseFloat(x) + " " + parseFloat(y)});
|
||||
};
|
||||
Raphael.el.arcTo = function (rx, ry, large_arc_flag, sweep_flag, x, y, angle) {
|
||||
this._last = {x: x, y: y};
|
||||
return this.attr({path: this.attrs.path + ["a", "A"][+this.isAbsolute] + [parseFloat(rx), parseFloat(ry), +angle, large_arc_flag, sweep_flag, parseFloat(x), parseFloat(y)].join(" ")});
|
||||
};
|
||||
Raphael.el.curveTo = function () {
|
||||
var args = Array.prototype.splice.call(arguments, 0, arguments.length),
|
||||
d = [0, 0, 0, 0, "s", 0, "c"][args.length] || "";
|
||||
this.isAbsolute && (d = d.toUpperCase());
|
||||
this._last = {x: args[args.length - 2], y: args[args.length - 1]};
|
||||
return this.attr({path: this.attrs.path + d + args});
|
||||
};
|
||||
Raphael.el.cplineTo = function (x, y, w) {
|
||||
this.attr({path: this.attrs.path + ["C", this._last.x + w, this._last.y, x - w, y, x, y]});
|
||||
this._last = {x: x, y: y};
|
||||
return this;
|
||||
};
|
||||
Raphael.el.qcurveTo = function () {
|
||||
var d = [0, 1, "t", 3, "q"][arguments.length],
|
||||
args = Array.prototype.splice.call(arguments, 0, arguments.length);
|
||||
if (this.isAbsolute) {
|
||||
d = d.toUpperCase();
|
||||
}
|
||||
this._last = {x: args[args.length - 2], y: args[args.length - 1]};
|
||||
return this.attr({path: this.attrs.path + d + args});
|
||||
};
|
||||
Raphael.el.addRoundedCorner = function (r, dir) {
|
||||
var rollback = this.isAbsolute;
|
||||
rollback && this.relatively();
|
||||
this._last = {x: r * (!!(dir.indexOf("r") + 1) * 2 - 1), y: r * (!!(dir.indexOf("d") + 1) * 2 - 1)};
|
||||
this.arcTo(r, r, 0, {"lu": 1, "rd": 1, "ur": 1, "dl": 1}[dir] || 0, this._last.x, this._last.y);
|
||||
rollback && this.absolutely();
|
||||
return this;
|
||||
};
|
||||
Raphael.el.andClose = function () {
|
||||
return this.attr({path: this.attrs.path + "z"});
|
||||
};
|
||||
67
public/js/stats.js
Normal file
@@ -0,0 +1,67 @@
|
||||
function graph(stats_labels, stats_data, stats_total) {
|
||||
Morris.Line({
|
||||
// ID of the element in which to draw the chart.
|
||||
element: 'evol-holder',
|
||||
// Chart data records -- each entry in this array corresponds to a point on
|
||||
// the chart.
|
||||
data: stats_data,
|
||||
// The name of the data record attribute that contains x-values.
|
||||
xkey: 'day',
|
||||
// A list of names of data record attributes that contain y-values.
|
||||
ykeys: ['value'],
|
||||
// Labels for the ykeys -- will be displayed when you hover over the
|
||||
// chart.
|
||||
labels: ['Uploaded files'],
|
||||
xLabels: 'day',
|
||||
dateFormat: function(x) { return new Date(x).toLocaleDateString(); },
|
||||
xLabelFormat: function(x) { return x.toLocaleDateString(); }
|
||||
});
|
||||
Morris.Line({
|
||||
// ID of the element in which to draw the chart.
|
||||
element: 'total-holder',
|
||||
// Chart data records -- each entry in this array corresponds to a point on
|
||||
// the chart.
|
||||
data: stats_total,
|
||||
// The name of the data record attribute that contains x-values.
|
||||
xkey: 'day',
|
||||
// A list of names of data record attributes that contain y-values.
|
||||
ykeys: ['value'],
|
||||
// Labels for the ykeys -- will be displayed when you hover over the
|
||||
// chart.
|
||||
labels: ['Uploaded files'],
|
||||
xLabels: 'day',
|
||||
lineColors: ['red'],
|
||||
dateFormat: function(x) { return new Date(x).toLocaleDateString(); },
|
||||
xLabelFormat: function(x) { return x.toLocaleDateString(); }
|
||||
});
|
||||
}
|
||||
$(document).ready(function() {
|
||||
// Get the data
|
||||
var stats_labels = [], stats_data = [], stats_total = [];
|
||||
$("#stats-data thead th").each(function () {
|
||||
stats_labels.push($(this).html());
|
||||
});
|
||||
var i = 0;
|
||||
$("#stats-data tbody tr:first-child td").each(function () {
|
||||
var s = stats_labels[i++];
|
||||
s = s.replace(/([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})/, "$3-$2-$1");
|
||||
stats_data.push({ day: s, value: $(this).html()});
|
||||
});
|
||||
i = 0;
|
||||
$("#stats-data tbody tr:nth-child(2) td").each(function () {
|
||||
var s = stats_labels[i++];
|
||||
s = s.replace(/([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})/, "$3-$2-$1");
|
||||
stats_total.push({ day: s, value: $(this).html()});
|
||||
});
|
||||
|
||||
// Hide the data
|
||||
$("#stats-data").hide();
|
||||
|
||||
graph(stats_labels, stats_data, stats_total);
|
||||
|
||||
$(window).resize(function() {
|
||||
$("#evol-holder").empty();
|
||||
$("#total-holder").empty();
|
||||
graph(stats_labels, stats_data, stats_total);
|
||||
});
|
||||
});
|
||||
5
templates/about.html.ep
Normal file
@@ -0,0 +1,5 @@
|
||||
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
|
||||
<div>
|
||||
<%==l 'informations-body', url_for('/')->to_abs(), config('contact') %>
|
||||
<%= link_to url_for('index') => ( class => "btn btn-primary btn-lg" ) => begin %><%=l 'back-to-index' %><% end%>
|
||||
</div>
|
||||
14
templates/data.html.ep.template
Normal file
@@ -0,0 +1,14 @@
|
||||
<div id="stats-data">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,19 +1,74 @@
|
||||
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
|
||||
<div class="messages">
|
||||
% if (defined(flash('short'))) {
|
||||
% if (config('always_encrypt')) {
|
||||
<p><%=l 'always_encrypt' %></p>
|
||||
% }
|
||||
% if (defined(stash('short'))) {
|
||||
<div class="alert alert-success">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<strong><%= flash('filename') %></strong>
|
||||
<ul>
|
||||
<li><%=l 'view-link'%><%= link_to url_for('/')->to_abs.flash('short') => begin %><%= url_for('/')->to_abs.flash('short') %><%= end %></li>
|
||||
<li><%=l 'download-link' %> <%= link_to url_for('/')->to_abs.flash('short').'?dl' => begin %><%= url_for('/')->to_abs.flash('short').'?dl' %><%= end %></li>
|
||||
<li><%=l 'twitter-link' %> <%= link_to url_for('/')->to_abs.flash('short').'?t' => begin %><%= url_for('/')->to_abs.flash('short').'?t' %><%= end %></li>
|
||||
</ul>
|
||||
% if (defined(stash('short'))) {
|
||||
<img class="pull-left thumbnail" alt="<%= stash('filename') %> thumbnail" src="<%= stash('thumb') %>">
|
||||
% }
|
||||
<div>
|
||||
% # Display image informations
|
||||
% my $url = url_for('/')->to_abs().stash('short');
|
||||
<strong><%= stash('filename') %></strong>
|
||||
<a target="_blank" class="btn btn-default btn-primary btn-xs" href="https://twitter.com/share?url=<%= $url %>?t"><%=l 'tweet_it' %></a>
|
||||
<ul class="list-unstyled">
|
||||
% my $delete_url = url_for('delete', {short => stash('real_short'), token => stash('token')})->to_abs();
|
||||
<li><i class="icon icon-eye" title =" <%= l 'view-link' %>"></i> <%= link_to $url => begin %> <%= $url %> <%= end %></li>
|
||||
<li><i class="icon icon-download" title =" <%= l 'download-link' %>"></i> <%= link_to $url.'?dl' => begin %> <%= $url.'?dl' %> <%= end %></li>
|
||||
<li><i class="icon icon-twitter" title =" <%= l 'twitter-link' %>"></i> <%= link_to $url.'?t' => begin %> <%= $url.'?t' %> <%= end %></li>
|
||||
<li><i class="icon icon-trash" title =" <%= l 'delete-link' %>"></i> <%= link_to $delete_url => begin %> <%= $delete_url %> <%= end %></li>
|
||||
</ul>
|
||||
</div>
|
||||
% # Delay modification form
|
||||
% my $modify_url = url_for('modify', {short => stash('real_short'), token => stash('token')})->to_abs();
|
||||
<form class="form" role="form" method="POST" action="<%== $modify_url %>">
|
||||
<div class="form-group form-inline">
|
||||
<input name="image_url" type="hidden" value="<%= $url %>">
|
||||
<select name="delete-day" class="form-control">
|
||||
% for my $delay (qw/0 1 7 30 365/) {
|
||||
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
|
||||
% if (config('max_delay')) {
|
||||
% if ($delay) {
|
||||
% if ($delay < config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% } elsif ($delay == config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% last;
|
||||
% } else {
|
||||
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
|
||||
<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>
|
||||
% last;
|
||||
% }
|
||||
% }
|
||||
% } else {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% }
|
||||
% }
|
||||
</select>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
|
||||
</label>
|
||||
<label <%== (config('always_encrypt')) ? 'class="always-encrypt"' : '' %>>
|
||||
<input type="checkbox" name="crypt"> <%=l 'crypt_image' %>
|
||||
</label>
|
||||
</div>
|
||||
<%= submit_button l('go'), class => 'btn btn-sm btn-default btn-primary', id => 'submitbutton' %>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
% }
|
||||
% if (defined(flash('success'))) {
|
||||
<div class="alert alert-success">
|
||||
<button type="button" class="close jsonly" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<p><%== flash('success') %></p>
|
||||
</div>
|
||||
% }
|
||||
% if (defined(flash('msg'))) {
|
||||
<div class="alert alert-danger">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<button type="button" class="close jsonly" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<strong><%=l 'some-bad' %></strong><br>
|
||||
<%= flash('filename') %> <%= flash('msg') %>
|
||||
</div>
|
||||
@@ -21,39 +76,81 @@
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<form class="form" role="form" method="POST" action="<%== url_for 'add' %>" enctype="multipart/form-data">
|
||||
<div class="form-group form-inline">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
|
||||
</label>
|
||||
<form class="form" role="form" method="POST" action="<%== url_for('add') %>" enctype="multipart/form-data">
|
||||
<div class="form-group form-inline">
|
||||
<select name="delete-day" class="form-control">
|
||||
% for my $delay (qw/0 1 7 30 365/) {
|
||||
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
|
||||
% if (config('max_delay')) {
|
||||
% if ($delay) {
|
||||
% if ($delay < config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% } elsif ($delay == config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% last;
|
||||
% } else {
|
||||
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
|
||||
<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>
|
||||
% last;
|
||||
% }
|
||||
% }
|
||||
% } else {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% }
|
||||
% }
|
||||
</select>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
|
||||
</label>
|
||||
<label <%== (config('always_encrypt')) ? 'class="always-encrypt"' : '' %>>
|
||||
<input type="checkbox" name="crypt"> <%=l 'crypt_image' %>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="delete-day"> <%=l 'delete-day' %>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label for="lutim-file"><%=l 'upload_image' %></label>
|
||||
<input type="file" name="file" id="lutim-file" accept="image/*">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lutim-file-url"><%=l 'upload_image_url' %></label>
|
||||
<input type="url" name="lutim-file-url" placeholder="<%=l 'image_url' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lutim-file"><%=l 'upload_image' %></label>
|
||||
<input type="file" name="file" id="lutim-file" required></input>
|
||||
<p class="help-block"><%=l 'image-only' %></p>
|
||||
</div>
|
||||
<%= submit_button l('go'), class => 'btn btn-default btn-primary', id => 'submitbutton' %>
|
||||
</form>
|
||||
<%= submit_button l('go'), class => 'btn btn-default btn-primary', id => 'submitbutton' %>
|
||||
</form>
|
||||
</noscript>
|
||||
|
||||
<!-- D&D Zone-->
|
||||
<div class="jsonly">
|
||||
<div class="form-group form-inline">
|
||||
<select id="delete-day" class="form-control">
|
||||
% for my $delay (qw/0 1 7 30 365/) {
|
||||
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
|
||||
% if (config('max_delay')) {
|
||||
% if ($delay) {
|
||||
% if ($delay < config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% } elsif ($delay == config('max_delay')) {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% last;
|
||||
% } else {
|
||||
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
|
||||
<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>
|
||||
% last;
|
||||
% }
|
||||
% }
|
||||
% } else {
|
||||
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
|
||||
% }
|
||||
% }
|
||||
</select>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="first-view"> <%=l 'delete-first' %>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="delete-day"> <%=l 'delete-day' %>
|
||||
<label <%== (config('always_encrypt')) ? 'class="always-encrypt"' : '' %>>
|
||||
<input type="checkbox" id="crypt"> <%=l 'crypt_image' %>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,35 +160,119 @@
|
||||
<div class="browser">
|
||||
<label>
|
||||
<span><%=l 'file-browser' %></span>
|
||||
<input type="file" name="files[]" multiple="multiple" title='Click to add Files'>
|
||||
<input type="file" name="files[]" multiple="multiple" title='<%=l 'file-browser' %>' accept="image/*">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block"><%=l 'image-only' %></p>
|
||||
<form class="form-horizontal" role="form" method="POST" action="<%== url_for('add') %>">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-3 col-xs-12"><span class="hidden-spin" style="font-size:200%; display:none;" > <i class="icon-spinner animate-spin pull-right"></i></span><label for="lutim-file-url" class="control-label pull-right"><%=l 'upload_image_url' %></label></span>
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<input type="url" name="lutim-file-url" class="form-control" id="lutim-file-url" placeholder="<%=l 'image_url' %>">
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-default btn-primary pull-right" id="file-url-button"><%=l 'go' %></a>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /D&D Zone -->
|
||||
|
||||
%= javascript begin
|
||||
function link(url, dl) {
|
||||
if (dl !== '') {
|
||||
function link(url, dl, token, modify) {
|
||||
if (token !== undefined) {
|
||||
if (modify !== undefined && modify === true) {
|
||||
return '<%== url_for('index')->to_abs() %>m/'+url+'/'+token;
|
||||
} else {
|
||||
url = 'd/'+url+'/'+token;
|
||||
}
|
||||
} else if (dl !== '') {
|
||||
url = url+'?'+dl;
|
||||
}
|
||||
return '<a href="<%== url_for('index')->to_abs() %>'+url+'"><%== url_for('index')->to_abs() %>'+url+'</a>';
|
||||
}
|
||||
function message(success, msg) {
|
||||
function share(url) {
|
||||
console.log(url);
|
||||
new MozActivity({
|
||||
name: "share",
|
||||
data: {
|
||||
type: "url",
|
||||
number: 1,
|
||||
url: url
|
||||
}
|
||||
});
|
||||
}
|
||||
function tw_url(url) {
|
||||
var btn = ' <a target="_blank" class="btn btn-default btn-primary btn-xs" href="https://twitter.com/share?url=<%== url_for('index')->to_abs() %>'+url+'?t"><%=l 'tweet_it' %></a>';
|
||||
if (navigator.mozSetMessageHandler !== undefined) {
|
||||
btn = btn+' <a target="_blank" class="btn btn-default btn-primary btn-xs" href="" onclick="share(\'<%== url_for('index')->to_abs() %>'+url+'?t\');return false;"><%=l 'share_it' %></a>';
|
||||
}
|
||||
return btn
|
||||
}
|
||||
function modify(url, short) {
|
||||
$.ajax({
|
||||
url : url,
|
||||
type : "POST",
|
||||
data : {
|
||||
'image_url' : '<%== url_for('index')->to_abs() %>'+short,
|
||||
'format' : 'json',
|
||||
'first-view' : ($("#first-view-"+short).prop('checked')) ? 1 : 0,
|
||||
'delete-day' : $("#day-"+short).val()
|
||||
},
|
||||
success: function(data) {
|
||||
alert(data.msg);
|
||||
},
|
||||
error: function() {
|
||||
alert('<%=l 'modify_image_error' %>');
|
||||
}
|
||||
});
|
||||
}
|
||||
function build_message(success, msg) {
|
||||
if(success) {
|
||||
return '<div class="alert alert-success"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button><strong>'
|
||||
var thumb = (msg.thumb !== null) ? '<img class="pull-left thumbnail" alt="'+msg.filename+' thumbnail" src="'+msg.thumb+'">' : ''
|
||||
return '<div class="alert alert-success"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>'
|
||||
+thumb
|
||||
+'<div><strong>'
|
||||
+msg.filename
|
||||
+'</strong><ul><li><%=l 'view-link'%>'
|
||||
+'</strong>'
|
||||
+tw_url(msg.short)
|
||||
+'<ul class="list-unstyled"><li><i class="icon icon-eye" title="<%=l 'view-link' %>"></i> '
|
||||
+link(msg.short, '')
|
||||
+'</a></li><li><%=l 'download-link' %>'
|
||||
+'</li><li><i class="icon icon-download" title="<%=l 'download-link' %>"></i> '
|
||||
+link(msg.short, 'dl')
|
||||
+'</a></li><li><%=l 'twitter-link' %>'
|
||||
+'</li><li><i class="icon icon-twitter" title="<%=l 'twitter-link' %>"></i> '
|
||||
+link(msg.short, 't')
|
||||
+'</li></div>';
|
||||
+'</li><li><i class="icon icon-trash" title="<%=l 'delete-link' %>"></i> '
|
||||
+link(msg.real_short, '', msg.token)
|
||||
+'</li></ul><form class="form" role="form" method="POST" action="'
|
||||
+link(msg.real_short, '', msg.token, true)
|
||||
+'"><div class="form-group form-inline"><select id="day-'+msg.real_short+'" name="delete-day" class="form-control">'
|
||||
% for my $delay (qw/0 1 7 30 365/) {
|
||||
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
|
||||
% if (config('max_delay')) {
|
||||
% if ($delay) {
|
||||
% if ($delay < config('max_delay')) {
|
||||
+'<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>'
|
||||
% } elsif ($delay == config('max_delay')) {
|
||||
+'<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>'
|
||||
% last;
|
||||
% } else {
|
||||
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
|
||||
+'<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>'
|
||||
% last;
|
||||
% }
|
||||
% }
|
||||
% } else {
|
||||
+'<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>'
|
||||
% }
|
||||
% }
|
||||
+'</select> <div class="checkbox"><label><input id="first-view-'+msg.real_short+'" type="checkbox" name="first-view"> <%=l 'delete-first' %></label>'
|
||||
+'</div> '
|
||||
+'<a href="#" onclick="modify(\''+link(msg.real_short, '', msg.token, true)+'\', \''+msg.real_short+'\');return false;" class="btn btn-sm btn-default btn-primary"><%=l 'go' %></a></div></form>'
|
||||
+'</div>';
|
||||
} else {
|
||||
return '<div class="alert alert-danger"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button><strong><%=l 'some-bad' %></strong><br>'
|
||||
+msg.filename
|
||||
+' '
|
||||
+"<br>"
|
||||
+msg.msg
|
||||
+'</div>';
|
||||
}
|
||||
@@ -101,21 +282,138 @@
|
||||
url: '<%== url_for('add') %>',
|
||||
dataType: 'json',
|
||||
allowedTypes: 'image/*',
|
||||
maxFileSize: <%= $max_file_size %>,
|
||||
onNewFile: function(id, file){
|
||||
$(".messages").append('<div id="'+id+'-div">'+file.name+'<br><div class="progress"><div id="'+id+'"class="progress-bar progress-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"><span id="'+id+'-text" class="pull-left" style="padding-left: 10px;"> 0%</span></div></div></div>');
|
||||
},
|
||||
onUploadProgress: function(id, percent){
|
||||
var percentStr = ' '+percent+'%';
|
||||
$('#'+id).prop('aria-valuenow', percent);
|
||||
$('#'+id).prop('style', 'width: '+percent+'%;');
|
||||
$('#'+id+'-text').html(percentStr);
|
||||
},
|
||||
onUploadSuccess: function(id, data){
|
||||
$(".messages").append(message(data.success, data.msg));
|
||||
$('#'+id+'-div').remove();
|
||||
$(".messages").append(build_message(data.success, data.msg));
|
||||
},
|
||||
onUploadError: function(id, message){
|
||||
$(".messages").append(message(false, ''));
|
||||
$(".messages").append(build_message(false, ''));
|
||||
},
|
||||
onFileSizeError: function(file){
|
||||
$(".messages").append(build_message(false, { filename: file.name, msg: '<%= l('file_too_big', $max_file_size) %>'}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function upload_url() {
|
||||
var val = $("#lutim-file-url").val();
|
||||
if (val !== undefined && val !== "") {
|
||||
$("#lutim-file-url").prop('disabled', 'disabled');
|
||||
$(".hidden-spin").css('display', 'block');
|
||||
console.log(val);
|
||||
$.ajax({
|
||||
url : '<%== url_for('add') %>',
|
||||
type : "POST",
|
||||
data : {
|
||||
'lutim-file-url' : val,
|
||||
'format' : 'json',
|
||||
'first-view' : ($("#first-view").prop('checked')) ? 1 : 0,
|
||||
'crypt' : ($("#crypt").prop('checked')) ? 1 : 0,
|
||||
'delete-day' : $("#delete-day").val()
|
||||
},
|
||||
success: function(data) {
|
||||
$(".messages").append(build_message(data.success, data.msg));
|
||||
if (data.success) {
|
||||
$("#lutim-file-url").val('');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$(".messages").append(build_message(false, ''));
|
||||
},
|
||||
complete: function() {
|
||||
$("#lutim-file-url").prop('disabled', '');
|
||||
$(".hidden-spin").css('display', 'none');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("fhdsjnf");
|
||||
}
|
||||
}
|
||||
|
||||
function fileUpload(file) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
fd.append('format', 'json');
|
||||
fd.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
|
||||
fd.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
|
||||
fd.append('delete-day', ($("#delete-day").val()));
|
||||
|
||||
$(".messages").append('<div id="1-div">'+file.name+'<br><div class="progress"><div id="1"class="progress-bar progress-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"><span id="1-text" class="pull-left" style="padding-left: 10px;"> 0%</span></div></div></div>');
|
||||
// Ajax Submit
|
||||
$.ajax({
|
||||
url: '<%== url_for('add') %>',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: fd,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
forceSync: false,
|
||||
xhr: function(){
|
||||
var xhrobj = $.ajaxSettings.xhr();
|
||||
if(xhrobj.upload){
|
||||
xhrobj.upload.addEventListener('progress', function(event) {
|
||||
var percent = 0;
|
||||
var position = event.loaded || event.position;
|
||||
var total = event.total || e.totalSize;
|
||||
if(event.lengthComputable){
|
||||
percent = Math.ceil(position / total * 100);
|
||||
}
|
||||
|
||||
var percentStr = ' '+percent+'%';
|
||||
$('#1').prop('aria-valuenow', percent);
|
||||
$('#1').prop('style', 'width: '+percent+'%;');
|
||||
$('#1-text').html(percentStr);
|
||||
}, false);
|
||||
}
|
||||
|
||||
return xhrobj;
|
||||
},
|
||||
success: function (data, message, xhr){
|
||||
$('#1-div').remove();
|
||||
$(".messages").append(build_message(data.success, data.msg));
|
||||
},
|
||||
error: function (xhr, status, errMsg){
|
||||
$(".messages").append(build_message(false, ''));
|
||||
},
|
||||
});
|
||||
}
|
||||
window.onload = function() {
|
||||
if (navigator.mozSetMessageHandler !== undefined) {
|
||||
navigator.mozSetMessageHandler('activity', function handler(activityRequest) {
|
||||
var activityName = activityRequest.source.name;
|
||||
if (activityName == 'share') {
|
||||
activity = activityRequest;
|
||||
blob = activity.source.data.blobs[0];
|
||||
fileUpload(blob);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$('document').ready(function() {
|
||||
$('.jsonly').show();
|
||||
|
||||
var firstview = ($("#first-view").prop('checked')) ? 1 : 0;
|
||||
var deleteday = ($("#delete-day").prop('checked')) ? 1 : 0;
|
||||
|
||||
bindddz(firstview, deleteday);
|
||||
|
||||
$("#file-url-button").on("click", upload_url);
|
||||
$('#lutim-file-url').keydown( function(e) {
|
||||
var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
|
||||
if(key == 13) {
|
||||
e.preventDefault();
|
||||
upload_url();
|
||||
}
|
||||
});
|
||||
});
|
||||
% end
|
||||
|
||||
@@ -4,38 +4,34 @@
|
||||
% my $url = url_for('/')->to_abs();
|
||||
% $twitter_url .= '?url='.url_escape("$url")
|
||||
% .'&via=framasky'
|
||||
% .'&text=Check out this %23LUTIm instance! ';
|
||||
% .'&text=Check out this %23Lutim instance! ';
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>LUTIm</title>
|
||||
<title>Lutim</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="utf-8" />
|
||||
%= stylesheet 'css/bootstrap.min.css', media => 'screen'
|
||||
%= stylesheet 'css/fontello.css'
|
||||
%= stylesheet 'css/uploader.css'
|
||||
%= stylesheet begin
|
||||
body {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.jsonly {
|
||||
display: none;
|
||||
}
|
||||
% end
|
||||
%= javascript 'js/jquery-2.1.0.min.js'
|
||||
%= javascript 'js/bootstrap.min.js'
|
||||
%= javascript 'js/dmuploader.min.js'
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<link rel="icon" type="image/png" href="<%= url_for('/') %>img/favicon.png">
|
||||
<link rel="icon" sizes="128x128" href="<%= url_for('/') %>img/lutim128.png">
|
||||
<link rel="icon" sizes="196x196" href="<%= url_for('/') %>img/lutim196.png">
|
||||
<link rel="apple-touch-icon" href="<%= url_for('/') %>img/lutim60.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= url_for('/') %>img/lutim76.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= url_for('/') %>img/lutim120.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= url_for('/') %>img/lutim152.png">
|
||||
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="<%= url_for('/') %>img/lutim128.png">
|
||||
% if (current_route 'stats') {
|
||||
%= asset 'stats.css'
|
||||
% } elsif (current_route 'about') {
|
||||
%= asset 'about.css'
|
||||
% } else {
|
||||
%= asset 'index.css'
|
||||
% }
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
% if (defined(config('hosted_by'))) {
|
||||
<div class="pull-right">
|
||||
@@ -43,43 +39,41 @@
|
||||
</div>
|
||||
% }
|
||||
<div>
|
||||
<h1>Let's Upload That Image!</h1>
|
||||
<div class="pull-left hidden-xs logo">
|
||||
<img src="<%= url_for('/') %>img/Lutim_small.png" alt="Lutim logo">
|
||||
</div>
|
||||
<a class="link_nocol" href="<%= url_for('/') %>" title="<%=l 'homepage' %>"><h1 class="hennypenny">Let's Upload That Image!</h1></a>
|
||||
<p>
|
||||
© 2014 <%= link_to 'http://www.fiat-tux.fr' => begin %>Luc Didry<% end %> —
|
||||
<%=l 'license' %> <%= link_to 'https://www.gnu.org/licenses/agpl-3.0.html' => begin %>AGPL<% end %> —
|
||||
<span class="jsonly"><a data-toggle="modal" href="#myModal"><%=l 'informations' %></a> — </span>
|
||||
<%= link_to url_for('about') => begin %><%=l 'informations' %><% end %> —
|
||||
<%= link_to 'https://github.com/ldidry/lutim' => (title => l 'fork-me') => begin %><i class="lead icon icon-github-circled"></i><% end %>
|
||||
<%= link_to $twitter_url => (title => l 'share-twitter') => begin %><i class="lead icon icon-touiteur"></i><% end %>
|
||||
<%= link_to 'https://flattr.com/submit/auto?user_id=_SKy_&url='.$url.'&title=LUTIm&category=software' => (title => 'Flattr this') => begin %><i class="lead icon icon-flattr"></i><% end %>
|
||||
<%= link_to 'bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim' => (title => 'Give Bitcoins') => begin %><i class="lead icon icon-bitcoin"></i><% end %>
|
||||
<%= link_to 'https://flattr.com/submit/auto?user_id=_SKy_&url='.$url.'&title=Lutim&category=software' => (title => 'Flattr this') => begin %><i class="lead icon icon-flattr"></i><% end %>
|
||||
<%= link_to 'bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim' => (title => 'Give Bitcoins') => begin %><i class="lead icon icon-bitcoin"></i><% end %>
|
||||
<a class="btn btn-default btn-xs" href="#" id="install-app"><img src="<%= url_for('/') %>img/rocket.png" alt="mozilla rocket logo"> <%=l 'install_as_webapp' %></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
% if (defined(config('broadcast_message'))) {
|
||||
<div class="alert alert-info">
|
||||
<strong><%= config('broadcast_message') %></strong>
|
||||
</div>
|
||||
% }
|
||||
% if (defined(stash('stop_upload'))) {
|
||||
<div class="alert alert-danger">
|
||||
<strong><%= stash('stop_upload') %></strong>
|
||||
</div>
|
||||
% }
|
||||
%= javascript begin
|
||||
var manifestUrl = '<%== url_for('manifest.webapp')->to_abs() %>';
|
||||
% end
|
||||
% if (current_route 'stats') {
|
||||
%= asset 'stats.js'
|
||||
% } elsif (!(current_route 'about')) {
|
||||
%= asset 'index.js'
|
||||
% }
|
||||
<%= content %>
|
||||
<div class="modal fade bs-modal-lg" id="myModal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 class="modal-title">LUTIm</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<%==l 'informations-body', url_for('index')->to_abs(), config('contact') %>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
<noscript>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">LUTIm</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<%==l 'informations-body', url_for('index')->to_abs(), config('contact') %>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
% if (defined(config('piwik_img'))) {
|
||||
<img src="<%== config('piwik_img') %>" style="border:0" alt="" />
|
||||
|
||||
33
templates/manifest.webapp.ep
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Lutim",
|
||||
"description": "Lets Upload That Image!",
|
||||
"launch_path": "<%= url_for('/') %>",
|
||||
"icons": {
|
||||
"32": "<%= url_for('/img/lutim32.png') %>",
|
||||
"60": "<%= url_for('/img/lutim60.png') %>",
|
||||
"90": "<%= url_for('/img/lutim90.png') %>",
|
||||
"120": "<%= url_for('/img/lutim120.png') %>",
|
||||
"128": "<%= url_for('/img/lutim128.png') %>",
|
||||
"256": "<%= url_for('/img/lutim256.png') %>"
|
||||
},
|
||||
"developer": {
|
||||
"name": "Lutim team !",
|
||||
"url": "https://github.com/ldidry/lutim"
|
||||
},
|
||||
"default_locale": "en",
|
||||
"locales": {
|
||||
"fr": {
|
||||
"description": "Envoyons cette image !"
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"share": {
|
||||
"filters": {
|
||||
"type": [ "image/*"]
|
||||
},
|
||||
"href": "<%= url_for('/') %>",
|
||||
"disposition": "window"
|
||||
}
|
||||
},
|
||||
"chrome": { "navigation": true }
|
||||
}
|
||||
13
templates/stats.html.ep
Normal file
@@ -0,0 +1,13 @@
|
||||
% # vim:set sts=4 sw=4 ts=4 ft=html.epl expandtab:
|
||||
<h4><%= $total %><%=l 'pushed-images' %></h4>
|
||||
<hr>
|
||||
|
||||
%= include 'data'
|
||||
<h4>Uploaded files by days</h4>
|
||||
<div id="evol-holder"></div>
|
||||
<hr>
|
||||
<h4>Evolution of total files</h4>
|
||||
<div id="total-holder"></div>
|
||||
<p><small><%=l 'graph-data-once-a-day' %></small></p>
|
||||
|
||||
<%= link_to url_for('index') => ( class => "btn btn-primary btn-lg" ) => begin %><%=l 'back-to-index' %><% end%>
|
||||
@@ -1,18 +1,17 @@
|
||||
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html style="height:100%;">
|
||||
<head>
|
||||
<title>LUTIm</title>
|
||||
<title>Lutim</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/png" href="<%= url_for('/')->to_abs() %>img/favicon.png">
|
||||
<meta name="twitter:card" content="photo">
|
||||
% if (defined(config('tweet_card_via'))) {
|
||||
<meta name="twitter:site" content="<%= config('tweet_card_via') %>">
|
||||
% }
|
||||
<meta name="twitter:image:src" content="<%= url_for('/')->to_abs().$short %>">
|
||||
</head>
|
||||
<body>
|
||||
<img src="<%= url_for('/').$short %>" alt="<%= $filename %>">
|
||||
<body style="height: 97%;">
|
||||
<img style="max-width:100%; max-height:100%;" src="<%= url_for('/').$short %>" alt="<%= $filename %>">
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -38,117 +38,108 @@ my $d = Locale::gettext->domain("shutter-upload-plugins");
|
||||
$d->dir( $ENV{'SHUTTER_INTL'} );
|
||||
|
||||
my %upload_plugin_info = (
|
||||
'module' => "Lutim",
|
||||
'url' => "http://lut.im/",
|
||||
'registration' => "-",
|
||||
'description' => $d->get( "Upload screenshots to lut.im" ),
|
||||
'supports_anonymous_upload' => TRUE,
|
||||
'supports_authorized_upload' => FALSE,
|
||||
'module' => "Lutim",
|
||||
'url' => "https://lut.im/",
|
||||
'registration' => "-",
|
||||
'name' => "Lutim",
|
||||
'description' => "Upload screenshots to Lutim",
|
||||
'supports_anonymous_upload' => TRUE,
|
||||
'supports_authorized_upload' => FALSE,
|
||||
'supports_oauth_upload' => FALSE,
|
||||
);
|
||||
|
||||
binmode( STDOUT, ":utf8" );
|
||||
if ( exists $upload_plugin_info{$ARGV[ 0 ]} ) {
|
||||
print $upload_plugin_info{$ARGV[ 0 ]};
|
||||
exit;
|
||||
print $upload_plugin_info{$ARGV[ 0 ]};
|
||||
exit;
|
||||
}
|
||||
|
||||
###################################################
|
||||
|
||||
#don't touch this
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $class = shift;
|
||||
|
||||
#call constructor of super class (host, debug_cparam, shutter_root, gettext_object, main_gtk_window, ua)
|
||||
my $self = $class->SUPER::new( shift, shift, shift, shift, shift, shift );
|
||||
#call constructor of super class (host, debug_cparam, shutter_root, gettext_object, main_gtk_window, ua)
|
||||
my $self = $class->SUPER::new( shift, shift, shift, shift, shift, shift );
|
||||
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
}
|
||||
|
||||
#load some custom modules here (or do other custom stuff)
|
||||
sub init {
|
||||
my $self = shift;
|
||||
my $self = shift;
|
||||
|
||||
#do custom stuff here
|
||||
use WWW::Mechanize;
|
||||
use HTTP::Status;
|
||||
use HTTP::Request::Common 'POST';
|
||||
use JSON;
|
||||
use LWP::UserAgent;
|
||||
use HTTP::Request::Common;
|
||||
|
||||
$self->{_mech} = WWW::Mechanize->new( agent => "$self->{_ua}", timeout => 20 );
|
||||
$self->{_http_status} = undef;
|
||||
|
||||
return TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#handle
|
||||
sub upload {
|
||||
my ( $self, $upload_filename, $username, $password ) = @_;
|
||||
my ( $self, $upload_filename, $username, $password ) = @_;
|
||||
|
||||
#store as object vars
|
||||
$self->{_filename} = $upload_filename;
|
||||
$self->{_username} = $username;
|
||||
$self->{_password} = $password;
|
||||
#store as object vars
|
||||
$self->{_filename} = $upload_filename;
|
||||
$self->{_username} = $username;
|
||||
$self->{_password} = $password;
|
||||
|
||||
my $filesize = -s $upload_filename;
|
||||
my $max_filesize = 15360000;
|
||||
if ( $filesize > $max_filesize ) {
|
||||
$self->{_links}{'status'} = 998;
|
||||
$self->{_links}{'max_filesize'} = sprintf( "%.2f", $max_filesize / 1024 ) . " KB";
|
||||
return %{ $self->{_links} };
|
||||
}
|
||||
utf8::encode $upload_filename;
|
||||
utf8::encode $password;
|
||||
utf8::encode $username;
|
||||
|
||||
utf8::encode $upload_filename;
|
||||
utf8::encode $password;
|
||||
utf8::encode $username;
|
||||
my $json = JSON->new();
|
||||
|
||||
eval{
|
||||
my $browser = LWP::UserAgent->new(
|
||||
'timeout' => 20,
|
||||
'keep_alive' => 10,
|
||||
'env_proxy' => 1,
|
||||
);
|
||||
|
||||
my $url = "http://lut.im";
|
||||
$self->{_mech}->get($url);
|
||||
$self->{_http_status} = $self->{_mech}->status();
|
||||
|
||||
if ( is_success( $self->{_http_status} ) ) {
|
||||
#upload the file
|
||||
eval{
|
||||
|
||||
$self->{_mech}->request(POST $url,
|
||||
Content_Type => 'form-data',
|
||||
Content => [
|
||||
file => [$upload_filename],
|
||||
format => 'json'
|
||||
],
|
||||
);
|
||||
my $url = 'https://lut.im/';
|
||||
my $request = HTTP::Request::Common::POST(
|
||||
$url,
|
||||
Content_Type => 'multipart/form-data',
|
||||
Content => [
|
||||
file => [$upload_filename],
|
||||
format => 'json'
|
||||
]
|
||||
);
|
||||
|
||||
$self->{_http_status} = $self->{_mech}->status();
|
||||
my $response = $browser->request($request);
|
||||
|
||||
if ( is_success( $self->{_http_status} ) ) {
|
||||
my $html_file = $self->{_mech}->content;
|
||||
if ($response->is_success) {
|
||||
my $hash = $json->decode($response->decoded_content);
|
||||
|
||||
my @links = $html_file =~ m/"short":"([^"]+)"/;
|
||||
#my @links = $html_file =~ m{ <textarea>(.*)</textarea> }gx;
|
||||
if ($hash->{success}) {
|
||||
my $link = $url.$hash->{msg}->{short};
|
||||
$self->{_links}->{'view_image'} = $link;
|
||||
$self->{_links}->{'download_link'} = $link.'?dl';
|
||||
$self->{_links}->{'twitter_link'} = $link.'?t';
|
||||
$self->{_links}->{'delete_link'} = $url.'d/'.$hash->{msg}->{real_short}.'/'.$hash->{msg}->{token};
|
||||
|
||||
$self->{_links}{'view_image'} = $url.'/'.$links[0];
|
||||
$self->{_links}{'download_link'} = $url.'/'.$links[0].'?dl';
|
||||
$self->{_links}{'twitter_link'} = $url.'/'.$links[0].'?t';
|
||||
#set success code (200)
|
||||
$self->{_links}{'status'} = 200;
|
||||
} else {
|
||||
$self->{_links}{'status'} = $hash->{msg}->{msg};
|
||||
|
||||
if ( $self->{_debug} ) {
|
||||
print "The following links were returned by http://lut.im:\n";
|
||||
print $self->{_links}{'view_image'},"\n";
|
||||
print $self->{_links}{'download_link'},"\n";
|
||||
print $self->{_links}{'twitter_link'},"\n";
|
||||
}
|
||||
|
||||
$self->{_links}{'status'} = $self->{_http_status};
|
||||
} else {
|
||||
$self->{_links}{'status'} = $self->{_http_status};
|
||||
}
|
||||
|
||||
}else{
|
||||
$self->{_links}{'status'} = $self->{_http_status};
|
||||
}
|
||||
|
||||
};
|
||||
if($@){
|
||||
$self->{_links}{'status'} = $@;
|
||||
}
|
||||
|
||||
return %{ $self->{_links} };
|
||||
}
|
||||
} else {
|
||||
$self->{_links}{'status'} = $response->status_line;
|
||||
}
|
||||
};
|
||||
if($@){
|
||||
$self->{_links}{'status'} = $@;
|
||||
}
|
||||
|
||||
#and return links
|
||||
return %{ $self->{_links} };
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
3
utilities/lutim.default
Normal file
@@ -0,0 +1,3 @@
|
||||
# LDIR is the path where you installed Lutim
|
||||
# It has to end with a final /
|
||||
LDIR=/var/lutim/
|
||||
191
utilities/lutim.init
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/bin/sh
|
||||
# vim: set ts=4 sw=4 sts=4 tw=0:
|
||||
# vim: set expandtab:
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: lutim
|
||||
# Required-Start: $local_fs $remote_fs $network $syslog
|
||||
# Required-Stop: $local_fs $remote_fs $network $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: starts lutim with hypnotoad
|
||||
# Description: starts lutim with hypnotoad
|
||||
### END INIT INFO
|
||||
|
||||
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
DAEMON=script/lutim
|
||||
NAME=lutim
|
||||
DESC=lutim
|
||||
|
||||
if [ -f "/etc/default/lutim" ]
|
||||
then
|
||||
. /etc/default/lutim
|
||||
if [ -z $LDIR ]
|
||||
then
|
||||
echo "LDIR variable is empty, please fill it in /etc/default/lutim"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Missing /etc/default/lutim file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$LDIR$DAEMON" ]
|
||||
then
|
||||
echo "Missing $LDIR$DAEMON file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
do_start()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been started
|
||||
# 1 if daemon was already running
|
||||
# 2 if daemon could not be started
|
||||
|
||||
cd $LDIR
|
||||
carton exec hypnotoad $DAEMON >/dev/null 2>&1
|
||||
return "$?"
|
||||
}
|
||||
|
||||
do_stop()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been stopped
|
||||
# 1 if daemon was already stopped
|
||||
# 2 if daemon could not be stopped
|
||||
# other if a failure occurred
|
||||
|
||||
cd $LDIR
|
||||
carton exec hypnotoad -s $DAEMON >/dev/null 2>&1
|
||||
return "$?"
|
||||
}
|
||||
|
||||
do_status()
|
||||
{
|
||||
cd $LDIR
|
||||
if [ -f "script/hypnotoad.pid" ]
|
||||
then
|
||||
pgrep -lf $DAEMON >/dev/null 2>&1
|
||||
if [ "$?" = "0" ]; then
|
||||
log_progress_msg "$NAME is running"
|
||||
else
|
||||
log_progress_msg "$NAME is NOT running"
|
||||
fi
|
||||
else
|
||||
log_progress_msg "$NAME is NOT running"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
log_daemon_msg "Starting $NAME"
|
||||
cd $LDIR
|
||||
if [ -f "script/hypnotoad.pid" ]
|
||||
then
|
||||
pgrep -lf $DAEMON >/dev/null 2>&1
|
||||
if [ "$?" = "0" ]
|
||||
then
|
||||
log_progress_msg "$NAME is already running. Unable to start."
|
||||
log_end_msg 1;
|
||||
else
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1)
|
||||
log_progress_msg "done"
|
||||
log_end_msg 0
|
||||
;;
|
||||
2)
|
||||
log_progress_msg "failed"
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1)
|
||||
log_progress_msg "done"
|
||||
log_end_msg 0
|
||||
;;
|
||||
2)
|
||||
log_progress_msg "failed"
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping $NAME"
|
||||
cd $LDIR
|
||||
if [ -f "script/hypnotoad.pid" ]
|
||||
then
|
||||
pgrep -lf $DAEMON >/dev/null 2>&1
|
||||
if [ "$?" = "0" ]; then
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1)
|
||||
log_progress_msg "done"
|
||||
log_end_msg 0
|
||||
;;
|
||||
*)
|
||||
log_progress_msg "failed"
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
log_progress_msg "$NAME is NOT running. Unable to stop"
|
||||
log_end_msg 1
|
||||
fi
|
||||
else
|
||||
log_progress_msg "$NAME is NOT running. Unable to stop"
|
||||
log_end_msg 1
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
log_daemon_msg "Checking $NAME status"
|
||||
do_status
|
||||
log_end_msg 0
|
||||
;;
|
||||
reload)
|
||||
log_daemon_msg "Reloading $NAME"
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1)
|
||||
log_progress_msg "done"
|
||||
log_end_msg 0
|
||||
;;
|
||||
2)
|
||||
log_progress_msg "failed"
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
restart)
|
||||
log_daemon_msg "Restarting $NAME"
|
||||
do_stop
|
||||
sleep 1
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1)
|
||||
log_progress_msg "done"
|
||||
log_end_msg 0
|
||||
;;
|
||||
2)
|
||||
log_progress_msg "failed";
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status|reload|restart}" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||