feat: backup and restore (#288)
This commit is contained in:
parent
65cc376dff
commit
7b509d2b3d
2
.tmp/.gitignore
vendored
Normal file
2
.tmp/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@ -6,11 +6,11 @@ WORKDIR /var/www/html
|
||||
|
||||
# Update packages and install dependencies
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache sqlite-dev libpng libpng-dev libjpeg-turbo libjpeg-turbo-dev freetype freetype-dev curl autoconf libgomp icu-dev nginx dcron tzdata imagemagick imagemagick-dev && \
|
||||
apk add --no-cache sqlite-dev libpng libpng-dev libjpeg-turbo libjpeg-turbo-dev freetype freetype-dev curl autoconf libgomp icu-dev nginx dcron tzdata imagemagick imagemagick-dev libzip-dev && \
|
||||
docker-php-ext-install pdo pdo_sqlite && \
|
||||
docker-php-ext-enable pdo pdo_sqlite && \
|
||||
docker-php-ext-configure gd --with-freetype --with-jpeg && \
|
||||
docker-php-ext-install -j$(nproc) gd intl && \
|
||||
docker-php-ext-install -j$(nproc) gd intl zip && \
|
||||
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS && \
|
||||
pecl install imagick && \
|
||||
docker-php-ext-enable imagick && \
|
||||
|
||||
@ -61,6 +61,7 @@ See instructions to run Wallos below.
|
||||
- intl
|
||||
- openssl
|
||||
- sqlite3
|
||||
- zip
|
||||
|
||||
#### Docker
|
||||
|
||||
|
||||
71
endpoints/db/backup.php
Normal file
71
endpoints/db/backup.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once '../../includes/connect_endpoint.php';
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => translate('session_expired', $i18n)
|
||||
]));
|
||||
}
|
||||
|
||||
function addFolderToZip($dir, $zipArchive, $zipdir = ''){
|
||||
if (is_dir($dir)) {
|
||||
if ($dh = opendir($dir)) {
|
||||
//Add the directory
|
||||
if(!empty($zipdir)) $zipArchive->addEmptyDir($zipdir);
|
||||
while (($file = readdir($dh)) !== false) {
|
||||
// Skip '.' and '..'
|
||||
if ($file == "." || $file == "..") {
|
||||
continue;
|
||||
}
|
||||
//If it's a folder, run the function again!
|
||||
if(is_dir($dir . $file)){
|
||||
$newdir = $dir . $file . '/';
|
||||
addFolderToZip($newdir, $zipArchive, $zipdir . $file . '/');
|
||||
}else{
|
||||
//Add the files
|
||||
$zipArchive->addFile($dir . $file, $zipdir . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "Directory does not exist: $dir"
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$filename = "backup_" . uniqid() . ".zip";
|
||||
$zipname = "../../.tmp/" . $filename;
|
||||
|
||||
if ($zip->open($zipname, ZipArchive::CREATE)!==TRUE) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => translate('cannot_open_zip', $i18n)
|
||||
]));
|
||||
}
|
||||
|
||||
addFolderToZip('../../db/', $zip);
|
||||
addFolderToZip('../../images/uploads/', $zip);
|
||||
|
||||
$numberOfFilesAdded = $zip->numFiles;
|
||||
|
||||
if ($zip->close() === false) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "Failed to finalize the zip file"
|
||||
]));
|
||||
} else {
|
||||
flush();
|
||||
die(json_encode([
|
||||
"success" => true,
|
||||
"message" => "Zip file created successfully",
|
||||
"numFiles" => $numberOfFilesAdded,
|
||||
"file" => $filename
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
112
endpoints/db/import.php
Normal file
112
endpoints/db/import.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
require_once '../../includes/connect_endpoint.php';
|
||||
session_start();
|
||||
|
||||
$result = $db->query("SELECT COUNT(*) as count FROM user");
|
||||
$row = $result->fetchArray(SQLITE3_NUM);
|
||||
if ($row[0] > 0) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "Denied"
|
||||
]));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_FILES['file'])) {
|
||||
$file = $_FILES['file'];
|
||||
$fileTmpName = $file['tmp_name'];
|
||||
$fileError = $file['error'];
|
||||
|
||||
if ($fileError === 0) {
|
||||
$fileDestination = '../../.tmp/restore.zip';
|
||||
move_uploaded_file($fileTmpName, $fileDestination);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($fileDestination) === true) {
|
||||
$zip->extractTo('../../.tmp/restore/');
|
||||
$zip->close();
|
||||
} else {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "Failed to extract the uploaded file"
|
||||
]));
|
||||
}
|
||||
|
||||
if (file_exists('../../.tmp/restore/wallos.db')) {
|
||||
if (file_exists('../../db/wallos.db')) {
|
||||
unlink('../../db/wallos.db');
|
||||
}
|
||||
rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');
|
||||
|
||||
if (file_exists('../../.tmp/restore/logos/')) {
|
||||
$dir = '../../images/uploads/logos/';
|
||||
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
||||
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
|
||||
|
||||
foreach ( $ri as $file ) {
|
||||
if ( $file->isDir() ) {
|
||||
rmdir($file->getPathname());
|
||||
} else {
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
$dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');
|
||||
$ite = new RecursiveIteratorIterator($dir);
|
||||
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
|
||||
|
||||
foreach ($ite as $filePath) {
|
||||
if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {
|
||||
$destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);
|
||||
$destinationDir = pathinfo($destination, PATHINFO_DIRNAME);
|
||||
|
||||
if (!is_dir($destinationDir)) {
|
||||
mkdir($destinationDir, 0755, true);
|
||||
}
|
||||
|
||||
copy($filePath, $destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($files as $fileinfo) {
|
||||
$removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
|
||||
$removeFunction($fileinfo->getRealPath());
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => translate("success", $i18n)
|
||||
]);
|
||||
} else {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "wallos.db does not exist in the backup file"
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Failed to upload file"
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "No file uploaded"
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Invalid request method"
|
||||
]);
|
||||
}
|
||||
?>
|
||||
109
endpoints/db/restore.php
Normal file
109
endpoints/db/restore.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
require_once '../../includes/connect_endpoint.php';
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => translate('session_expired', $i18n)
|
||||
]));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_FILES['file'])) {
|
||||
$file = $_FILES['file'];
|
||||
$fileTmpName = $file['tmp_name'];
|
||||
$fileError = $file['error'];
|
||||
|
||||
if ($fileError === 0) {
|
||||
$fileDestination = '../../.tmp/restore.zip';
|
||||
move_uploaded_file($fileTmpName, $fileDestination);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($fileDestination) === true) {
|
||||
$zip->extractTo('../../.tmp/restore/');
|
||||
$zip->close();
|
||||
} else {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "Failed to extract the uploaded file"
|
||||
]));
|
||||
}
|
||||
|
||||
if (file_exists('../../.tmp/restore/wallos.db')) {
|
||||
if (file_exists('../../db/wallos.db')) {
|
||||
unlink('../../db/wallos.db');
|
||||
}
|
||||
rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');
|
||||
|
||||
if (file_exists('../../.tmp/restore/logos/')) {
|
||||
$dir = '../../images/uploads/logos/';
|
||||
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
||||
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
|
||||
|
||||
foreach ( $ri as $file ) {
|
||||
if ( $file->isDir() ) {
|
||||
rmdir($file->getPathname());
|
||||
} else {
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
$dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');
|
||||
$ite = new RecursiveIteratorIterator($dir);
|
||||
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
|
||||
|
||||
foreach ($ite as $filePath) {
|
||||
if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {
|
||||
$destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);
|
||||
$destinationDir = pathinfo($destination, PATHINFO_DIRNAME);
|
||||
|
||||
if (!is_dir($destinationDir)) {
|
||||
mkdir($destinationDir, 0755, true);
|
||||
}
|
||||
|
||||
copy($filePath, $destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($files as $fileinfo) {
|
||||
$removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
|
||||
$removeFunction($fileinfo->getRealPath());
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => translate("success", $i18n)
|
||||
]);
|
||||
} else {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => "wallos.db does not exist in the backup file"
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Failed to upload file"
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "No file uploaded"
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Invalid request method"
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once '../../includes/connect_endpoint.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"message" => translate('session_expired', $i18n)
|
||||
]));
|
||||
}
|
||||
|
||||
require_once '../../includes/getdbkeys.php';
|
||||
|
||||
$query = "SELECT * FROM subscriptions";
|
||||
|
||||
$result = $db->query($query);
|
||||
if ($result) {
|
||||
$subscriptions = array();
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
// Map foreign keys to their corresponding values
|
||||
$row['currency'] = $currencies[$row['currency_id']];
|
||||
$row['payment_method'] = $payment_methods[$row['payment_method_id']];
|
||||
$row['payer_user'] = $members[$row['payer_user_id']];
|
||||
$row['category'] = $categories[$row['category_id']];
|
||||
$row['cycle'] = $cycles[$row['cycle']];
|
||||
$row['frequency'] = $frequencies[$row['frequency']];
|
||||
|
||||
$subscriptions[] = $row;
|
||||
}
|
||||
|
||||
// Output JSON
|
||||
$json = json_encode($subscriptions, JSON_PRETTY_PRINT);
|
||||
|
||||
// Set headers for file download
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Disposition: attachment; filename="subscriptions.json"');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
// Output JSON for download
|
||||
echo $json;
|
||||
} else {
|
||||
echo json_encode(array('error' => 'Failed to fetch subscriptions.'));
|
||||
}
|
||||
|
||||
?>
|
||||
@ -8,6 +8,12 @@
|
||||
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$userData = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if ($userData === false) {
|
||||
header('Location: logout.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($userData['avatar'] == "") {
|
||||
$userData['avatar'] = "0";
|
||||
}
|
||||
|
||||
@ -24,5 +24,11 @@
|
||||
<div class="progress success"></div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (isset($db)) {
|
||||
$db->close();
|
||||
}
|
||||
?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Die Passwörter stimmen nicht überein",
|
||||
"registration_failed" => "Registrierung fehlgeschlagen, bitte erneut versuchen.",
|
||||
"register" => "Registrieren",
|
||||
"restore_database" => "Datenbank wiederherstellen",
|
||||
// Login Page
|
||||
'please_login' => "Bitte einloggen",
|
||||
'stay_logged_in' => "Angemeldet bleiben (30 Tage)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Hinzufügen",
|
||||
"save" => "Speichern",
|
||||
"reset" => "Zurücksetzen",
|
||||
"export_subscriptions" => "Abonnements exportieren",
|
||||
"export_to_json" => "Nach JSON exportieren",
|
||||
"backup_and_restore" => "Backup und Wiederherstellung",
|
||||
"backup" => "Backup",
|
||||
"restore" => "Wiederherstellen",
|
||||
"restore_info" => "Durch die Wiederherstellung der Datenbank werden alle aktuellen Daten überschrieben. Nach der Wiederherstellung werden Sie abgemeldet.",
|
||||
// Filters menu
|
||||
"filter" => "Filter",
|
||||
"clear" => "Leeren",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
|
||||
"registration_failed" => "Η εγγραφή απέτυχε, παρακαλώ προσπάθησε ξανά.",
|
||||
"register" => "Εγγραφή",
|
||||
"restore_database" => "Επαναφορά βάσης δεδομένων",
|
||||
// Login Page
|
||||
'please_login' => "Παρακαλώ συνδέσου",
|
||||
'stay_logged_in' => "Μείνε συνδεδεμένος (30 ημέρες)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Προσθήκη",
|
||||
"save" => "Αποθήκευση",
|
||||
"reset" => "Επαναφορά",
|
||||
"export_subscriptions" => "Εξαγωγή συνδρομών",
|
||||
"export_to_json" => "Εξαγωγή σε JSON",
|
||||
"backup_and_restore" => "Αντίγραφο ασφαλείας και επαναφορά",
|
||||
"backup" => "Αντίγραφο ασφαλείας",
|
||||
"restore" => "Επαναφορά",
|
||||
"restore_info" => "Η επαναφορά της βάσης δεδομένων θα ακυρώσει όλα τα τρέχοντα δεδομένα. Μετά την επαναφορά θα αποσυνδεθείτε.",
|
||||
// Filters menu
|
||||
"filter" => "Φίλτρο",
|
||||
"clear" => "Καθαρισμός",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Passwords do not match",
|
||||
"registration_failed" => "Registration failed, please try again.",
|
||||
"register" => "Register",
|
||||
"restore_database" => "Restore Database",
|
||||
// Login Page
|
||||
'please_login' => "Please login",
|
||||
'stay_logged_in' => "Stay logged in (30 days)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Add",
|
||||
"save" => "Save",
|
||||
"reset" => "Reset",
|
||||
"export_subscriptions" => "Export Subscriptions",
|
||||
"export_to_json" => "Export to JSON",
|
||||
"backup_and_restore" => "Backup and Restore",
|
||||
"backup" => "Backup",
|
||||
"restore" => "Restore",
|
||||
"restore_info" => "Restoring the database will override all current data. You will be signed out after the restore.",
|
||||
// Filters menu
|
||||
"filter" => "Filter",
|
||||
"clear" => "Clear",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Las contraseñas no coinciden",
|
||||
"registration_failed" => "Error en el registro, por favor inténtalo de nuevo.",
|
||||
"register" => "Registrar",
|
||||
"restore_database" => "Restaurar Base de Datos",
|
||||
// Login Page
|
||||
'please_login' => "Por favor, inicia sesión",
|
||||
'stay_logged_in' => "Mantener sesión iniciada (30 días)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Agregar",
|
||||
"save" => "Guardar",
|
||||
"reset" => "Restablecer",
|
||||
"export_subscriptions" => "Exportar suscripciones",
|
||||
"export_to_json" => "Exportar a JSON",
|
||||
"backup_and_restore" => "Copia de Seguridad y Restauración",
|
||||
"backup" => "Copia de Seguridad",
|
||||
"restore" => "Restaurar",
|
||||
"restore_info" => "La restauración de la base de datos anulará todos los datos actuales. Se cerrará la sesión después de la restauración.",
|
||||
// Filters menu
|
||||
"filter" => "Filtrar",
|
||||
"clear" => "Limpiar",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Les mots de passe ne correspondent pas",
|
||||
"registration_failed" => "L'inscription a échoué, veuillez réessayer.",
|
||||
"register" => "S'inscrire",
|
||||
"restore_database" => "Restaurer la base de données",
|
||||
// Page de connexion
|
||||
'please_login' => "Veuillez vous connecter",
|
||||
'stay_logged_in' => "Rester connecté (30 jours)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Ajouter",
|
||||
"save" => "Enregistrer",
|
||||
"reset" => "Réinitialiser",
|
||||
"export_subscriptions" => "Exporter les abonnements",
|
||||
"export_to_json" => "Exporter en JSON",
|
||||
"backup_and_restore" => "Sauvegarde et restauration",
|
||||
"backup" => "Sauvegarde",
|
||||
"restore" => "Restauration",
|
||||
"restore_info" => "La restauration de la base de données annulera toutes les données actuelles. Vous serez déconnecté après la restauration.",
|
||||
// Menu des filtes
|
||||
"filter" => "Filtre",
|
||||
"clear" => "Effacer",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
'passwords_dont_match' => 'Le password non corrispondono',
|
||||
'registration_failed' => 'Registrazione fallita, riprova.',
|
||||
'register' => 'Registrati',
|
||||
"restore_database" => 'Ripristina database',
|
||||
|
||||
// Login
|
||||
'please_login' => 'Per favore, accedi',
|
||||
@ -171,9 +172,10 @@ $i18n = [
|
||||
'add' => 'Aggiungi',
|
||||
'save' => 'Salva',
|
||||
"reset" => 'Ripristina',
|
||||
'export_subscriptions' => 'Esporta abbonamenti',
|
||||
'export_to_json' => 'Esporta in JSON',
|
||||
|
||||
"backup_and_restore" => 'Backup e ripristino',
|
||||
"backup" => 'Backup',
|
||||
"restore" => 'Ripristina',
|
||||
"restore_info" => "Il ripristino del database annullerà tutti i dati correnti. Al termine del ripristino, l'utente verrà disconnesso.",
|
||||
// Filters
|
||||
'filter' => 'Filtra',
|
||||
'clear' => 'Pulisci',
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "パスワードが違います",
|
||||
"registration_failed" => "登録に失敗しました。もう一度お試しください。",
|
||||
"register" => "登録する",
|
||||
"restore_database" => "データベースをリストア",
|
||||
// Login Page
|
||||
'please_login' => "ログインしてください",
|
||||
'stay_logged_in' => "ログインしたままにする (30日)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "追加",
|
||||
"save" => "保存",
|
||||
"reset" => "リセット",
|
||||
"export_subscriptions" => "購読をエクスポート",
|
||||
"export_to_json" => "JSONにエクスポート",
|
||||
"backup_and_restore" => "バックアップとリストア",
|
||||
"backup" => "バックアップ",
|
||||
"restore" => "リストア",
|
||||
"restore_info" => "データベースをリストアすると、現在のデータがすべて上書きされます。リストア後はサインアウトされます。",
|
||||
// Filters menu
|
||||
"filter" => "フィルタ",
|
||||
"clear" => "クリア",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Hasła nie pasują",
|
||||
"registration_failed" => "Rejestracja nie powiodła się, spróbuj ponownie.",
|
||||
"register" => "Rejestracja",
|
||||
"restore_database" => "Przywróć bazę danych",
|
||||
// Login Page
|
||||
'please_login' => "Proszę się zalogować",
|
||||
'stay_logged_in' => "Pozostań zalogowany (30 dni)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Dodaj",
|
||||
"save" => "Zapisz",
|
||||
"reset" => "Resetuj",
|
||||
"export_subscriptions" => "Eksportuj subskrypcje",
|
||||
"export_to_json" => "Eksportuj do JSON",
|
||||
"backup_and_restore" => "Kopia zapasowa i przywracanie",
|
||||
"backup" => "Kopia zapasowa",
|
||||
"restore" => "Przywróć",
|
||||
"restore_info" => "Przywrócenie bazy danych zastąpi wszystkie bieżące dane. Po przywróceniu zostaniesz wylogowany.",
|
||||
// Filters menu
|
||||
"filter" => "Filtr",
|
||||
"clear" => "Wyczyść",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "As passwords não coincidem",
|
||||
"registration_failed" => "O registo falhou. Tente novamente",
|
||||
"register" => "Registar",
|
||||
"restore_database" => "Restaurar base de dados",
|
||||
// Login Page
|
||||
'please_login' => "Por favor inicie sessão",
|
||||
'stay_logged_in' => "Manter sessão (30 dias)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Adicionar",
|
||||
"save" => "Guardar",
|
||||
"reset" => "Repor",
|
||||
"export_subscriptions" => "Exportar Subscrições",
|
||||
"export_to_json" => "Exportar para JSON",
|
||||
"backup_and_restore" => "Backup e Restauro",
|
||||
"backup" => "Backup",
|
||||
"restore" => "Restauro",
|
||||
"restore_info" => "O restauro da base de dados apagará todos os dados actuais. A sua sessão irá terminar após o restauro.",
|
||||
// Filters menu
|
||||
"filter" => "Filtro",
|
||||
"clear" => "Limpar",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "As senhas não são iguais",
|
||||
"registration_failed" => "O registro falhou. Por favor, tente novamente",
|
||||
"register" => "Registrar",
|
||||
"restore_database" => "Restaurar banco de dados",
|
||||
// Login Page
|
||||
'please_login' => "Por favor, faça o login",
|
||||
'stay_logged_in' => "Me manter logado (30 dias)",
|
||||
@ -164,8 +165,10 @@ $i18n = [
|
||||
"add" => "Adicionar",
|
||||
"save" => "Salvar",
|
||||
"reset" => "Redefinir",
|
||||
"export_subscriptions" => "Exportar assinaturas",
|
||||
"export_to_json" => "Exportar para JSON",
|
||||
"backup_and_restore" => "Backup e Restauração",
|
||||
"backup" => "Backup",
|
||||
"restore" => "Restaurar",
|
||||
"restore_info" => "A restauração do banco de dados substituirá todos os dados atuais. Você será desconectado após a restauração.",
|
||||
// Filters menu
|
||||
"filter" => "Filtrar",
|
||||
"clear" => "Limpar",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Лозинке се не поклапају",
|
||||
"registration_failed" => "Регистрација није успела, покушајте поново.",
|
||||
"register" => "Региструј се",
|
||||
"restore_database" => "Врати базу података",
|
||||
// Страница за пријаву
|
||||
'please_login' => "Молимо вас да се пријавите",
|
||||
'stay_logged_in' => "Остани пријављен (30 дана)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Додај",
|
||||
"save" => "Сачувај",
|
||||
"reset" => "Ресетуј",
|
||||
"export_subscriptions" => "Извоз претплата",
|
||||
"export_to_json" => "Извоз у JSON формат",
|
||||
"backup_and_restore" => "Бекап и ресторе",
|
||||
"backup" => "Бекап",
|
||||
"restore" => "Ресторе",
|
||||
"restore_info" => "Враћање базе података ће заменити све тренутне податке. Бићете одјављени након враћања.",
|
||||
// Мени са филтерима
|
||||
"filter" => "Филтер",
|
||||
"clear" => "Очисти",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Lozinke se ne poklapaju",
|
||||
"registration_failed" => "Registracija nije uspela, pokušajte ponovo.",
|
||||
"register" => "Registruj se",
|
||||
"restore_database" => "Vrati bazu podataka",
|
||||
// Stranica za prijavu
|
||||
'please_login' => "Molimo vas da se prijavite",
|
||||
'stay_logged_in' => "Ostani prijavljen (30 dana)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Dodaj",
|
||||
"save" => "Sačuvaj",
|
||||
"reset" => "Resetuj",
|
||||
"export_subscriptions" => "Izvezi pretplate",
|
||||
"export_to_json" => "Izvezi u JSON format",
|
||||
"backup_and_restore" => "Backup i restore",
|
||||
"backup" => "Backup",
|
||||
"restore" => "Restore",
|
||||
"restore_info" => "Vraćanje baze podataka će zameniti sve trenutne podatke. Bićete odjavljeni nakon vraćanja.",
|
||||
// Meni sa filterima
|
||||
"filter" => "Filter",
|
||||
"clear" => "Očisti",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "Şifreler eşleşmiyor",
|
||||
"registration_failed" => "Kayıt başarısız, lütfen tekrar deneyin.",
|
||||
"register" => "Kayıt Ol",
|
||||
"restore_database" => "Veritabanını geri yükle",
|
||||
// Login Page
|
||||
'please_login' => "Lütfen giriş yapın",
|
||||
'stay_logged_in' => "Oturumu açık tut (30 gün)",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "Ekle",
|
||||
"save" => "Kaydet",
|
||||
"reset" => "Sıfırla",
|
||||
"export_subscriptions" => "Abonelikleri Dışa Aktar",
|
||||
"export_to_json" => "JSON'a dışa aktar",
|
||||
"backup_and_restore" => "Yedekle ve Geri Yükle",
|
||||
"backup" => "Yedekle",
|
||||
"restore" => "Geri Yükle",
|
||||
"restore_info" => "Veritabanının geri yüklenmesi tüm mevcut verileri geçersiz kılacaktır. Geri yüklemeden sonra oturumunuz kapatılacaktır.",
|
||||
// Filters menu
|
||||
"filter" => "Filtre",
|
||||
"clear" => "Temizle",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "密码不匹配",
|
||||
"registration_failed" => "注册失败,请重试。",
|
||||
"register" => "注册",
|
||||
"restore_database" => "恢复数据库",
|
||||
|
||||
// 登录页面
|
||||
'please_login' => "请登录",
|
||||
@ -173,9 +174,10 @@ $i18n = [
|
||||
"add" => "添加",
|
||||
"save" => "保存",
|
||||
"reset" => "重置",
|
||||
"export_subscriptions" => "导出订阅",
|
||||
"export_to_json" => "导出为 JSON",
|
||||
|
||||
"backup_and_restore" => "备份和恢复",
|
||||
"backup" => "备份",
|
||||
"restore" => "恢复",
|
||||
"restore_info" => "还原数据库将覆盖所有当前数据。还原后,您将退出登录。",
|
||||
// Filters menu
|
||||
"filter" => "筛选",
|
||||
"clear" => "清除",
|
||||
|
||||
@ -12,6 +12,7 @@ $i18n = [
|
||||
"passwords_dont_match" => "密碼不一致",
|
||||
"registration_failed" => "註冊失敗,請再試一次。",
|
||||
"register" => "註冊",
|
||||
"restore_database" => "還原資料庫",
|
||||
// 登入頁面
|
||||
'please_login' => "請先登入",
|
||||
'stay_logged_in' => "保持登入 30 天",
|
||||
@ -166,8 +167,10 @@ $i18n = [
|
||||
"add" => "新增",
|
||||
"save" => "儲存",
|
||||
"reset" => "重設",
|
||||
"export_subscriptions" => "匯出訂閱",
|
||||
"export_to_json" => "匯出為 JSON 檔案",
|
||||
"backup_and_restore" => "備份與還原",
|
||||
"backup" => "備份",
|
||||
"restore" => "還原",
|
||||
"restore_info" => "復原資料庫將覆蓋所有目前資料。 恢復後您將被註銷。",
|
||||
// Filters menu
|
||||
"filter" => "篩選",
|
||||
"clear" => "清除",
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<?php
|
||||
$version = "v1.22.0";
|
||||
$version = "v1.23.0";
|
||||
?>
|
||||
|
||||
@ -88,7 +88,7 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
|
||||
<link rel="stylesheet" href="styles/theme.css?<?= $version ?>">
|
||||
<link rel="stylesheet" href="styles/login.css?<?= $version ?>">
|
||||
<link rel="stylesheet" href="styles/themes/red.css?<?= $version ?>" id="red-theme" <?= $colorTheme != "red" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/themes/yellow.css?<?= $version ?>" id="yellow-theme" <?= $colorTheme != "yellow" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/barlow.css">
|
||||
<link rel="stylesheet" href="styles/login-dark-theme.css?<?= $version ?>" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>
|
||||
|
||||
@ -26,6 +26,11 @@ if (isset($_COOKIE['theme'])) {
|
||||
$theme = $_COOKIE['theme'];
|
||||
}
|
||||
|
||||
$colorTheme = "blue";
|
||||
if (isset($_COOKIE['colorTheme'])) {
|
||||
$colorTheme = $_COOKIE['colorTheme'];
|
||||
}
|
||||
|
||||
$currencies = array();
|
||||
$query = "SELECT * FROM currencies";
|
||||
$result = $db->query($query);
|
||||
@ -94,9 +99,12 @@ if (isset($_POST['username'])) {
|
||||
<title>Wallos - Subscription Tracker</title>
|
||||
<link rel="icon" type="image/png" href="images/icon/favicon.ico" sizes="16x16">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="images/icon/apple-touch-icon.png">
|
||||
<link rel="manifest" href="manifes.json">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="stylesheet" href="styles/theme.css?<?= $version ?>">
|
||||
<link rel="stylesheet" href="styles/login.css?<?= $version ?>">
|
||||
<link rel="stylesheet" href="styles/themes/red.css?<?= $version ?>" id="red-theme" <?= $colorTheme != "red" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/themes/yellow.css?<?= $version ?>" id="yellow-theme" <?= $colorTheme != "yellow" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/login-dark-theme.css?<?= $version ?>" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>
|
||||
<link rel="stylesheet" href="styles/barlow.css">
|
||||
<script type="text/javascript" src="scripts/registration.js?<?= $version ?>"></script>
|
||||
@ -107,9 +115,9 @@ if (isset($_POST['username'])) {
|
||||
<header>
|
||||
<?php
|
||||
if ($theme == "light") {
|
||||
?> <img src="images/siteicons/blue/wallos.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" /> <?php
|
||||
?> <img src="images/siteicons/<?= $colorTheme ?>/wallos.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" width="215" /> <?php
|
||||
} else {
|
||||
?> <img src="images/siteicons/blue/walloswhite.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" /> <?php
|
||||
?> <img src="images/siteicons/<?= $colorTheme ?>/walloswhite.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" width="215" /> <?php
|
||||
}
|
||||
?>
|
||||
<p>
|
||||
@ -180,8 +188,14 @@ if (isset($_POST['username'])) {
|
||||
<input type="submit" value="<?= translate('register', $i18n) ?>">
|
||||
</div>
|
||||
</form>
|
||||
<div class="separator">
|
||||
<input type="button" class="secondary-button" value="<?= translate('restore_database', $i18n) ?>" id="restoreDB" onClick="openRestoreDBFileSelect()" />
|
||||
<input type="file" name="restoreDBFile" id="restoreDBFile" style="display: none;" onChange="restoreDB()" accept=".zip">
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
@ -62,6 +62,101 @@ function runDatabaseMigration() {
|
||||
});
|
||||
}
|
||||
|
||||
function showErrorMessage(message) {
|
||||
const toast = document.querySelector(".toast#errorToast");
|
||||
(closeIcon = document.querySelector(".close-error")),
|
||||
(errorMessage = document.querySelector(".errorMessage")),
|
||||
(progress = document.querySelector(".progress.error"));
|
||||
let timer1, timer2;
|
||||
errorMessage.textContent = message;
|
||||
toast.classList.add("active");
|
||||
progress.classList.add("active");
|
||||
timer1 = setTimeout(() => {
|
||||
toast.classList.remove("active");
|
||||
closeIcon.removeEventListener("click", () => {});
|
||||
}, 5000);
|
||||
|
||||
timer2 = setTimeout(() => {
|
||||
progress.classList.remove("active");
|
||||
}, 5300);
|
||||
|
||||
closeIcon.addEventListener("click", () => {
|
||||
toast.classList.remove("active");
|
||||
|
||||
setTimeout(() => {
|
||||
progress.classList.remove("active");
|
||||
}, 300);
|
||||
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
closeIcon.removeEventListener("click", () => {});
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccessMessage(message) {
|
||||
const toast = document.querySelector(".toast#successToast");
|
||||
(closeIcon = document.querySelector(".close-success")),
|
||||
(successMessage = document.querySelector(".successMessage")),
|
||||
(progress = document.querySelector(".progress.success"));
|
||||
let timer1, timer2;
|
||||
successMessage.textContent = message;
|
||||
toast.classList.add("active");
|
||||
progress.classList.add("active");
|
||||
timer1 = setTimeout(() => {
|
||||
toast.classList.remove("active");
|
||||
closeIcon.removeEventListener("click", () => {});
|
||||
}, 5000);
|
||||
|
||||
timer2 = setTimeout(() => {
|
||||
progress.classList.remove("active");
|
||||
}, 5300);
|
||||
|
||||
closeIcon.addEventListener("click", () => {
|
||||
toast.classList.remove("active");
|
||||
|
||||
setTimeout(() => {
|
||||
progress.classList.remove("active");
|
||||
}, 300);
|
||||
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
closeIcon.removeEventListener("click", () => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function openRestoreDBFileSelect() {
|
||||
document.getElementById('restoreDBFile').click();
|
||||
};
|
||||
|
||||
function restoreDB() {
|
||||
const input = document.getElementById('restoreDBFile');
|
||||
const file = input.files[0];
|
||||
|
||||
if (!file) {
|
||||
console.error('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
fetch('endpoints/db/import.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message)
|
||||
window.location.href = 'logout.php';
|
||||
} else {
|
||||
showErrorMessage(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Error:', error));
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
restoreFormFields();
|
||||
removeFromStorage();
|
||||
|
||||
@ -1008,8 +1008,64 @@ function setHideDisabled() {
|
||||
storeSettingsOnDB('hide_disabled', value);
|
||||
}
|
||||
|
||||
function exportToJson() {
|
||||
window.location.href = "endpoints/subscriptions/export.php";
|
||||
function backupDB() {
|
||||
const button = document.getElementById("backupDB");
|
||||
button.disabled = true;
|
||||
|
||||
fetch('endpoints/db/backup.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const link = document.createElement('a');
|
||||
const filename = data.file;
|
||||
link.href = '.tmp/' + filename;
|
||||
link.download = 'backup.zip';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
button.disabled = false;
|
||||
} else {
|
||||
showErrorMessage(data.errorMessage);
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showErrorMessage(error);
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openRestoreDBFileSelect() {
|
||||
document.getElementById('restoreDBFile').click();
|
||||
};
|
||||
|
||||
function restoreDB() {
|
||||
const input = document.getElementById('restoreDBFile');
|
||||
const file = input.files[0];
|
||||
|
||||
if (!file) {
|
||||
console.error('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
fetch('endpoints/db/restore.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message)
|
||||
window.location.href = 'logout.php';
|
||||
} else {
|
||||
showErrorMessage(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Error:', error));
|
||||
}
|
||||
|
||||
function saveCategorySorting() {
|
||||
|
||||
20
settings.php
20
settings.php
@ -681,11 +681,23 @@
|
||||
|
||||
<section class="account-section">
|
||||
<header>
|
||||
<h2><?= translate('export_subscriptions', $i18n) ?></h2>
|
||||
<h2><?= translate('backup_and_restore', $i18n) ?></h2>
|
||||
</header>
|
||||
<div>
|
||||
<input type="button" class="button thin" value="<?= translate('export_to_json', $i18n) ?>" id="exportToJson" onClick="exportToJson()"/>
|
||||
<div>
|
||||
<div class="form-group-inline">
|
||||
<div>
|
||||
<input type="button" class="button thin" value="<?= translate('backup', $i18n) ?>" id="backupDB" onClick="backupDB()"/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="button" class="secondary-button thin" value="<?= translate('restore', $i18n) ?>" id="restoreDB" onClick="openRestoreDBFileSelect()" />
|
||||
<input type="file" name="restoreDBFile" id="restoreDBFile" style="display: none;" onChange="restoreDB()" accept=".zip">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-notes">
|
||||
<p>
|
||||
<i class="fa-solid fa-circle-info"></i>
|
||||
<?= translate('restore_info', $i18n) ?>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
145
styles/login.css
145
styles/login.css
@ -90,7 +90,8 @@ select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
input[type="submit"],
|
||||
input[type="button"] {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
@ -105,6 +106,20 @@ input[type="submit"]:hover {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
input[type="button"].secondary-button,
|
||||
button.button.secondary-button {
|
||||
background-color: #FFFFFF;
|
||||
color: var(--main-color);
|
||||
border: 2px solid var(--main-color);
|
||||
}
|
||||
|
||||
input[type="button"].secondary-button:hover,
|
||||
button.button.secondary-button:hover {
|
||||
background-color: #EEEEEE;
|
||||
color: var(--hover-color);
|
||||
border-color: var(--hover-color);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 25px;
|
||||
@ -123,3 +138,131 @@ input[type="checkbox"] {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-top: 1px solid #ccc;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* TOAST MESSAGE */
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
right: 30px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #eeeeee;
|
||||
background: #fff;
|
||||
padding: 20px 35px 20px 25px;
|
||||
box-shadow: 0 6px 20px -5px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transform: translateX(calc(100% + 30px));
|
||||
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toast {
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.active {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
.toast .toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-content .toast-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 35px;
|
||||
min-width: 35px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toast-content .toast-icon.error {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.toast-content .toast-icon.success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
|
||||
.toast-content .message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.toast-content .message .text {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.toast-content .message .text.text-1 {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toast .close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.toast .close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast .progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.toast .progress:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast .progress.error:before {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.toast .progress.success:before {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.progress.active:before {
|
||||
animation: progress 5s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
100% {
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* TOAST END */
|
||||
Loading…
Reference in New Issue
Block a user