Compare commits

...

3 commits

Author SHA1 Message Date
Benjamin Renard
942d39d755
Translate application's messages with english as default and add french translation 2024-09-22 18:45:33 +02:00
Benjamin Renard
60b8e5cb53
Introduce some pre-commit hooks 2024-09-22 18:45:32 +02:00
Benjamin Renard
67a89bb091
Implement server scases data synchronization 2024-09-22 18:45:31 +02:00
31 changed files with 3660 additions and 997 deletions

6
.codespell-exclusions Normal file
View file

@ -0,0 +1,6 @@
.git
./static/lib
./vendor
./locales/*/LC_MESSAGES/*.po
./locales/*.js
./locales/*.pot

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/config.local.yml
# Ignore vim temporary/backup files # Ignore vim temporary/backup files
*~ *~
.*.swp .*.swp

7
.phplint.yml Normal file
View file

@ -0,0 +1,7 @@
path: ./
jobs: 10
extensions:
- php
exclude:
- vendor
warning: true

33
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,33 @@
# Pre-commit hooks to run tests and ensure code is cleaned.
# See https://pre-commit.com for more information
---
repos:
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
exclude: static/lib/|locales/.*\.js$|\.pot$
#args: ["--write-changes"]
exclude_types: [csv, json, pofile]
- repo: https://github.com/adrienverge/yamllint
rev: v1.32.0
hooks:
- id: yamllint
ignore: .github/
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
- repo: https://github.com/digitalpulp/pre-commit-php.git
rev: 1.4.0
hooks:
- id: php-stan
files: ^(?!example/).*\.(php)$
args: ["--configuration=phpstan.neon"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-executables-have-shebangs
stages: [manual]
- id: check-json
exclude: (.vscode|.devcontainer)

2
.prettierignore Normal file
View file

@ -0,0 +1,2 @@
/static/lib/*
/locales/*

9
.yamllint.yaml Normal file
View file

@ -0,0 +1,9 @@
extends: default
ignore: |
static/lib/*
rules:
line-length:
max: 100
level: warning

View file

@ -1,26 +1,29 @@
{ {
"name": "brenard/mysc", "name": "brenard/mysc",
"description": "My Sweetcase", "description": "My Sweetcase",
"type": "project", "type": "project",
"require": { "require": {
"brenard/eesyphp": "dev-master" "brenard/eesyphp": "dev-master"
}, },
"license": "GPL3", "license": "GPL3",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Brenard\\Mysc\\": "src/" "MySC\\": "src/"
}
},
"authors": [
{
"name": "Benjamin Renard",
"email": "brenard@zionetrix.net"
}
],
"minimum-stability": "dev",
"config": {
"allow-plugins": {
"php-http/discovery": true
}
} }
},
"authors": [
{
"name": "Benjamin Renard",
"email": "brenard@zionetrix.net"
}
],
"minimum-stability": "dev",
"config": {
"allow-plugins": {
"php-http/discovery": true
}
},
"require-dev": {
"phpstan/phpstan": "2.0.x-dev"
}
} }

View file

@ -112,12 +112,12 @@ session:
# #
db: db:
# Sqlite # Sqlite
#dsn: "sqlite:${data_directory}/db.sqlite3" dsn: "sqlite:${data_directory}/db.sqlite3"
#options: null options: null
# Date/Datetime format in database (strptime format) # Date/Datetime format in database (strptime format)
#date_format: '%s' date_format: "%s"
#datetime_format: '%s' datetime_format: "%s"
# Postgresql # Postgresql
#dsn: "pgsql:host=localhost;port=5432;dbname=items" #dsn: "pgsql:host=localhost;port=5432;dbname=items"
@ -144,19 +144,7 @@ db:
# #
auth: auth:
# Enabled authentication # Enabled authentication
enabled: false enabled: true
# Methods to authenticate users
methods:
- form
- http
#- cas
# User backends
backends:
#- ldap
#- db
#- casuser
# #
# Login form # Login form
@ -257,14 +245,6 @@ auth:
# Database user backend # Database user backend
# #
db: db:
# DSN (required)
dsn: "${db.dsn}"
# Username (optional but could be required with some PDO drivers)
user: "${db.user}"
# Password (optional)
password: "${db.password}"
# PDO options (optional)
options: "${db.options}"
# Users table name (optional, default: users) # Users table name (optional, default: users)
#users_table: "users" #users_table: "users"
# Username field name (optional, default: username) # Username field name (optional, default: username)

1
data/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
db.sqlite3

45
data/sqlite.init-db.sql Normal file
View file

@ -0,0 +1,45 @@
CREATE TABLE users (
username text NOT NULL PRIMARY KEY,
name text COLLATE NOCASE NOT NULL,
mail text COLLATE NOCASE,
password text NOT NULL
);
INSERT INTO users (username, name, mail, password) VALUES (
"admin", "Administrator", "admin@example.com",
"$argon2id$v=19$m=65536,t=4,p=1$WTQ0di44NW11MUJ1b3RMQw$+LRAQRaIXE2jhfavNFNuxnEtEUT6tEBz/98pTtD0EnM"
);
CREATE TABLE auth_tokens (
token text NOT NULL PRIMARY KEY,
username text NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE,
creation_date REAL,
expiration_date REAL
);
CREATE TABLE scases (
uuid text NOT NULL PRIMARY KEY,
username text NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE,
name text NOT NULL,
last_change REAL NOT NULL,
removed INTEGER NOT NULL
);
CREATE TABLE categories (
uuid text NOT NULL PRIMARY KEY,
scase_uuid text NOT NULL REFERENCES scases(uuid) ON UPDATE CASCADE ON DELETE CASCADE,
name text NOT NULL,
color text NOT NULL,
last_change REAL NOT NULL,
removed INTEGER NOT NULL
);
CREATE TABLE things (
uuid text NOT NULL PRIMARY KEY,
category_uuid text NOT NULL REFERENCES categories(uuid) ON UPDATE CASCADE ON DELETE CASCADE,
label text NOT NULL,
nb INTEGER NOT NULL,
checked INTEGER NOT NULL,
last_change REAL NOT NULL,
removed INTEGER NOT NULL
);

View file

@ -1,9 +1,12 @@
<?php <?php
use EesyPHP\App; use EesyPHP\App;
use EesyPHP\Db;
use EesyPHP\I18n; use EesyPHP\I18n;
use EesyPHP\SentrySpan; use EesyPHP\SentrySpan;
use MySC\Auth\API;
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED); error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
// Root directory path // Root directory path
@ -17,7 +20,7 @@ else {
if (basename($script) == 'core.php') if (basename($script) == 'core.php')
break; break;
} }
if (!$script) die('Fail to detect root directory path'); if (!$script) die('Failed to detect root directory path');
$root_dir_path = realpath(dirname($script).'/../'); $root_dir_path = realpath(dirname($script).'/../');
// Include App's includes and vendor directories to PHP include paths // Include App's includes and vendor directories to PHP include paths
@ -38,8 +41,25 @@ App::init(
"$root_dir_path/static" "$root_dir_path/static"
), ),
), ),
'auth' => array(
'enabled' => true,
'methods' => array(
'\\MySC\\Auth\\API',
),
'backends' => array(
'db',
),
),
'default' => array( 'default' => array(
// Set here your configuration parameters default value 'auth' => array(
'enabled' => true,
'methods' => array(
'\\MySC\\Auth\\API',
),
'auth_token' => array(
'expiration_delay' => 31536000,
),
),
), ),
), ),
$root_dir_path $root_dir_path
@ -48,7 +68,8 @@ App::init(
$sentry_span = new SentrySpan('core.init', 'Core initialization'); $sentry_span = new SentrySpan('core.init', 'Core initialization');
// Put here your own initialization stuff // Put here your own initialization stuff
Db :: init();
API :: init();
require 'views/index.php'; require 'views/index.php';
$sentry_span->finish(); $sentry_span->finish();

View file

@ -1,8 +1,21 @@
<?php <?php
use EesyPHP\Auth;
use EesyPHP\Db;
use EesyPHP\Log;
use EesyPHP\Tpl; use EesyPHP\Tpl;
use EesyPHP\Url; use EesyPHP\Url;
use MySC\Db\Category;
use MySC\Db\SCase;
use MySC\Db\Thing;
use function EesyPHP\vardump;
use function EesyPHP\generate_uuid;
if (php_sapi_name() == "cli")
return true;
/** /**
* Redirect to homepage * Redirect to homepage
* @param EesyPHP\UrlRequest $request * @param EesyPHP\UrlRequest $request
@ -11,7 +24,7 @@ use EesyPHP\Url;
function handle_redirect_homepage($request) { function handle_redirect_homepage($request) {
Url::redirect("home"); Url::redirect("home");
} }
Url :: add_url_handler(null, 'handle_redirect_homepage'); Url :: add_url_handler(null, 'handle_redirect_homepage', null, false);
/** /**
* Homepage * Homepage
@ -21,7 +34,7 @@ Url :: add_url_handler(null, 'handle_redirect_homepage');
function handle_homepage($request) { function handle_homepage($request) {
Tpl :: display("index.tpl"); Tpl :: display("index.tpl");
} }
Url :: add_url_handler("#^home$#", 'handle_homepage'); Url :: add_url_handler("#^home$#", 'handle_homepage', null, false);
function _list_static_directory_files($root_dir, $dir, &$result, &$last_updated) { function _list_static_directory_files($root_dir, $dir, &$result, &$last_updated) {
foreach (array_diff(scandir($dir), array('.','..')) as $file) { foreach (array_diff(scandir($dir), array('.','..')) as $file) {
@ -61,4 +74,88 @@ function handle_cache_manifest($request) {
} }
Url :: add_url_handler("#^cache\.manifest$#", 'handle_cache_manifest'); Url :: add_url_handler("#^cache\.manifest$#", 'handle_cache_manifest');
/**
* Sync data
* @param EesyPHP\UrlRequest $request
* @return void
*/
function handle_sync($request) {
$data = json_decode($_POST["data"], true);
Log::debug("Sync scases data: %s", vardump($data["scases"]));
$user = Auth::user();
$updated = false;
$db_scases = SCase :: list(['username' => $user -> username]);
foreach($data["scases"] as $scase_uuid => $scase_data) {
Log::debug("sync(): scase %s", $scase_uuid);
$scase = Scase :: from_json($scase_data, $user);
if (array_key_exists($scase_uuid, $db_scases)) {
Log::debug("sync(): scase %s exist in DB", $scase_uuid);
$db_scases[$scase_uuid] -> sync($scase, $updated);
// @phpstan-ignore-next-line
$db_categories = $db_scases[$scase_uuid] -> categories();
}
else {
Log::debug("sync(): scase %s does not exist in DB, create it", $scase_uuid);
$scase -> save();
$updated = true;
$db_categories = [];
}
foreach($scase_data['cats'] as $category_uuid => $category_data) {
Log::debug("sync(): scase %s / category %s", $scase_uuid, $category_uuid);
$category = Category :: from_json($category_data, $scase);
if (array_key_exists($category_uuid, $db_categories)) {
Log::debug("sync(): scase %s / category %s exists in DB, sync it", $scase_uuid, $category_uuid);
$db_categories[$category_uuid] -> sync($category, $updated);
$db_things = $db_categories[$category_uuid] ->things();
}
else {
Log::debug("sync(): scase %s / category %s does not exists in DB, create it", $scase_uuid, $category_uuid);
$category -> save();
$updated = true;
$db_things = [];
}
foreach($category_data['things'] as $thing_uuid => $thing_data) {
$thing = Thing :: from_json($thing_data, $category);
if (array_key_exists($thing_uuid, $db_things)) {
$db_things[$thing_uuid] -> sync($thing, $updated);
}
else {
$thing -> save();
$updated = true;
}
}
}
}
$data = [
"scases" => [],
"updated" => $updated,
];
foreach (SCase :: list(['username' => $user -> username]) as $uuid => $scase) {
$data["scases"][$uuid] = $scase -> to_json();
}
Tpl::display_ajax_return($data);
}
Url :: add_url_handler("#^sync$#", 'handle_sync', null, true, true, true);
/**
* Support info page
* @param EesyPHP\UrlRequest $request
* @return void
*/
function handle_support_info($request) {
if (isset($_REQUEST['download'])) {
header('Content-Type: text/plain');
header('Content-disposition: attachment; filename="'.date('Ymd-His-').Auth::user()->username.'-aide-support.txt"');
}
Tpl :: display(
isset($_REQUEST['download'])?
'support_info_content.tpl':"support_info.tpl",
"Page d'aide à l'assistance utilisateurs"
);
}
Url :: add_url_handler('|^support/?$|', 'handle_support_info');
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

1
locales/fr_FR.UTF8.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,657 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-09-22 16:25+0000\n"
"PO-Revision-Date: 2024-09-22 18:26+0200\n"
"Last-Translator: Benjamin Renard <brenard@easter-eggs.com>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.2.2\n"
#: src/Auth/API.php:49
msgid "Invalid username or password."
msgstr "Nom d'utilisateur ou mot de passe invalide."
#: src/Auth/API.php:56
msgid "Invalid authentication token."
msgstr "Jeton d'authentification invalide."
#: src/Auth/API.php:58
msgid "Authentication token expired."
msgstr "Jeton d'authentification expiré."
#: src/Auth/API.php:65
msgid "Your account appears to have been deleted."
msgstr "Votre compte semble avoir été supprimé."
#: static/main.js:27
msgid "Confirmation"
msgstr "Confirmation"
#: static/main.js:29
msgid "OK"
msgstr "OK"
#: static/main.js:31
msgid "Cancel"
msgstr "Annuler"
#: static/main.js:86
msgid "You have to enter the name of the suitcase!"
msgstr "Vous devez saisir le nom de la valise !"
#: static/main.js:96 static/main.js:143 static/main.js:200
msgid "A suitcase exist with this name exist in the trash."
msgstr "Une valise portant ce nom existe déjà dans la corbeille."
#: static/main.js:101
msgid "This suitcase already exist!"
msgstr "Cette valise existe déjà !"
#: static/main.js:135
msgid "You have to enter the new name of the suitcase!"
msgstr "Vous devez saisir le nouveau nom de la valise !"
#: static/main.js:149 static/main.js:206
msgid "A suitcase with this name already exist!"
msgstr "Une valise portant ce nom existe déjà !"
#: static/main.js:164
msgid "An error occurred renaming this suitcase."
msgstr "Une erreur est survenue en renommant cette valise."
#: static/main.js:193
msgid "You have to enter the new suitcase name."
msgstr "Vous devez saisir le nom de la nouvelle valise."
#: static/main.js:219
msgid "An error occurred copying the suitcase."
msgstr "Une erreur est survenue en copiant cette valise."
#: static/main.js:240
#, javascript-format
msgid "Reset the %s suitcase"
msgstr "Réinitialiser la valise %s"
#: static/main.js:241
#, javascript-format
msgid "Are-you sure you want to reset the suitcase %s?"
msgstr "Êtes-vous sûre de vouloir réinitialiser la valise %s ?"
#: static/main.js:260
#, javascript-format
msgid "Delete the %s suitcase"
msgstr "Supprimer la valise %s"
#: static/main.js:261
#, javascript-format
msgid "Are-you sure you want to delete the suitcase %s?"
msgstr "Êtes-vous sûre de vouloir supprimer la valise %s ?"
#: static/main.js:278
#, javascript-format
msgid "Restaure the %s suitcase"
msgstr "Restaurer la valise %s"
#: static/main.js:279
#, javascript-format
msgid "Are-you sure you want to restaure the suitcase %s?"
msgstr "Êtes-vous sûre de vouloir restaurer la valise %s ?"
#: static/main.js:311
msgid "You have to enter the category name!"
msgstr "Vous devez saisir le nom de la catégorie !"
#: static/main.js:320 static/main.js:378
msgid "A category with this name already exist in the trash!"
msgstr "Une catégorie portant ce nom existe déjà dans la corbeille !"
#: static/main.js:326
msgid "A category with this name already exist!"
msgstr "Une catégorie portant ce nom existe déjà !"
#: static/main.js:366
msgid "You have to enter the new name of the category!"
msgstr "Vous devez saisir le nouveau nom de la catégorie !"
#: static/main.js:383
msgid "A category with this name already!"
msgstr "Une catégorie portant ce nom existe déjà !"
#: static/main.js:414
#, javascript-format
msgid "Delete the category %s"
msgstr "Supprimer la catégorie %s"
#: static/main.js:415
#, javascript-format
msgid "Are-you sure you want to delete the category %s?"
msgstr "Êtes-vous sûre de vouloir supprimer la catégorie %s ?"
#: static/main.js:433
#, javascript-format
msgid "Restore the category %s"
msgstr "Restaurer la catégorie %s"
#: static/main.js:434
#, javascript-format
msgid "Are-you sure you want to restore the category %s?"
msgstr "Êtes-vous sûre de vouloir restaurer la catégorie %s ?"
#: static/main.js:497
msgid "Tow elements can't have the same name!"
msgstr "Deux éléments ne peuvent pas porter le même nom !"
#: static/main.js:505
#, javascript-format
msgid "The element '%s' already exist!"
msgstr "Un élément portant le nom '%s' existe déjà !"
#: static/main.js:528
msgid "You have to enter at least one element name!"
msgstr "Vous devez saisir au moins un nom d'élément !"
#: static/main.js:561
msgid "Another?"
msgstr "Un autre ?"
#: static/main.js:566
msgid "Nb"
msgstr "Nb"
#: static/main.js:593
msgid "You have to enter the new element name!"
msgstr "Vous devez saisir le nom du nouvel élément !"
#: static/main.js:609
msgid "An element with this name already exist in the trash!"
msgstr "Un élément portant ce nom existe déjà dans la corbeille !"
#: static/main.js:615
msgid "An element with this name already exist!"
msgstr "Un élément portant ce nom existe déjà !"
#: static/main.js:659
#, javascript-format
msgid "Delete the element %s"
msgstr "Supprimer l'élément %s"
#: static/main.js:660
#, javascript-format
msgid "Are-you sure you want to delete the element %s?"
msgstr "Êtes-vous sûre de vouloir supprimer l'élément %s ?"
#: static/main.js:681
#, javascript-format
msgid "Restore the element %s"
msgstr "Restaurer l'élément %s"
#: static/main.js:682
#, javascript-format
msgid "Are-you sure you want to restore the element %s?"
msgstr "Êtes-vous sûre de vouloir restaurer l'élément %s ?"
#: static/main.js:785
msgid "Add an element"
msgstr "Ajouter un élément"
#: static/main.js:823 static/main.js:963
msgid "Trash"
msgstr "Corbeille"
#: static/main.js:836
msgid "The trash is empty."
msgstr "La corbeille est vide."
#: static/main.js:924
msgid "Your suitcases"
msgstr "Vos valises"
#: static/main.js:995
msgid "No suitcase in the trash."
msgstr "Aucune valise dans la corbeille."
#: static/main.js:1050
msgid "Delete all local data"
msgstr "Purger les données locales"
#: static/main.js:1051
msgid "Are-you sure you want to delete all local data (irreversible action)?"
msgstr ""
"Êtes-vous sûre de vouloir supprimer toutes les données locales (action "
"irréversible) ?"
#: static/main.js:1068
msgid "Loading example data"
msgstr "Charger les données d'exemple"
#: static/main.js:1070
msgid ""
"Are-you sure you want to load example data in place of your own local data "
"(irreversible action)?"
msgstr ""
"Êtes-vous sûre de vouloir charger les données d'exemple à la place de vos "
"données locales (action irréversible) ?"
#: static/main.js:1120
msgid "Failed to decode JSON file."
msgstr "Impossible de décoder le fichier JSON."
#: static/main.js:1125
msgid "Import from file"
msgstr "Import depuis un fichier"
#: static/main.js:1127
msgid ""
"Are-you sure you want to overwrite your local data with the data from this "
"file (irreversible action)?"
msgstr ""
"Êtes-vous sûre de vouloir écraser vos données locales par celles issues de "
"ce fichier (action irréversible) ?"
#: static/main.js:1134
msgid "The file has been imported successfully."
msgstr "Le fichier as bien été importé."
#: static/main.js:1141
msgid "An error occurred loading this file. Restoring previous data..."
msgstr ""
"Une erreur est survenue en chargeant ce fichier. Restauration des données "
"précédentes..."
#: static/main.js:1148 static/main.js:1315
msgid "Previous data has been restored successfully."
msgstr "Les données précédentes ont bien été restaurées."
#: static/main.js:1154 static/main.js:1321
msgid "An error occurred restoring previous data."
msgstr "Une erreur est survenue en restaurant les données précédentes."
#: static/main.js:1200
msgid "You have to enter your username and password!"
msgstr "Vous devez saisir votre nom d'utilisateur et votre mot de passe !"
#: static/main.js:1218
msgid "Connected."
msgstr "Connecté."
#: static/main.js:1226
msgid "An error occurred logging in. Please try again later."
msgstr ""
"Une erreur est survenue durant la connexion. Merci de réessayer "
"ultérieurement."
#: static/main.js:1247
msgid "Logout"
msgstr "Déconnexion"
#: static/main.js:1248
msgid "Do you really want to log out?"
msgstr "Voulez-vous vraiment vous déconnecter ?"
#: static/main.js:1259
msgid "Logged out"
msgstr "Déconnecté"
#: static/main.js:1262 static/main.js:1270
msgid "An error occurred logging you out. Please try again later."
msgstr ""
"Une erreur est survenue durant la déconnexion. Merci de réessayer "
"ultérieurement."
#: static/main.js:1303
msgid "Data synchronized."
msgstr "Données synchronisées."
#: static/main.js:1308
msgid ""
"An error occurred loading data from the server. Restoring previous data..."
msgstr ""
"Une erreur est survenue en chargeant les données depuis le serveur. "
"Restauration des données précédentes..."
#: static/main.js:1331 static/main.js:1353
msgid "An error occurred synchronizing your data. Please try again later."
msgstr ""
"Une erreur est survenue durant la synchronisation de vos données. Merci de "
"réessayer ultérieurement."
#: static/main.js:1345
msgid "Your session appears to have expired, please re-authenticate."
msgstr "Votre session semble avoir expirée, merci de vous réidentifier."
#: static/main.js:1372
msgid "You must sign in before you can sync your data."
msgstr "Vous devez vous connecter avant de synchroniser vos données."
#: static/main.js:1386
msgid "Synchronize your suitcases from server"
msgstr "Synchronisation de vos valises avec les serveurs"
#: static/main.js:1387
msgid "Are-you sure you want to synchronize your suitcases from server?"
msgstr "Êtes-vous sûre de vouloir synchroniser vos valises avec le serveur ?"
#: static/main.js:1450
msgid "An example of a suitcase?"
msgstr "Un exemple de valise ?"
#: static/main.js:1451
msgid "Would you like to load a sample suitcase to get started?"
msgstr "Voulez-vous chargeant un exemple de valise pour commencer ?"
#: static/main.js:1468
msgid ""
"Your internet browser does not support local data storage. Therefore, "
"unfortunately you cannot use this application."
msgstr ""
"Votre navigateur internet ne semble pas supporter le stockage de données "
"locales. Malheureusement, vous ne pouvez donc pas utiliser cette application."
#: static/main.js:1557
msgid "Error loading local data"
msgstr "Une erreur est survenue en chargeant les données locales"
#: static/main.js:1558
msgid "An error occurred while loading local data. Should we purge it?"
msgstr ""
"Une erreur est survenue en chargeant vos données locales. Devons-nous les "
"purger ?"
#: static/mysc_objects.js:10
msgid "White paper"
msgstr "Papier blanc"
#: static/mysc_objects.js:11
msgid "Pen"
msgstr "Stylo"
#: static/mysc_objects.js:12
msgid "ID card/passport"
msgstr "Carte d'identité / passeport"
#: static/mysc_objects.js:18
msgid "Watch"
msgstr "Montre"
#: static/mysc_objects.js:19
msgid "Watch charger"
msgstr "Chargeur montre"
#: static/mysc_objects.js:20
msgid "Laptop"
msgstr "PC portable"
#: static/mysc_objects.js:21
msgid "Laptop charger"
msgstr "Chargeur PC portable"
#: static/mysc_objects.js:294
#, javascript-format
msgid "An error occurred while loading the %s suitcase from the cache."
msgstr "Une erreur est survenue en chargeant la valise %s depuis le cache."
#: static/mysc_objects.js:417
msgid "An error occurred while loading the category list from the cache."
msgstr ""
"Une erreur est survenue en chargeant la liste de catégories depuis le cache."
#: static/mysc_objects.js:565
#, javascript-format
msgid "An error occurred while loading the %s category from the cache."
msgstr "Une erreur est survenue en chargeant la catégorie %s depuis le cache."
#: static/mysc_objects.js:636
msgid "Error loading your login information. Please log in again."
msgstr ""
"Erreur en chargeant vos données d'authentification. Merci de vous "
"réidentifier."
#: templates/support_info.tpl:4
msgid "Helpdesk page"
msgstr "Page d'assistance aux utilisateurs"
#: templates/support_info.tpl:5
msgid ""
"Upon request, please download and forward the following information to the "
"support service:"
msgstr ""
"À leur demande, merci de télécharger et transmettre les informations ci-"
"dessous au service support :"
#: templates/support_info.tpl:9
msgid "Download"
msgstr "Télécharger"
#: templates/support_info_content.tpl:1
msgid "Application URL:"
msgstr "URL de l'application :"
#: templates/support_info_content.tpl:2
msgid "Current page URL:"
msgstr "URL de la page courante :"
#: templates/support_info_content.tpl:4
msgid "Connected user:"
msgstr "Utilisateur connecté :"
#: templates/support_info_content.tpl:6
msgid "Extra user information:"
msgstr "Informations supplémentaires de l'utilisateur :"
#: templates/index.tpl:39 templates/index.tpl:91
msgid "Add a suitcase"
msgstr "Ajouter une valise"
#: templates/index.tpl:40
msgid "Show trash"
msgstr "Voir la corbeille"
#: templates/index.tpl:41
msgid "Suitcases list"
msgstr "Liste des valises"
#: templates/index.tpl:43
msgid "Manage the suitcase"
msgstr "Gérer les valises"
#: templates/index.tpl:45 templates/index.tpl:157
msgid "Add a category"
msgstr "Ajouter une catégorie"
#: templates/index.tpl:47 templates/index.tpl:135
msgid "Rename the suitcase"
msgstr "Renommer la valise"
#: templates/index.tpl:48 templates/index.tpl:113
msgid "Copy the suitcase"
msgstr "Copier la valise"
#: templates/index.tpl:49
msgid "Reset the suitcase"
msgstr "Réinitialiser la valise"
#: templates/index.tpl:50
msgid "Delete the suitcase"
msgstr "Supprimer la valise"
#: templates/index.tpl:52
msgid "Show suitcase's trash"
msgstr "Voir la corbeille de la valise"
#: templates/index.tpl:56
msgid "Manage data"
msgstr "Gérer les données"
#: templates/index.tpl:58
msgid "Backup your data"
msgstr "Sauvegarder vos données"
#: templates/index.tpl:59
msgid "Restaure your data"
msgstr "Restaurer vos données"
#: templates/index.tpl:61
msgid "Synchronize local data"
msgstr "Synchroniser les données locales"
#: templates/index.tpl:63
msgid "Purge local data"
msgstr "Purger les données locales"
#: templates/index.tpl:64
msgid "Load example data"
msgstr "Charger les données d'exemple"
#: templates/index.tpl:74
msgid "Login"
msgstr "Connexion"
#: templates/index.tpl:96
msgid "Suitcase name"
msgstr "Nom de la valise"
#: templates/index.tpl:102 templates/index.tpl:168 templates/index.tpl:218
msgid "Add"
msgstr "Ajouter"
#: templates/index.tpl:118
msgid "Name of the new suitcase"
msgstr "Nom de la nouvelle valise"
#: templates/index.tpl:124
msgid "Copy"
msgstr "Copier"
#: templates/index.tpl:140
msgid "New suitcase name"
msgstr "Nouveau nom de la valise"
#: templates/index.tpl:146 templates/index.tpl:190
msgid "Rename"
msgstr "Renommer"
#: templates/index.tpl:162
msgid "Category name"
msgstr "Nom de la catégorie"
#: templates/index.tpl:179
msgid "Rename the category"
msgstr "Renommer la catégorie"
#: templates/index.tpl:184
msgid "New category's name"
msgstr "Nouveau nom de la catégorie"
#: templates/index.tpl:207 templates/index.tpl:234
msgid "Element name"
msgstr "Nom de l'élément"
#: templates/index.tpl:229
msgid "Edit the element"
msgstr "Modifier l'élément"
#: templates/index.tpl:241
msgid "Modify"
msgstr "Modifier"
#: templates/index.tpl:252 templates/index.tpl:257
msgid "Loading..."
msgstr "Chargement..."
#: templates/index.tpl:271
msgid "Connection"
msgstr "Connexion"
#: templates/index.tpl:276
msgid "Username"
msgstr "Nom d'utilisateur"
#: templates/index.tpl:277
msgid "Password"
msgstr "Mot de passe"
#: templates/index.tpl:283 templates/index.tpl:326
msgid "Connect"
msgstr "Connexion"
#: templates/index.tpl:295
msgid "Welcome"
msgstr "Bienvenu"
#: templates/index.tpl:298
msgid ""
"<p>\n"
" This application allows you to manage lists of things not to "
"forget to pack in your\n"
" suitcase before your departure:\n"
" <ul>\n"
" <li>\n"
" To get started, create a suitcase, add the categories of "
"things you will need to\n"
" pack, and add all these things to it;\n"
" </li>\n"
" <li>\n"
" Before your departure, you can then gradually check off all "
"the things you have\n"
" already gathered and ensure you don't forget anything!\n"
" </li>\n"
" </ul>\n"
" </p>\n"
" <p>\n"
" <strong>Note:</strong> This application has been designed to work "
"completely locally.\n"
" The data manipulated (your suitcases, etc.) is stored only in your "
"internet browser.\n"
" It is also possible to export/import this information in JSON "
"format.<br/>\n"
" However, to facilitate the management of your suitcases from "
"multiple devices, it's\n"
" possible to synchronize this information on the server. For this, "
"you need an account,\n"
" and at this time, registrations are not open.\n"
" </p>\n"
" <p>\n"
" If you have an account, you can log in by clicking the <em>Login</"
"em> button below.\n"
" If not, click the <em>Anonymous usage</em> button to start using "
"the application.\n"
" </p>"
msgstr ""
"<p>\n"
" Cette application vous permet de gérer des listes de choses à ne pas "
"oublier de glisser dans votre valise avant votre départ :\n"
" <ul>\n"
" <li>Pour commencer, créer une valise, ajouter les catégories de choses "
"que vous aurez à y glisser et ajouter y toutes ces choses.</li>\n"
" <li>Avant votre départ, vous pourrez alors cocher petit à petit toutes "
"les choses que vous aurez déjà réunies et vous asurez de ne rien oublier !</"
"li>\n"
" </ul>\n"
"</p>\n"
"<p>\n"
" <strong>Note :</strong> Cette application a été conçue pour pouvoir "
"fonctionner complètement localement. Les données manipulées (vos "
"valises, ...) sont stockés\n"
" uniquement dans votre navigateur internet. Il est par ailleurs possible "
"d'exporter/importer ces informations au format JSON.<br/>\n"
" Cependant, pour faciliter la gestion de vos valises depuis plusieurs "
"appareils, il est possible de synchroniser ces informations sur le serveur. "
"Pour cela,\n"
" il vous faut un compte et à ce jour, les inscriptions ne sont pas "
"ouvertes.\n"
"</p>\n"
"<p>\n"
" Si vous disposez d'un compte, vous pouvez vous connecter en cliquant sur "
"le bouton <em>Connexion</em> ci-dessous.\n"
" À défaut, cliquer sur le bouton <em>Utilisation annonyme</em> pour "
"commencer à utiliser l'application.\n"
"</p>"
#: templates/index.tpl:327
msgid "Anonymous usage"
msgstr "Utilisation annonyme"

597
locales/messages.pot Normal file
View file

@ -0,0 +1,597 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2024-09-22 16:25+0000\n"
"PO-Revision-Date: 2024-09-22 16:25+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/Auth/API.php:49
msgid "Invalid username or password."
msgstr ""
#: src/Auth/API.php:56
msgid "Invalid authentication token."
msgstr ""
#: src/Auth/API.php:58
msgid "Authentication token expired."
msgstr ""
#: src/Auth/API.php:65
msgid "Your account appears to have been deleted."
msgstr ""
#: static/main.js:27
msgid "Confirmation"
msgstr ""
#: static/main.js:29
msgid "OK"
msgstr ""
#: static/main.js:31
msgid "Cancel"
msgstr ""
#: static/main.js:86
msgid "You have to enter the name of the suitcase!"
msgstr ""
#: static/main.js:96 static/main.js:143 static/main.js:200
msgid "A suitcase exist with this name exist in the trash."
msgstr ""
#: static/main.js:101
msgid "This suitcase already exist!"
msgstr ""
#: static/main.js:135
msgid "You have to enter the new name of the suitcase!"
msgstr ""
#: static/main.js:149 static/main.js:206
msgid "A suitcase with this name already exist!"
msgstr ""
#: static/main.js:164
msgid "An error occurred renaming this suitcase."
msgstr ""
#: static/main.js:193
msgid "You have to enter the new suitcase name."
msgstr ""
#: static/main.js:219
msgid "An error occurred copying the suitcase."
msgstr ""
#: static/main.js:240
#, javascript-format
msgid "Reset the %s suitcase"
msgstr ""
#: static/main.js:241
#, javascript-format
msgid "Are-you sure you want to reset the suitcase %s?"
msgstr ""
#: static/main.js:260
#, javascript-format
msgid "Delete the %s suitcase"
msgstr ""
#: static/main.js:261
#, javascript-format
msgid "Are-you sure you want to delete the suitcase %s?"
msgstr ""
#: static/main.js:278
#, javascript-format
msgid "Restaure the %s suitcase"
msgstr ""
#: static/main.js:279
#, javascript-format
msgid "Are-you sure you want to restaure the suitcase %s?"
msgstr ""
#: static/main.js:311
msgid "You have to enter the category name!"
msgstr ""
#: static/main.js:320 static/main.js:378
msgid "A category with this name already exist in the trash!"
msgstr ""
#: static/main.js:326
msgid "A category with this name already exist!"
msgstr ""
#: static/main.js:366
msgid "You have to enter the new name of the category!"
msgstr ""
#: static/main.js:383
msgid "A category with this name already!"
msgstr ""
#: static/main.js:414
#, javascript-format
msgid "Delete the category %s"
msgstr ""
#: static/main.js:415
#, javascript-format
msgid "Are-you sure you want to delete the category %s?"
msgstr ""
#: static/main.js:433
#, javascript-format
msgid "Restore the category %s"
msgstr ""
#: static/main.js:434
#, javascript-format
msgid "Are-you sure you want to restore the category %s?"
msgstr ""
#: static/main.js:497
msgid "Tow elements can't have the same name!"
msgstr ""
#: static/main.js:505
#, javascript-format
msgid "The element '%s' already exist!"
msgstr ""
#: static/main.js:528
msgid "You have to enter at least one element name!"
msgstr ""
#: static/main.js:561
msgid "Another?"
msgstr ""
#: static/main.js:566
msgid "Nb"
msgstr ""
#: static/main.js:593
msgid "You have to enter the new element name!"
msgstr ""
#: static/main.js:609
msgid "An element with this name already exist in the trash!"
msgstr ""
#: static/main.js:615
msgid "An element with this name already exist!"
msgstr ""
#: static/main.js:659
#, javascript-format
msgid "Delete the element %s"
msgstr ""
#: static/main.js:660
#, javascript-format
msgid "Are-you sure you want to delete the element %s?"
msgstr ""
#: static/main.js:681
#, javascript-format
msgid "Restore the element %s"
msgstr ""
#: static/main.js:682
#, javascript-format
msgid "Are-you sure you want to restore the element %s?"
msgstr ""
#: static/main.js:785
msgid "Add an element"
msgstr ""
#: static/main.js:823 static/main.js:963
msgid "Trash"
msgstr ""
#: static/main.js:836
msgid "The trash is empty."
msgstr ""
#: static/main.js:924
msgid "Your suitcases"
msgstr ""
#: static/main.js:995
msgid "No suitcase in the trash."
msgstr ""
#: static/main.js:1050
msgid "Delete all local data"
msgstr ""
#: static/main.js:1051
msgid "Are-you sure you want to delete all local data (irreversible action)?"
msgstr ""
#: static/main.js:1068
msgid "Loading example data"
msgstr ""
#: static/main.js:1070
msgid ""
"Are-you sure you want to load example data in place of your own local data "
"(irreversible action)?"
msgstr ""
#: static/main.js:1120
msgid "Failed to decode JSON file."
msgstr ""
#: static/main.js:1125
msgid "Import from file"
msgstr ""
#: static/main.js:1127
msgid ""
"Are-you sure you want to overwrite your local data with the data from this "
"file (irreversible action)?"
msgstr ""
#: static/main.js:1134
msgid "The file has been imported successfully."
msgstr ""
#: static/main.js:1141
msgid "An error occurred loading this file. Restoring previous data..."
msgstr ""
#: static/main.js:1148 static/main.js:1315
msgid "Previous data has been restored successfully."
msgstr ""
#: static/main.js:1154 static/main.js:1321
msgid "An error occurred restoring previous data."
msgstr ""
#: static/main.js:1200
msgid "You have to enter your username and password!"
msgstr ""
#: static/main.js:1218
msgid "Connected."
msgstr ""
#: static/main.js:1226
msgid "An error occurred logging in. Please try again later."
msgstr ""
#: static/main.js:1247
msgid "Logout"
msgstr ""
#: static/main.js:1248
msgid "Do you really want to log out?"
msgstr ""
#: static/main.js:1259
msgid "Logged out"
msgstr ""
#: static/main.js:1262 static/main.js:1270
msgid "An error occurred logging you out. Please try again later."
msgstr ""
#: static/main.js:1303
msgid "Data synchronized."
msgstr ""
#: static/main.js:1308
msgid ""
"An error occurred loading data from the server. Restoring previous data..."
msgstr ""
#: static/main.js:1331 static/main.js:1353
msgid "An error occurred synchronizing your data. Please try again later."
msgstr ""
#: static/main.js:1345
msgid "Your session appears to have expired, please re-authenticate."
msgstr ""
#: static/main.js:1372
msgid "You must sign in before you can sync your data."
msgstr ""
#: static/main.js:1386
msgid "Synchronize your suitcases from server"
msgstr ""
#: static/main.js:1387
msgid "Are-you sure you want to synchronize your suitcases from server?"
msgstr ""
#: static/main.js:1450
msgid "An example of a suitcase?"
msgstr ""
#: static/main.js:1451
msgid "Would you like to load a sample suitcase to get started?"
msgstr ""
#: static/main.js:1468
msgid ""
"Your internet browser does not support local data storage. Therefore, "
"unfortunately you cannot use this application."
msgstr ""
#: static/main.js:1557
msgid "Error loading local data"
msgstr ""
#: static/main.js:1558
msgid "An error occurred while loading local data. Should we purge it?"
msgstr ""
#: static/mysc_objects.js:10
msgid "White paper"
msgstr ""
#: static/mysc_objects.js:11
msgid "Pen"
msgstr ""
#: static/mysc_objects.js:12
msgid "ID card/passport"
msgstr ""
#: static/mysc_objects.js:18
msgid "Watch"
msgstr ""
#: static/mysc_objects.js:19
msgid "Watch charger"
msgstr ""
#: static/mysc_objects.js:20
msgid "Laptop"
msgstr ""
#: static/mysc_objects.js:21
msgid "Laptop charger"
msgstr ""
#: static/mysc_objects.js:294
#, javascript-format
msgid "An error occurred while loading the %s suitcase from the cache."
msgstr ""
#: static/mysc_objects.js:417
msgid "An error occurred while loading the category list from the cache."
msgstr ""
#: static/mysc_objects.js:565
#, javascript-format
msgid "An error occurred while loading the %s category from the cache."
msgstr ""
#: static/mysc_objects.js:636
msgid "Error loading your login information. Please log in again."
msgstr ""
#: templates/support_info.tpl:4
msgid "Helpdesk page"
msgstr ""
#: templates/support_info.tpl:5
msgid ""
"Upon request, please download and forward the following information to the "
"support service:"
msgstr ""
#: templates/support_info.tpl:9
msgid "Download"
msgstr ""
#: templates/support_info_content.tpl:1
msgid "Application URL:"
msgstr ""
#: templates/support_info_content.tpl:2
msgid "Current page URL:"
msgstr ""
#: templates/support_info_content.tpl:4
msgid "Connected user:"
msgstr ""
#: templates/support_info_content.tpl:6
msgid "Extra user information:"
msgstr ""
#: templates/index.tpl:39 templates/index.tpl:91
msgid "Add a suitcase"
msgstr ""
#: templates/index.tpl:40
msgid "Show trash"
msgstr ""
#: templates/index.tpl:41
msgid "Suitcases list"
msgstr ""
#: templates/index.tpl:43
msgid "Manage the suitcase"
msgstr ""
#: templates/index.tpl:45 templates/index.tpl:157
msgid "Add a category"
msgstr ""
#: templates/index.tpl:47 templates/index.tpl:135
msgid "Rename the suitcase"
msgstr ""
#: templates/index.tpl:48 templates/index.tpl:113
msgid "Copy the suitcase"
msgstr ""
#: templates/index.tpl:49
msgid "Reset the suitcase"
msgstr ""
#: templates/index.tpl:50
msgid "Delete the suitcase"
msgstr ""
#: templates/index.tpl:52
msgid "Show suitcase's trash"
msgstr ""
#: templates/index.tpl:56
msgid "Manage data"
msgstr ""
#: templates/index.tpl:58
msgid "Backup your data"
msgstr ""
#: templates/index.tpl:59
msgid "Restaure your data"
msgstr ""
#: templates/index.tpl:61
msgid "Synchronize local data"
msgstr ""
#: templates/index.tpl:63
msgid "Purge local data"
msgstr ""
#: templates/index.tpl:64
msgid "Load example data"
msgstr ""
#: templates/index.tpl:74
msgid "Login"
msgstr ""
#: templates/index.tpl:96
msgid "Suitcase name"
msgstr ""
#: templates/index.tpl:102 templates/index.tpl:168 templates/index.tpl:218
msgid "Add"
msgstr ""
#: templates/index.tpl:118
msgid "Name of the new suitcase"
msgstr ""
#: templates/index.tpl:124
msgid "Copy"
msgstr ""
#: templates/index.tpl:140
msgid "New suitcase name"
msgstr ""
#: templates/index.tpl:146 templates/index.tpl:190
msgid "Rename"
msgstr ""
#: templates/index.tpl:162
msgid "Category name"
msgstr ""
#: templates/index.tpl:179
msgid "Rename the category"
msgstr ""
#: templates/index.tpl:184
msgid "New category's name"
msgstr ""
#: templates/index.tpl:207 templates/index.tpl:234
msgid "Element name"
msgstr ""
#: templates/index.tpl:229
msgid "Edit the element"
msgstr ""
#: templates/index.tpl:241
msgid "Modify"
msgstr ""
#: templates/index.tpl:252 templates/index.tpl:257
msgid "Loading..."
msgstr ""
#: templates/index.tpl:271
msgid "Connection"
msgstr ""
#: templates/index.tpl:276
msgid "Username"
msgstr ""
#: templates/index.tpl:277
msgid "Password"
msgstr ""
#: templates/index.tpl:283 templates/index.tpl:326
msgid "Connect"
msgstr ""
#: templates/index.tpl:295
msgid "Welcome"
msgstr ""
#: templates/index.tpl:298
msgid ""
"<p>\n"
" This application allows you to manage lists of things not to "
"forget to pack in your\n"
" suitcase before your departure:\n"
" <ul>\n"
" <li>\n"
" To get started, create a suitcase, add the categories of "
"things you will need to\n"
" pack, and add all these things to it;\n"
" </li>\n"
" <li>\n"
" Before your departure, you can then gradually check off all "
"the things you have\n"
" already gathered and ensure you don't forget anything!\n"
" </li>\n"
" </ul>\n"
" </p>\n"
" <p>\n"
" <strong>Note:</strong> This application has been designed to work "
"completely locally.\n"
" The data manipulated (your suitcases, etc.) is stored only in your "
"internet browser.\n"
" It is also possible to export/import this information in JSON "
"format.<br/>\n"
" However, to facilitate the management of your suitcases from "
"multiple devices, it's\n"
" possible to synchronize this information on the server. For this, "
"you need an account,\n"
" and at this time, registrations are not open.\n"
" </p>\n"
" <p>\n"
" If you have an account, you can log in by clicking the <em>Login</"
"em> button below.\n"
" If not, click the <em>Anonymous usage</em> button to start using "
"the application.\n"
" </p>"
msgstr ""
#: templates/index.tpl:327
msgid "Anonymous usage"
msgstr ""

14
phpstan.neon Normal file
View file

@ -0,0 +1,14 @@
parameters:
level: 5
paths:
- bin
- includes
- src
- public_html
universalObjectCratesClasses:
- EesyPHP\HookEvent
- EesyPHP\UrlRequest
- EesyPHP\Auth\User
- EesyPHP\Db\DbObject
bootstrapFiles:
- includes/core.php

4
setup.cfg Normal file
View file

@ -0,0 +1,4 @@
[codespell]
ignore-words-list=fro,hass
exclude-file=.codespell-exclusions
quiet-level=2

128
src/Auth/API.php Normal file
View file

@ -0,0 +1,128 @@
<?php
namespace MySC\Auth;
use EesyPHP\Auth;
use EesyPHP\Auth\Method;
use EesyPHP\Log;
use EesyPHP\Url;
use EesyPHP\Tpl;
use MySC\Db\AuthToken;
use function EesyPHP\vardump;
class API extends Method {
protected static $initialized = false;
/**
* Initialize
* @return boolean
*/
public static function init() {
if (php_sapi_name() != "cli" && !self :: $initialized) {
Url :: add_url_handler(
'#^login$#',
['MySC\\Auth\\API', 'handle_login'],
null,
false,
true,
true
);
self :: $initialized = true;
}
return true;
}
/**
* Log user
* @param bool $force Force user authentication
* @return \EesyPHP\Auth\User|null
*/
public static function login($force=false) {
$user = null;
if (isset($_REQUEST['username']) && isset($_REQUEST['password'])) {
Log::debug("API::login(): Try to authenticate user by username & password");
$user = Auth :: authenticate($_REQUEST['username'], $_REQUEST['password']);
if (!$user)
Tpl :: add_error(_('Invalid username or password.'));
}
elseif (isset($_REQUEST['username']) && isset($_REQUEST['token'])) {
Log::debug("API::login(): Try to authenticate user by token");
$auth_token = AuthToken :: get($_REQUEST['token']);
Log::debug("API::login(): auth token = %s", vardump($auth_token));
if (!$auth_token || $auth_token->username != $_REQUEST['username'])
Tpl :: add_error(_('Invalid authentication token.'));
elseif ($auth_token->expired()) {
Tpl :: add_error(_('Authentication token expired.'));
$auth_token->delete();
}
else {
Log::debug("API::login(): auth token valid, retrieve corresponding user");
$user = Auth::get_user($auth_token->username);
if (!$user) {
Tpl :: add_error(_('Your account appears to have been deleted.'));
$auth_token->delete();
}
Log::debug("login(): authenticated as %s via token", $user);
}
}
return $user;
}
/**
* The login form view
* @param \EesyPHP\UrlRequest $request
* @return void
*/
public static function handle_login($request) {
$user = Auth :: login(false, '\\mysc\\auth\\api');
if ($user)
Tpl :: display_ajax_return([
"success" => true,
"token" => AuthToken :: create($user) -> token,
"username" => $user -> username,
"name" => $user->name,
"mail" => $user->mail,
]);
Tpl :: display_ajax_return([
"success" => false,
]);
}
/**
* The logout method
* @return void
*/
public static function logout() {
Log::debug("API::logout()");
if (isset($_REQUEST['token'])) {
$auth_token = AuthToken :: get($_REQUEST['token']);
Log::debug("API::logout(): auth token = %s", vardump($auth_token));
if (!$auth_token) {
Log::debug("API::logout(): unknown auth token");
}
else if (isset($_REQUEST['username']) && $auth_token->username != $_REQUEST['username']) {
Log::warning(
"API::logout(): bad auth token owner ('%s' vs '%s')",
$auth_token->username,
$_REQUEST['username']
);
}
else if ($auth_token->delete()) {
Log::debug("API::logout(): auth token deleted");
}
else {
Log::error("API::logout(): error deleting the auth token %s", $_REQUEST['token']);
}
}
else {
Log::warning("API::logout(): no token provided by the request");
}
Tpl :: display_ajax_return([
"success" => true,
]);
}
}

55
src/Db/AuthToken.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace MySC\Db;
use EesyPHP\App;
use EesyPHP\Auth;
use EesyPHP\Db\AttrStr;
use EesyPHP\Db\AttrTimestamp;
use EesyPHP\Db\DbObject;
/**
* MySC database object
* @property string|null $token
* @property string|null $username
* @property \DateTime|null $creation_date
* @property \DateTime|null $expiration_date
* @property boolean $removed
*/
class AuthToken extends DbObject {
protected const TABLE = 'auth_tokens';
protected const PRIMARY_KEYS = ['token'];
protected const DEFAULT_ORDER = 'creation_date';
protected const DEFAULT_ORDER_DIRECTION = 'DESC';
protected static function get_schema() {
return [
'token' => new AttrStr(['required' => true, 'default' => '\\EesyPHP\\generate_uuid']),
'username' => new AttrStr(['required' => true]),
'creation_date' => new AttrTimestamp(['default' => 'time']),
'expiration_date' => new AttrTimestamp(
['default' => ['\\MySC\\Db\\AuthToken', 'generate_expiration_date']]
),
];
}
public static function generate_expiration_date() {
return time() + App::get('auth.auth_token.expiration_delay', null, 'int');
}
public static function create($user=null) {
$user = $user ? $user : Auth::user();
$token = new AuthToken();
$token -> username = $user->username;
$token->save();
return $token;
}
public function user() {
return User::get($this -> username);
}
public function expired() {
return $this -> expiration_date -> getTimestamp() <= time();
}
}

56
src/Db/Category.php Normal file
View file

@ -0,0 +1,56 @@
<?php
namespace MySC\Db;
use EesyPHP\Db\AttrStr;
/**
* SCase category database object
* @property string $uuid
* @property string $scase_uuid
* @property string $name
* @property string $color
* @property \DateTime|null $last_change
* @property boolean $removed
*/
class Category extends DbObject {
protected const TABLE = "categories";
protected const DEFAULT_ORDER = "name";
protected const DEFAULT_ORDER_DIRECTION = "ASC";
protected const POSSIBLE_ORDERS = ["name", "last_change"];
protected const UPDATABLE_FIELDS = ["name", "color"];
protected static function get_schema() {
return array_merge(
parent :: get_schema(),
[
"scase_uuid" => new AttrStr(["required" => true]),
"name" => new AttrStr(["required" => true]),
"color" => new AttrStr(["required" => true]),
]
);
}
public function to_json() {
$data = parent :: to_json();
$data["things"] = [];
foreach($this -> things() as $uuid => $thing)
$data["things"][$uuid] = $thing -> to_json();
return $data;
}
public static function from_json($data, $scase) {
$obj = parent :: _from_json($data);
$obj -> scase_uuid = $scase -> uuid;
return $obj;
}
public function things() {
return Thing :: list(["category_uuid" => $this -> uuid]);
}
public function delete_all_things() {
return Thing :: deleteAll(["category_uuid" => $this -> uuid]);
}
}

107
src/Db/DbObject.php Normal file
View file

@ -0,0 +1,107 @@
<?php
namespace MySC\Db;
use EesyPHP\Date;
use EesyPHP\Log;
use EesyPHP\Db\AttrBool;
use EesyPHP\Db\AttrStr;
use EesyPHP\Db\AttrTimestamp;
use function EesyPHP\vardump;
/**
* MySC database object
* @property string $uuid
* @property boolean $removed
* @property \DateTime|null $last_change
*/
class DbObject extends \EesyPHP\Db\DbObject {
protected const PRIMARY_KEYS = ['uuid'];
protected const UPDATABLE_FIELDS = [];
protected static function get_schema() {
return [
'uuid' => new AttrStr(['required' => true]),
'last_change' => new AttrTimestamp(['default' => 'time']),
'removed' => new AttrBool(['default' => false]),
];
}
public function to_json() {
$data = [
'uuid' => $this -> uuid,
'lastChange' => intval($this -> last_change -> format('Uv')),
'removed' => boolval($this -> removed),
];
foreach(static :: UPDATABLE_FIELDS as $field)
$data[$field] = $this -> $field;
return $data;
}
public static function _from_json($data) {
Log::debug(
"from_json(): input=%s",
vardump($data),
);
$class = get_called_class();
$obj = new $class();
$obj -> apply([
'uuid' => $data['uuid'],
'last_change' => Date :: from_timestamp($data['lastChange'] / 1000),
'removed' => $data['removed'] == "true",
]);
foreach(static :: UPDATABLE_FIELDS as $field)
$obj -> $field = $data[$field];
Log::debug(
"%s::from_json(): result=%s",
$obj,
vardump($obj->to_json()),
);
return $obj;
}
public function sync(&$other, &$updated) {
if ($this -> last_change >= $other -> last_change) {
Log::debug(
"%s::sync(): keep current (%s >= %s)",
$this,
Date::format($this->last_change),
Date::format($other->last_change),
);
return true;
}
Log::debug(
"%s::sync(): update from other (%s < %s) : %s",
$this,
Date::format($this->last_change),
Date::format($other->last_change),
vardump($other->to_json()),
);
$this -> apply([
'last_change' => $other -> last_change,
'removed' => $other -> removed,
]);
foreach(static :: UPDATABLE_FIELDS as $field)
$this -> $field = $other -> $field;
$updated = true;
return $this -> save();
}
/**
* List objects
* @param array<string,mixed> $where Where clauses as associative array of field name and value
* @return array<string,DbObject>|false
*/
public static function list($where=null) {
$objs = [];
foreach(parent :: list($where) as $obj)
$objs[$obj -> uuid] = $obj;
return $objs;
}
public function __toString() {
return get_called_class()."<".$this -> uuid.">";
}
}

81
src/Db/SCase.php Normal file
View file

@ -0,0 +1,81 @@
<?php
namespace MySC\Db;
use EesyPHP\Db\AttrStr;
use EesyPHP\Log;
/**
* SCase database object
* @property string $uuid
* @property string $name
* @property \DateTime|null $last_change
* @property boolean $removed
*/
class SCase extends DbObject {
protected const TABLE = 'scases';
protected const DEFAULT_ORDER = 'name';
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
protected const POSSIBLE_ORDERS = ['name', 'last_change'];
protected const UPDATABLE_FIELDS = ['name'];
protected static function get_schema() {
return array_merge(
parent :: get_schema(),
[
'username' => new AttrStr(['required' => true]),
'name' => new AttrStr(['required' => true]),
]
);
}
public function to_json() {
$data = parent :: to_json();
$data["cats"] = [];
foreach($this -> categories() as $uuid => $cat)
$data["cats"][$uuid] = $cat -> to_json();
return $data;
}
public static function from_json($data, $user) {
$obj = parent :: _from_json($data);
$obj -> username = $user -> username;
return $obj;
}
/**
* Get scase's categories
* @return array<Category>|false
*/
public function categories() {
return Category :: list(['scase_uuid' => $this -> uuid]);
}
public function delete() {
foreach($this -> categories() as $category) {
if (!$category->delete_all_things()) {
Log::error(
"Db: error occurred deleting things of category '%s' (%s)",
$category->name, $category->uuid
);
return false;
}
if (!$category->delete()) {
Log::error(
"Db: error occurred deleting category '%s' (%s)",
$category->name, $category->uuid
);
return false;
}
}
if (!parent::delete()) {
Log::error(
"Db: error occurred deleting scases '%s' (%s)",
$this->name, $this->uuid
);
return false;
}
return true;
}
}

44
src/Db/Thing.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace MySC\Db;
use EesyPHP\Db\AttrBool;
use EesyPHP\Db\AttrInt;
use EesyPHP\Db\AttrStr;
/**
* Thing database object
* @property string $uuid
* @property string $category_uuid
* @property string $label
* @property int $nb
* @property boolean $checked
* @property \DateTime|null $last_change
* @property boolean $removed
*/
class Thing extends DbObject {
protected const TABLE = 'things';
protected const DEFAULT_ORDER = 'label';
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
protected const POSSIBLE_ORDERS = ['label', 'last_change'];
protected const UPDATABLE_FIELDS = ['label', 'nb', 'checked'];
protected static function get_schema() {
return array_merge(
parent :: get_schema(),
[
'category_uuid' => new AttrStr(['required' => true]),
'label' => new AttrStr(['required' => true]),
'nb' => new AttrInt(['required' => true, 'default' => 1]),
'checked' => new AttrBool(['required' => false, 'default' => false]),
]
);
}
public static function from_json($data, $category) {
$obj = parent :: _from_json($data);
$obj -> category_uuid = $category -> uuid;
return $obj;
}
}

39
src/Db/User.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace MySC\Db;
use EesyPHP\Db\AttrStr;
use EesyPHP\Db\DbObject;
/**
* MySC database object
* @property string|null $username
* @property string|null $name
* @property string|null $password
* @property string|null $mail
* @property boolean $removed
*/
class User extends DbObject {
protected const TABLE = 'users';
protected const PRIMARY_KEYS = ['username'];
protected const DEFAULT_ORDER = 'name';
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
protected static function get_schema() {
return [
'username' => new AttrStr(['required' => true]),
'name' => new AttrStr(['required' => true]),
'password' => new AttrStr(['required' => true]),
'mail' => new AttrStr(),
];
}
/**
* Get user's scases
* @return bool|SCase[]
*/
public function scases() {
return SCase :: list(['username' => $this -> username]);
}
}

View file

@ -1,8 +1,10 @@
body{ body {
margin-top: 4em; margin-top: 4em;
} }
div.panel-heading, li.list-group-item, a { div.panel-heading,
li.list-group-item,
a {
cursor: pointer; cursor: pointer;
} }
@ -19,7 +21,7 @@ div.panel-heading, li.list-group-item, a {
} }
.checkable:before { .checkable:before {
content: '\2713'; content: "\2713";
margin-right: 0.2em; margin-right: 0.2em;
font-style: italic; font-style: italic;
color: #999; color: #999;
@ -43,12 +45,14 @@ div.panel-heading, li.list-group-item, a {
font-size: 1.5em; font-size: 1.5em;
} }
.add_thing_label, #edit_thing_label { .add_thing_label,
#edit_thing_label {
width: 80%; width: 80%;
display: inline-block; display: inline-block;
} }
.add_thing_nb, #edit_thing_nb { .add_thing_nb,
#edit_thing_nb {
width: 18%; width: 18%;
display: inline-block; display: inline-block;
} }
@ -58,7 +62,8 @@ div.panel-heading, li.list-group-item, a {
background: rgba(66, 215, 252, 0.95); background: rgba(66, 215, 252, 0.95);
} }
.alertify .ajs-header, .alertify .ajs-footer { .alertify .ajs-header,
.alertify .ajs-footer {
background-color: #4e5d6c; background-color: #4e5d6c;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,254 +1,243 @@
function SCaseList() { function SCaseList() {
lastChange=0; lastChange = 0;
this.importExampleData=function() { this.importExampleData = function () {
var exampleData={ var exampleData = {
'Vacances': { Vacances: {
'Papier': { Papier: {
'color': '#f00', color: "#f00",
'things': [ things: [
{'label': 'Papier blanc', 'nb': 1 }, { label: _("White paper"), nb: 1 },
{'label': 'Stylo', 'nb': 3 }, { label: _("Pen"), nb: 3 },
{'label': "Carte d'identité", 'nb': 1 }, { label: _("ID card/passport"), nb: 1 },
] ],
}, },
'Multimédia' : { Multimédia: {
'color': '#0f0', color: "#0f0",
'things': [ things: [
{'label': 'Montre', 'nb': 1 }, { label: _("Watch"), nb: 1 },
{'label': 'Chargeur montre', 'nb': 1 }, { label: _("Watch charger"), nb: 1 },
{'label': 'PC portable', 'nb': 1 }, { label: _("Laptop"), nb: 1 },
] { label: _("Laptop charger"), nb: 1 },
} ],
} },
},
}; };
for (scaseName in exampleData) { for (scaseName in exampleData) {
var scase=this.newSCase(scaseName); var scase = this.newSCase(scaseName);
for (catName in exampleData[scaseName]) { for (catName in exampleData[scaseName]) {
var cat=scase.cats.newCat(catName); var cat = scase.cats.newCat(catName);
for (idx in exampleData[scaseName][catName].things) { for (idx in exampleData[scaseName][catName].things) {
cat.newThing(exampleData[scaseName][catName].things[idx]['label'],exampleData[scaseName][catName].things[idx]['nb']); cat.newThing(
} exampleData[scaseName][catName].things[idx]["label"],
} exampleData[scaseName][catName].things[idx]["nb"]
}
}
this.loadFromLocalStorage=function(backData) {
if (jQuery.type(localStorage.scases)!='undefined') {
try {
var data=JSON.parse(localStorage.scases);
this.lastChange=data.lastChange;
for (el in data.scases) {
this[el]=new SCase(false,false,data.scases[el]);
}
}
catch(e) {
for (el in this) {
if (this.isSCase(this[el])) {
delete this[el];
}
}
if (jQuery.type(backData)!='undefined') {
alert('Erreur en chargeant les données. Restauration des données précédentes');
localStorage.scases=backData;
return this.loadFromLocalStorage();
}
else {
alertify.confirm(
"Erreur en chargeant les données locales",
'Une erreur est survenue en chargeant les données locales. On les purges ?',
function(data) {
delete localStorage.scases;
location.reload();
},
null
); );
} }
} }
} }
else { };
alertify.confirm(
"Bienvenu", this.loadFromLocalStorage = function (data) {
"<h2>Bienvenu !</h2><p>Souhaitez-vous charger les données d'exemple ?</p>", if (jQuery.type(localStorage.scases) != "undefined") {
function() { try {
this.importExampleData(); return this.loadFromJsonData(JSON.parse(localStorage.scases));
this.save(); } catch (e) {
show_scases(); return false;
}.bind(this), }
null,
);
} }
} return null;
};
this.export=function() { this.loadFromJsonData = function (data) {
try {
this.lastChange = data.lastChange;
for (el in data.scases) {
this[el] = new SCase(false, false, data.scases[el]);
}
return true;
} catch (e) {
for (el in this) {
if (this.isSCase(this[el])) {
delete this[el];
}
}
}
return false;
};
this.export = function () {
return { return {
'lastChange': this.lastChange, lastChange: this.lastChange,
'scases': this.each(function(idx,scase) { scases: this.each(function (idx, scase) {
return scase.export(); return scase.export();
}) }),
}; };
} };
this.import=function(data) { this.import = function (data) {
ret={}; ret = {};
for (el in this) { for (el in this) {
if (this.isSCase(this[el])) { if (this.isSCase(this[el])) {
delete ret[el]; delete ret[el];
} }
} }
this.lastChange=data.lastChange; this.lastChange = data.lastChange;
for (el in data.scases) { for (el in data.scases) {
this[el]=new SCase(false,false,data.scases[el]); this[el] = new SCase(false, false, data.scases[el]);
} }
return true; return true;
} };
this.save=function() { this.save = function () {
localStorage.scases=JSON.stringify(this.export()); localStorage.scases = JSON.stringify(this.export());
} };
this.each=function(fct) { this.each = function (fct) {
var idx=0; var idx = 0;
var ret={}; var ret = {};
for (el in this) { for (el in this) {
if(this.isSCase(this[el])) { if (this.isSCase(this[el])) {
ret[el]=fct(idx++,this[el]); ret[el] = fct(idx++, this[el]);
} }
} }
return ret; return ret;
} };
this.count=function() { this.count = function () {
len=0; len = 0;
this.each(function(idx,scase) { this.each(function (idx, scase) {
len=len+1; len = len + 1;
}); });
return len; return len;
} };
this.isSCase=function(el) { this.isSCase = function (el) {
return (jQuery.type(el)=='object' && jQuery.type(el.isSCase)=='function' && el.isSCase()); return (
} jQuery.type(el) == "object" &&
jQuery.type(el.isSCase) == "function" &&
el.isSCase()
);
};
this.byName=function(name) { this.byName = function (name) {
for (el in this) { for (el in this) {
if(this.isSCase(this[el])) { if (this.isSCase(this[el])) {
if (this[el].name==name) { if (this[el].name == name) {
return this[el]; return this[el];
} }
} }
} }
return false; return false;
} };
this.removeSCase=function(name) { this.byUUID = function (uuid) {
return this.isCase(this[uuid]) ? this[uuid] : null;
};
this.removeSCase = function (name) {
for (el in this) { for (el in this) {
if (this.isSCase(this[el]) && this[el].name==name) { if (this.isSCase(this[el]) && this[el].name == name) {
this[el].remove(); this[el].remove();
return true; return true;
} }
} }
return false; return false;
} };
this.newSCase=function(name) { this.newSCase = function (name) {
if (this.byName(this[name])) { if (this.byName(this[name])) {
var scase=this.byName(name); var scase = this.byName(name);
if (scase.removed) { if (scase.removed) {
scase.restore(); scase.restore();
return true; return true;
} }
} else {
} var uuid = uuid || generate_uuid();
else { this[uuid] = new SCase(uuid, name);
var uuid=uuid||generate_uuid();
this[uuid]=new SCase(uuid,name);
return this[uuid]; return this[uuid];
} }
return false; return false;
} };
this.renameSCase=function(name,newname) { this.renameSCase = function (name, newname) {
var scase=this.byName(name); var scase = this.byName(name);
if (scase && !this.byName(newname)) { if (scase && !this.byName(newname)) {
scase.name=newname; scase.name = newname;
scase.lastChange=new Date().getTime(); scase.lastChange = new Date().getTime();
return scase; return scase;
} }
return false; return false;
} };
this.copySCase=function(name,newname) { this.copySCase = function (name, newname) {
var orig_scase=this.byName(name); var orig_scase = this.byName(name);
if (this.isSCase(orig_scase) && !this.byName(newname)) { if (this.isSCase(orig_scase) && !this.byName(newname)) {
var uuid=uuid||generate_uuid(); var uuid = uuid || generate_uuid();
this[uuid]=new SCase(false,false,orig_scase.export()); this[uuid] = new SCase(false, false, orig_scase.export());
this[uuid].uuid=uuid; this[uuid].uuid = uuid;
this[uuid].lastChange=new Date().getTime(); this[uuid].lastChange = new Date().getTime();
this[uuid].name=newname; this[uuid].name = newname;
return this[uuid]; return this[uuid];
} }
return false; return false;
} };
this.resetSCase=function(name) { this.resetSCase = function (name) {
for (el in this) { for (el in this) {
if (this.isSCase(this[el]) && this[el].name==name) { if (this.isSCase(this[el]) && this[el].name == name) {
return this[el].reset(); return this[el].reset();
} }
} }
return false; return false;
} };
} }
function SCase(uuid,name,data) { function SCase(uuid, name, data) {
this.uuid=uuid||generate_uuid(); this.uuid = uuid || generate_uuid();
this.name=name; this.name = name;
this.cats=new CatList(); this.cats = new CatList();
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
this.removed=false; this.removed = false;
this.isSCase=function() { this.isSCase = function () {
return true; return true;
} };
this.import=function(data) { this.import = function (data) {
this.uuid=data.uuid || generate_uuid(); this.uuid = data.uuid || generate_uuid();
this.lastChange=data.lastChange || new Date().getTime(); this.lastChange = data.lastChange || new Date().getTime();
this.name=decodeURIComponent(data.name); this.name = decodeURIComponent(data.name);
this.removed=data.removed||false; this.removed = data.removed || false;
if (jQuery.type(data.cats) == 'object') { if (jQuery.type(data.cats) == "object") {
this.cats=new CatList(data.cats); this.cats = new CatList(data.cats);
} }
return true; return true;
} };
this.export=function() { this.export = function () {
return { return {
'uuid': this.uuid, uuid: this.uuid,
'lastChange': this.lastChange, lastChange: this.lastChange,
'name': encodeURIComponent(this.name), name: encodeURIComponent(this.name),
'removed': this.removed, removed: this.removed,
'cats': this.cats.export() cats: this.cats.export(),
}; };
} };
this.byName=function(name) { this.byName = function (name) {
for (idx in this.cats) { for (idx in this.cats) {
if (name==this.cats[idx].name) { if (name == this.cats[idx].name) {
return this.cats[idx]; return this.cats[idx];
} }
} }
return false; return false;
} };
this.stats=function() { this.stats = function () {
var cats=0; var cats = 0;
var things=0; var things = 0;
var things_done=0; var things_done = 0;
this.cats.each(function(cidx,cat) { this.cats.each(function (cidx, cat) {
if (cat.removed) { if (cat.removed) {
return true; return true;
} }
@ -264,357 +253,405 @@ function SCase(uuid,name,data) {
} }
}); });
return { return {
'cats': cats, cats: cats,
'things': things, things: things,
'done': things_done done: things_done,
} };
} };
this.reset=function() { this.reset = function () {
this.cats.each(function(idx,cat) { this.cats.each(function (idx, cat) {
for (idx in cat.things) { for (idx in cat.things) {
if (cat.things[idx].checked) { if (cat.things[idx].checked) {
cat.things[idx].checked=false; cat.things[idx].checked = false;
} }
} }
}); });
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
return true; return true;
} };
this.remove=function() { this.remove = function () {
this.removed=true; this.removed = true;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
this.restore=function() { this.restore = function () {
this.removed=false; this.removed = false;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
/* /*
* Contructor * Constructor
*/ */
if (jQuery.type(data)=='object') { if (jQuery.type(data) == "object") {
try { try {
this.import(data); this.import(data);
} } catch (e) {
catch (e) {
console.log(e); console.log(e);
alert('Une erreur est survenue en chargeant la valise '+this.name+' depuis le cache'); alert(
_(
"An error occurred while loading the %s suitcase from the cache.",
this.name
)
);
} }
} }
} }
function CatList(data) { function CatList(data) {
this.export=function() { this.export = function () {
return this.each(function(idx,cat) { return this.each(function (idx, cat) {
return cat.export(); return cat.export();
}); });
} };
this.import=function(data) { this.import = function (data) {
for (el in this) { for (el in this) {
if (this.isCat(this[el])) { if (this.isCat(this[el])) {
delete this[el]; delete this[el];
} }
} }
for (el in data) { for (el in data) {
this[el]=new Cat(el,false,false,data[el]); this[el] = new Cat(el, false, false, data[el]);
} }
return true; return true;
} };
this.each=function(fct) { this.each = function (fct) {
var idx=0; var idx = 0;
var ret={}; var ret = {};
for (el in this) { for (el in this) {
if(this.isCat(this[el])) { if (this.isCat(this[el])) {
ret[el]=fct(idx++,this[el]); ret[el] = fct(idx++, this[el]);
} }
} }
return ret; return ret;
} };
this.count=function() { this.count = function () {
len=0; len = 0;
this.each(function(idx,cat) { this.each(function (idx, cat) {
len=len+1; len = len + 1;
}); });
return len; return len;
} };
this.isCat=function(el) { this.isCat = function (el) {
return (jQuery.type(el)=='object' && jQuery.type(el.isCat)=='function' && el.isCat()); return (
} jQuery.type(el) == "object" &&
jQuery.type(el.isCat) == "function" &&
el.isCat()
);
};
this.byName=function(name) { this.byName = function (name) {
for (el in this) { for (el in this) {
if(this.isCat(this[el])) { if (this.isCat(this[el])) {
if (this[el].name==name) { if (this[el].name == name) {
return this[el]; return this[el];
} }
} }
} }
return false; return false;
} };
this.newCat=function(name) { this.byUUID = function (uuid) {
return this.isCas(this[uuid]) ? this[uuid] : null;
};
this.newCat = function (name) {
if (this.byName(name)) { if (this.byName(name)) {
var cat=this.byName(name); var cat = this.byName(name);
if (cat.removed) { if (cat.removed) {
cat.restore(); cat.restore();
return true; return true;
} }
} } else {
else { var uuid = uuid || generate_uuid();
var uuid=uuid||generate_uuid(); this[uuid] = new Cat(uuid, name);
this[uuid]=new Cat(uuid,name);
return this[uuid]; return this[uuid];
} }
return false; return false;
} };
this.renameCat=function(name,newname) { this.renameCat = function (name, newname) {
var cat=this.byName(name); var cat = this.byName(name);
if (cat && !this.byName(newname)) { if (cat && !this.byName(newname)) {
cat.name=newname; cat.name = newname;
cat.lastChange=new Date().getTime(); cat.lastChange = new Date().getTime();
return cat; return cat;
} }
return false; return false;
} };
this.removeCat=function(name) { this.removeCat = function (name) {
for (el in this) { for (el in this) {
if (this.isCat(this[el]) && this[el].name==name) { if (this.isCat(this[el]) && this[el].name == name) {
this[el].remove(); this[el].remove();
return true; return true;
} }
} }
return false; return false;
} };
this.restoreCat=function(name) { this.restoreCat = function (name) {
for (el in this) { for (el in this) {
if (this.isCat(this[el]) && this[el].name==name && this[el].removed) { if (this.isCat(this[el]) && this[el].name == name && this[el].removed) {
this[el].restore(); this[el].restore();
return true; return true;
} }
} }
return false; return false;
} };
/* /*
* Contructor * Constructor
*/ */
if (jQuery.type(data)=='object') { if (jQuery.type(data) == "object") {
try { try {
this.import(data); this.import(data);
} } catch (e) {
catch (e) {
console.log(e); console.log(e);
alert('Une erreur est survenue en chargeant la liste de catégorie depuis le cache'); alert(
_("An error occurred while loading the category list from the cache.")
);
} }
} }
} }
function Cat(uuid,name,color,data) { function Cat(uuid, name, color, data) {
this.uuid=generate_uuid(); this.uuid = generate_uuid();
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
this.name=name; this.name = name;
this.color=color || '#'+(0x1000000+(Math.random())*0xffffff).toString(16).substr(1,6); this.color =
this.things={}; color ||
this.removed=false; "#" + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
this.things = {};
this.removed = false;
this.isCat=function() { this.isCat = function () {
return true; return true;
} };
this.import=function(data) { this.import = function (data) {
this.uuid=data.uuid || generate_uuid(); this.uuid = data.uuid || generate_uuid();
this.lastChange=data.lastChange||new Date().getTime(); this.lastChange = data.lastChange || new Date().getTime();
this.name=decodeURIComponent(data.name); this.name = decodeURIComponent(data.name);
this.color=data.color; this.color = data.color;
this.removed=data.removed||false; this.removed = data.removed || false;
if (jQuery.type(data.things) == 'object') { if (jQuery.type(data.things) == "object") {
for (tuuid in data.things) { for (tuuid in data.things) {
this.things[tuuid]=new Thing(tuuid); this.things[tuuid] = new Thing(tuuid);
this.things[tuuid].import(data.things[tuuid]); this.things[tuuid].import(data.things[tuuid]);
} }
} }
return true; return true;
} };
this.export=function() { this.export = function () {
var things={}; var things = {};
for (tuuid in this.things) { for (tuuid in this.things) {
things[tuuid]=this.things[tuuid].export(); things[tuuid] = this.things[tuuid].export();
} }
return { return {
'uuid': this.uuid, uuid: this.uuid,
'lastChange': this.lastChange, lastChange: this.lastChange,
'name': encodeURIComponent(this.name), name: encodeURIComponent(this.name),
'color': this.color, color: this.color,
'removed': this.removed, removed: this.removed,
'things': things things: things,
}; };
} };
this.byLabel=function(label) { this.byLabel = function (label) {
for (idx in this.things) { for (idx in this.things) {
if (label==this.things[idx].label) { if (label == this.things[idx].label) {
return this.things[idx]; return this.things[idx];
} }
} }
return false; return false;
} };
this.count=function() { this.count = function () {
return keys(this.things).length; return keys(this.things).length;
} };
this.stats=function() { this.stats = function () {
var count=0; var count = 0;
var done=0; var done = 0;
for (idx in this.things) { for (idx in this.things) {
if (this.things[idx].removed) { if (this.things[idx].removed) {
continue; continue;
} }
if (this.things[idx].checked) { if (this.things[idx].checked) {
done+=1; done += 1;
} }
count+=1; count += 1;
} }
return { return {
'things': count, things: count,
'done': done done: done,
}; };
} };
this.newThing=function(label,nb) { this.newThing = function (label, nb) {
if (this.byLabel(label)) { if (this.byLabel(label)) {
var thing=this.byLabel(label); var thing = this.byLabel(label);
if (thing.removed) { if (thing.removed) {
thing.restore(); thing.restore();
thing.setChecked(false); thing.setChecked(false);
thing.setNb(nb); thing.setNb(nb);
return true; return true;
} }
} } else {
else { var uuid = generate_uuid();
var uuid=generate_uuid(); this.things[uuid] = new Thing(uuid, label, nb);
this.things[uuid]=new Thing(uuid,label,nb);
return true; return true;
} }
return false; return false;
} };
this.renameThing=function(label,newlabel) { this.renameThing = function (label, newlabel) {
var thing=this.byLabel(label); var thing = this.byLabel(label);
if (thing && !this.byLabel(newlabel)) { if (thing && !this.byLabel(newlabel)) {
thing.label=newlabel; thing.label = newlabel;
thing.lastChange=new Date().getTime(); thing.lastChange = new Date().getTime();
return thing; return thing;
} }
return false; return false;
} };
this.removeThing=function(label) { this.removeThing = function (label) {
for (idx in this.things) { for (idx in this.things) {
if (this.things[idx].label==label) { if (this.things[idx].label == label) {
this.things[idx].remove(); this.things[idx].remove();
return true; return true;
} }
} }
return false; return false;
} };
this.restoreThing=function(label) { this.restoreThing = function (label) {
for (idx in this.things) { for (idx in this.things) {
if (this.things[idx].label==label && this.things[idx].removed) { if (this.things[idx].label == label && this.things[idx].removed) {
this.things[idx].restore(); this.things[idx].restore();
return true; return true;
} }
} }
return false; return false;
} };
this.remove=function() { this.remove = function () {
this.removed=true; this.removed = true;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
this.restore=function() {
this.removed=false;
this.lastChange=new Date().getTime();
}
this.restore = function () {
this.removed = false;
this.lastChange = new Date().getTime();
};
/* /*
* Contructor * Constructor
*/ */
if (jQuery.type(data)=='object') { if (jQuery.type(data) == "object") {
try { try {
this.import(data); this.import(data);
} } catch (e) {
catch (e) {
console.log(e); console.log(e);
alert('Une erreur est survenue en chargeant la catégorie catégorie '+this.name+' depuis le cache'); alert(
_(
"An error occurred while loading the %s category from the cache.",
this.name
)
);
} }
} }
} }
function Thing(uuid,label,nb,checked) { function Thing(uuid, label, nb, checked) {
this.uuid=uuid||generate_uuid(); this.uuid = uuid || generate_uuid();
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
this.label=label; this.label = label;
this.nb=nb || 1; this.nb = nb || 1;
this.checked=checked; this.checked = checked;
this.removed=false; this.removed = false;
this.import=function(data) { this.import = function (data) {
this.uuid=data.uuid||generate_uuid(); this.uuid = data.uuid || generate_uuid();
this.lastChange=data.lastChange||new Date().getTime(); this.lastChange = data.lastChange || new Date().getTime();
this.label=decodeURIComponent(data.label), (this.label = decodeURIComponent(data.label)), (this.nb = data.nb || 1);
this.nb=data.nb||1; this.checked = data.checked;
this.checked=data.checked; this.removed = data.removed || false;
this.removed=data.removed||false; };
}
this.export=function() { this.export = function () {
return { return {
'uuid': this.uuid, uuid: this.uuid,
'lastChange': this.lastChange, lastChange: this.lastChange,
'label': encodeURIComponent(this.label), label: encodeURIComponent(this.label),
'nb': this.nb, nb: this.nb,
'checked': this.checked, checked: this.checked,
'removed': this.removed, removed: this.removed,
}; };
} };
this.setNb=function(nb) { this.setNb = function (nb) {
this.nb=nb; this.nb = nb;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
this.setChecked=function(value) { this.setChecked = function (value) {
this.checked=value; this.checked = value;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} console.log(
`Thing<${this.uuid}>.setChecked(${this.checked}): ${this.lastChange}`
);
};
this.remove=function() { this.remove = function () {
this.removed=true; this.removed = true;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
this.restore=function() { this.restore = function () {
this.removed=false; this.removed = false;
this.lastChange=new Date().getTime(); this.lastChange = new Date().getTime();
} };
}
function User() {
this.username = null;
this.name = null;
this.token = null;
this.loadFromLocalStorage = function () {
if (jQuery.type(localStorage.user) == "undefined") return;
try {
var data = JSON.parse(localStorage.user);
this.username = data.username;
this.name = data.name;
this.token = data.token;
} catch (e) {
alert(_("Error loading your login information. Please log in again."));
}
};
this.connected = function () {
return this.username && this.token;
};
this.reset = function () {
this.username = null;
this.name = null;
this.token = null;
};
this.save = function () {
localStorage.user = JSON.stringify({
username: this.username,
name: this.name,
token: this.token,
});
};
} }

View file

@ -34,31 +34,45 @@
</button> </button>
<a class="navbar-brand" id='app-name'>MySC</a> <a class="navbar-brand" id='app-name'>MySC</a>
</div> </div>
<div class="collapse navbar-collapse" id="navbar-top-collapse"> <div class="collapse navbar-collapse" id="navbar-top-collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="menu menu-scases"><a href="#add_scase" id="add_scase_btn"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une valise</a></li> <li class="menu menu-scases"><a href="#add_scase" id="add_scase_btn"><span class="glyphicon glyphicon-plus-sign"></span> {t}Add a suitcase{/t}</a></li>
<li class="menu menu-scases"><a href="#scases_trash" id="scases_trash_btn"><span class="glyphicon glyphicon-trash"></span> Voir la corbeille</a></li> <li class="menu menu-scases"><a href="#scases_trash" id="scases_trash_btn"><span class="glyphicon glyphicon-trash"></span> {t}Show trash{/t}</a></li>
<li class="menu menu-scase"><a href="#scases" id="back_to_scases"><span class="glyphicon glyphicon-briefcase"></span> Liste des valises</a></li> <li class="menu menu-scase"><a href="#scases" id="back_to_scases"><span class="glyphicon glyphicon-briefcase"></span> {t}Suitcases list{/t}</a></li>
<li class="menu menu-scase dropdown"> <li class="menu menu-scase dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class='glyphicon glyphicon-tag'></span> Gérer la valise <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class='glyphicon glyphicon-tag'></span> {t}Manage the suitcase{/t} <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#add_cat" id="add_cat_btn"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une catégorie</a></li> <li><a href="#add_cat" id="add_cat_btn"><span class="glyphicon glyphicon-plus-sign"></span> {t}Add a category{/t}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="#rename_scase" id="rename_scase_btn"><span class="glyphicon glyphicon-edit"></span> Renommer la valise</a></li> <li><a href="#rename_scase" id="rename_scase_btn"><span class="glyphicon glyphicon-edit"></span> {t}Rename the suitcase{/t}</a></li>
<li><a href="#copy_scase" id="copy_scase_btn"><span class="glyphicon glyphicon-duplicate"></span> Copier la valise</a></li> <li><a href="#copy_scase" id="copy_scase_btn"><span class="glyphicon glyphicon-duplicate"></span> {t}Copy the suitcase{/t}</a></li>
<li><a href="#reset_scase" id="reset_scase_btn"><span class="glyphicon glyphicon-cog"></span> Réinitialiser la valise</a></li> <li><a href="#reset_scase" id="reset_scase_btn"><span class="glyphicon glyphicon-cog"></span> {t}Reset the suitcase{/t}</a></li>
<li><a href="#delete_scase" id="delete_scase_btn"><span class="glyphicon glyphicon-trash"></span> Supprimer la valise</a></li> <li><a href="#delete_scase" id="delete_scase_btn"><span class="glyphicon glyphicon-trash"></span> {t}Delete the suitcase{/t}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="#scase_trash" id="scase_trash_btn"><span class="glyphicon glyphicon-trash"></span> Voir la corbeille de la valise</a></li> <li><a href="#scase_trash" id="scase_trash_btn"><span class="glyphicon glyphicon-trash"></span> {t}Show suitcase's trash{/t}</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class='glyphicon glyphicon-hdd'></span> Gérer vos données <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class='glyphicon glyphicon-hdd'></span> {t}Manage data{/t} <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a id='export_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-save'></span> Sauvegarder vos données</a></li> <li><a id='export_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-save'></span> {t}Backup your data{/t}</a></li>
<li><a id='import_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-open'></span> Restaurer vos données</a></li> <li><a id='import_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-open'></span> {t}Restaure your data{/t}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a id='clear_local_data'><span class='glyphicon glyphicon-trash'></span> Purger les données locales</a></li> <li><a id='sync_local_data'><span class='glyphicon glyphicon-refresh'></span> {t}Synchronize local data{/t}</a></li>
<li class="divider"></li>
<li><a id='clear_local_data'><span class='glyphicon glyphicon-trash'></span> {t}Purge local data{/t}</a></li>
<li><a id='load_example_data'><span class='glyphicon glyphicon-trash'></span> {t}Load example data{/t}</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class='glyphicon glyphicon-user'></span>
<span id="username"></span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a id='login' href='#'><span class='glyphicon glyphicon-loging'></span> {t}Login{/t}</a></li>
<li><a id='logout' href='#'><span class='glyphicon glyphicon-logout'></span> {t}Logout{/t}</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
@ -74,18 +88,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Ajouter une valise</h4> <h4 class="modal-title">{t}Add a suitcase{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='add_scase_name' class="form-control" placeholder="Nom de la valise"/> <input type='text' id='add_scase_name' class="form-control" placeholder="{t}Suitcase name{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='add_scase_submit'>Ajouter</button> <button type="button" class="btn btn-primary" id='add_scase_submit'>{t}Add{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -96,18 +110,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Copier une valise</h4> <h4 class="modal-title">{t}Copy the suitcase{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='copy_scase_name' class="form-control" placeholder="Nom de la nouvelle valise"/> <input type='text' id='copy_scase_name' class="form-control" placeholder="{t}Name of the new suitcase{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='copy_scase_submit'>Copier</button> <button type="button" class="btn btn-primary" id='copy_scase_submit'>{t}Copy{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -118,18 +132,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Renomer la valise</h4> <h4 class="modal-title">{t}Rename the suitcase{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='rename_scase_name' class="form-control" placeholder="Nom de la nouvelle valise"/> <input type='text' id='rename_scase_name' class="form-control" placeholder="{t}New suitcase name{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='rename_scase_submit'>Renomer</button> <button type="button" class="btn btn-primary" id='rename_scase_submit'>{t}Rename{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -140,18 +154,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Ajouter une catégorie</h4> <h4 class="modal-title">{t}Add a category{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='add_cat_name' class="form-control" placeholder="Nom de la catégorie"/> <input type='text' id='add_cat_name' class="form-control" placeholder="{t}Category name{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='add_cat_submit'>Ajouter</button> <button type="button" class="btn btn-primary" id='add_cat_submit'>{t}Add{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -162,18 +176,18 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Renommer une catégorie</h4> <h4 class="modal-title">{t}Rename the category{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='rename_cat_name' class="form-control" placeholder="Nouveau nom de la catégorie"/> <input type='text' id='rename_cat_name' class="form-control" placeholder="{t}New category's name{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='rename_cat_submit'>Renommer</button> <button type="button" class="btn btn-primary" id='rename_cat_submit'>{t}Rename{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -185,23 +199,23 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Ajouter un élément</h4> <h4 class="modal-title">{t}Add an element{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form role="form"> <form role="form">
<div class="form-group"> <div class="form-group">
<input type='text' class='form-control add_thing_label' placeholder="Nom de l'élément"/> <input type='text' class='form-control add_thing_label' placeholder="{t}Element name{/t}"/>
<input type='number' class='form-control add_thing_nb' placeholder="Nb"/> <input type='number' class='form-control add_thing_nb' placeholder="{t}Nb{/t}"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type='text' class='form-control add_thing_label' placeholder="Un autre ?"/> <input type='text' class='form-control add_thing_label' placeholder="{t}Another?{/t}"/>
<input type='number' class='form-control add_thing_nb' placeholder="Nb"/> <input type='number' class='form-control add_thing_nb' placeholder="{t}Nb{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='add_thing_submit'>Ajouter</button> <button type="button" class="btn btn-primary" id='add_thing_submit'>{t}Add{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -212,19 +226,19 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Modifier un élément</h4> <h4 class="modal-title">{t}Edit the element{/t}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<input type='text' id='edit_thing_label' class="form-control" placeholder="Nouveau nom de l'élément"/> <input type='text' id='edit_thing_label' class="form-control" placeholder="{t}Element name{/t}"/>
<input type='number' id='edit_thing_nb' class="form-control" placeholder="Nb"/> <input type='number' id='edit_thing_nb' class="form-control" placeholder="{t}Nb{/t}"/>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button> <button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='edit_thing_submit'>Modifier</button> <button type="button" class="btn btn-primary" id='edit_thing_submit'>{t}Modify{/t}</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
@ -235,12 +249,12 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Chargement...</h2> <h2 class="modal-title">{t}Loading...{/t}</h2>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="progress progress-striped active"> <div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"> <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">
<span class="sr-only">Chargement...</span> <span class="sr-only">{t}Loading...{/t}</span>
</div> </div>
</div> </div>
</div> </div>
@ -249,19 +263,87 @@
</div> </div>
<div class="modal" id="login_modal" tabindex="-1" role="dialog" aria-labelledby="loginModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{t}Connection{/t}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" role="form" action="login">
<div class="form-group">
<input type='text' id='login_username' class="form-control" placeholder="{t}Username{/t}" required/>
<input type='password' id='login_password' class="form-control" placeholder="{t}Password{/t}" required/>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{t}Cancel{/t}</button>
<button type="button" class="btn btn-primary" id='login_submit'>{t}Connect{/t}</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<div class="modal" id="welcome_modal" tabindex="-1" role="dialog" aria-labelledby="welcomeModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{t}Welcome{/t}</h4>
</div>
<div class="modal-body">
{t escape=false}<p>
This application allows you to manage lists of things not to forget to pack in your
suitcase before your departure:
<ul>
<li>
To get started, create a suitcase, add the categories of things you will need to
pack, and add all these things to it;
</li>
<li>
Before your departure, you can then gradually check off all the things you have
already gathered and ensure you don't forget anything!
</li>
</ul>
</p>
<p>
<strong>Note:</strong> This application has been designed to work completely locally.
The data manipulated (your suitcases, etc.) is stored only in your internet browser.
It is also possible to export/import this information in JSON format.<br/>
However, to facilitate the management of your suitcases from multiple devices, it's
possible to synchronize this information on the server. For this, you need an account,
and at this time, registrations are not open.
</p>
<p>
If you have an account, you can log in by clicking the <em>Login</em> button below.
If not, click the <em>Anonymous usage</em> button to start using the application.
</p>{/t}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id='welcome_connect'>{t}Connect{/t}</button>
<button type="button" class="btn btn-default" id='welcome_annonymous'>{t}Anonymous usage{/t}</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{static_url path="lib/jquery.min.js"}"></script> <script src="{static_url path="lib/jquery.min.js"}"></script>
<!-- Latest compiled and minified JavaScript --> <!-- Latest compiled and minified JavaScript -->
<script src="{static_url path="lib/alertify/alertify.min.js"}"></script> <script src="{static_url path="lib/alertify/alertify.min.js"}"></script>
<script src="{static_url path="lib/bootstrap/js/bootstrap.js"}"></script> <script src="{static_url path="lib/bootstrap/js/bootstrap.js"}"></script>
<script> <!-- Other libs & JavaScript scripts -->
{foreach $js as $path}
<script language="javascript" src="{$path|escape:"quotes"}"></script>
{/foreach}
</script> <script src="{static_url path="lib/uuid.js"}"></script>
<script src="{static_url path="mysc_objects.js"}"></script>
<script src="{static_url path="lib/uuid.js"}"></script> <script src="{static_url path="main.js"}"></script>
<script src="{static_url path="mysc_objects.js"}"></script>
<script src="{static_url path="main.js"}"></script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,13 @@
{extends file='Tpl:empty.tpl'}
{block name="pagetitle"}{/block}
{block name="content"}
<h1>{t}Helpdesk page{/t}</h1>
<p>{t}Upon request, please download and forward the following information to the support service:{/t}</p>
<div class="text-center mb-2">
<a href="{$request->current_url}?download" class="btn btn-primary">
<i class="fa fa-download" aria-hidden="true"></i>
{t}Download{/t}
</a>
</div>
<pre class="text-bg-light p-3 copyable">{include file="Tpl:support_info_content.tpl"}</pre>
{/block}

View file

@ -0,0 +1,8 @@
{t}Application URL:{/t} {$public_root_url}
{t}Current page URL:{/t} {$public_root_url}/{$request->current_url}
{t}Connected user:{/t} {$auth_user->username}
{t}Extra user information:{/t}
================================================
{var_dump data=$auth_user->info}