From 3ccbeee4e6fcb3a1ea1a57b7c8dd2fb9cac0ad8b Mon Sep 17 00:00:00 2001
From: Miguel Ribeiro
Date: Fri, 4 Oct 2024 15:02:27 +0200
Subject: [PATCH 1/7] Add API
---
api/subscriptions/get_monthly_cost.php | 151 +++++++++++
endpoints/user/regenerateapikey.php | 40 +++
includes/header.php | 2 +
includes/i18n/de.php | 7 +-
includes/i18n/el.php | 7 +-
includes/i18n/en.php | 7 +-
includes/i18n/es.php | 7 +-
includes/i18n/fr.php | 7 +-
includes/i18n/it.php | 7 +-
includes/i18n/jp.php | 7 +-
includes/i18n/ko.php | 7 +-
includes/i18n/pl.php | 7 +-
includes/i18n/pt.php | 7 +-
includes/i18n/pt_br.php | 7 +-
includes/i18n/ru.php | 7 +-
includes/i18n/sl.php | 7 +-
includes/i18n/sr.php | 7 +-
includes/i18n/sr_lat.php | 7 +-
includes/i18n/tr.php | 7 +-
includes/i18n/zh_cn.php | 7 +-
includes/i18n/zh_tw.php | 7 +-
migrations/000029.php | 21 ++
profile.php | 303 +++++++++++++++++++++
scripts/profile.js | 356 +++++++++++++++++++++++++
scripts/settings.js | 329 +----------------------
settings.php | 277 +------------------
styles/styles.css | 6 +-
27 files changed, 1000 insertions(+), 611 deletions(-)
create mode 100644 api/subscriptions/get_monthly_cost.php
create mode 100644 endpoints/user/regenerateapikey.php
create mode 100644 migrations/000029.php
create mode 100644 profile.php
create mode 100644 scripts/profile.js
diff --git a/api/subscriptions/get_monthly_cost.php b/api/subscriptions/get_monthly_cost.php
new file mode 100644
index 000000000..e0e7f4bc5
--- /dev/null
+++ b/api/subscriptions/get_monthly_cost.php
@@ -0,0 +1,151 @@
+ false,
+ "title" => "Missing parameters"
+ ];
+ echo json_encode($response);
+ exit;
+ }
+
+ $month = $_REQUEST['month'];
+ $year = $_REQUEST['year'];
+ $apiKey = $_REQUEST['api_key'];
+
+ $sql = "SELECT * FROM user WHERE api_key = :apiKey";
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':apiKey', $apiKey);
+ $result = $stmt->execute();
+ $user = $result->fetchArray(SQLITE3_ASSOC);
+
+ $sql = "SELECT * FROM last_exchange_update";
+ $result = $db->query($sql);
+ $lastExchangeUpdate = $result->fetchArray(SQLITE3_ASSOC);
+
+ $userId = $user['id'];
+ $userCurrencyId = $user['main_currency'];
+ $needsCurrencyConversion = false;
+ $canConvertCurrency = empty($lastExchangeUpdate['date']) ? false : true;
+
+ $sql = "SELECT * FROM currencies WHERE id = :currencyId";
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':currencyId', $userCurrencyId);
+ $result = $stmt->execute();
+ $currency = $result->fetchArray(SQLITE3_ASSOC);
+ $currency_code = $currency['code'];
+ $currency_symbol = $currency['symbol'];
+
+
+ $title = date('F Y', strtotime($year . '-' . $month . '-01'));
+ $monthlyCost = 0;
+ $notes = [];
+
+ $sql = "SELECT * FROM subscriptions WHERE user_id = :userId AND inactive = 0";
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':userId', $userId);
+ $result = $stmt->execute();
+ $subscriptions = [];
+ while ($subscription = $result->fetchArray(SQLITE3_ASSOC)) {
+ $subscriptions[] = $subscription;
+ if ($subscription['currency_id'] !== $userCurrencyId) {
+ $needsCurrencyConversion = true;
+ }
+ }
+
+ if ($needsCurrencyConversion) {
+ if (!$canConvertCurrency) {
+ $notes[] = "You are using multiple currencies, but the exchange rates have not been updated yet. Please check your Fixer API Key.";
+ } else {
+ $sql = "SELECT * FROM currencies WHERE user_id = :userId";
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':userId', $userId);
+ $result = $stmt->execute();
+ $currencies = [];
+ while ($currency = $result->fetchArray(SQLITE3_ASSOC)) {
+ $currencies[$currency['id']] = $currency['rate'];
+ }
+ }
+ }
+
+ // Calculate the monthly cost based on the next_payment_date, payment cycle, and payment frequency
+ foreach ($subscriptions as $subscription) {
+ $nextPaymentDate = strtotime($subscription['next_payment']);
+ $cycle = $subscription['cycle']; // Integer from 1 to 4
+ $frequency = $subscription['frequency'];
+
+ // Determine the strtotime increment string based on cycle
+ switch ($cycle) {
+ case 1: // Days
+ $incrementString = "+{$frequency} days";
+ break;
+ case 2: // Weeks
+ $incrementString = "+{$frequency} weeks";
+ break;
+ case 3: // Months
+ $incrementString = "+{$frequency} months";
+ break;
+ case 4: // Years
+ $incrementString = "+{$frequency} years";
+ break;
+ default:
+ $incrementString = "+{$frequency} months"; // Default case, if needed
+ }
+
+ // Calculate the start of the month
+ $startOfMonth = strtotime($year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01');
+
+ // Find the first payment date of the month by moving backwards
+ $startDate = $nextPaymentDate;
+ while ($startDate > $startOfMonth) {
+ $startDate = strtotime("-" . $incrementString, $startDate);
+ }
+
+ // Calculate the monthly cost
+ for ($date = $startDate; $date <= strtotime("+1 month", $startOfMonth); $date = strtotime($incrementString, $date)) {
+ if (date('Y-m', $date) == $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT)) {
+ $price = $subscription['price'];
+ if ($userCurrencyId !== $subscription['currency_id']) {
+ $price *= $currencies[$userCurrencyId] / $currencies[$subscription['currency_id']];
+ }
+ $monthlyCost += $price;
+ }
+ }
+ }
+
+ $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
+ $localizedMonthlyCost = $formatter->formatCurrency($monthlyCost, $currency_code);
+
+ echo json_encode([
+ 'success' => true,
+ 'title' => $title,
+ 'monthly_cost' => number_format($monthlyCost, 2),
+ 'localized_monthly_cost' => $localizedMonthlyCost,
+ 'currency_code' => $currency_code,
+ 'currency_symbol' => $currency_symbol,
+ 'notes' => $notes
+ ], JSON_UNESCAPED_UNICODE);
+
+}
+?>
\ No newline at end of file
diff --git a/endpoints/user/regenerateapikey.php b/endpoints/user/regenerateapikey.php
new file mode 100644
index 000000000..3034d12d3
--- /dev/null
+++ b/endpoints/user/regenerateapikey.php
@@ -0,0 +1,40 @@
+ false,
+ "message" => translate('session_expired', $i18n)
+ ]));
+}
+
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+ $postData = file_get_contents("php://input");
+ $data = json_decode($postData, true);
+
+ $apiKey = bin2hex(random_bytes(32));
+
+ $sql = "UPDATE user SET api_key = :apiKey WHERE id = :userId";
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':apiKey', $apiKey, SQLITE3_TEXT);
+ $stmt->bindValue(':userId', $userId, SQLITE3_TEXT);
+ $result = $stmt->execute();
+
+ if ($result) {
+ $response = [
+ "success" => true,
+ "message" => translate('user_details_saved', $i18n),
+ "apiKey" => $apiKey
+ ];
+ echo json_encode($response);
+ } else {
+ $response = [
+ "success" => false,
+ "message" => translate('error_updating_user_data', $i18n)
+ ];
+ echo json_encode($response);
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/includes/header.php b/includes/header.php
index d2186556a..e140a6827 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -165,6 +165,8 @@ function hex2rgb($hex)
= $username ?>
+
= translate('profile', $i18n) ?>
= translate('subscriptions', $i18n) ?>
"Passwort zurücksetzen E-Mail wurde gesendet. Bitte überprüfen Sie Ihr Postfach.",
'password_reset_successful' => "Passwort erfolgreich zurückgesetzt",
// Header
+ 'profile' => "Profil",
'subscriptions' => "Abonnements",
'stats' => "Statistiken",
'settings' => "Einstellungen",
@@ -116,7 +117,7 @@
'the_author' => "Der Autor",
'icons' => "Icons",
'payment_icons' => "Zahlungsweisen Icons",
- // Settings page
+ // Profile page
'upload_avatar' => "Avatar hochladen",
'file_type_error' => "Dateityp nicht unterstützt",
'user_details' => "Benutzerdetails",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Speichern Sie diese Codes an einem sicheren Ort. Sie können sie verwenden, wenn Sie keinen Zugriff auf Ihre Authentifizierungs-App haben.",
"disable_two_factor_authentication" => "Zwei-Faktor-Authentifizierung deaktivieren",
"totp_code" => "TOTP-Code",
+ "api_key" => "API Key",
+ "regenerate" => "Neu generieren",
+ "api_key_info" => "Der API-Schlüssel wird für die Integration von Drittanbieter-Apps verwendet. Wenn Sie Ihren Schlüssel neu generieren, müssen Sie ihn in allen Apps aktualisieren, die ihn verwenden.",
+ // Settings page
"monthly_budget" => "Monatliches Budget",
"budget_info" => "Das monatliche Budget wird für die Berechnung der Statistiken verwendet.",
"household" => "Haushalt",
diff --git a/includes/i18n/el.php b/includes/i18n/el.php
index cec840c45..530627cbb 100644
--- a/includes/i18n/el.php
+++ b/includes/i18n/el.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Ένα email με οδηγίες για την επαναφορά του κωδικού πρόσβασης σας έχει σταλεί. Παρακαλώ ελέγξτε το email σας.",
'password_reset_successful' => "Επιτυχής επαναφορά κωδικού πρόσβασης",
// Header
+ 'profile' => "Προφίλ",
'subscriptions' => "Συνδρομές",
'stats' => "Στατιστικές",
'settings' => "Ρυθμίσεις",
@@ -116,7 +117,7 @@
'the_author' => "Προγραμματιστής",
'icons' => "Εικονίδια",
'payment_icons' => "Εικονίδια Payment",
- // Settings page
+ // Profile page
'upload_avatar' => "μεταφόρτωση άβαταρ",
'file_type_error' => "Το αρχείο πρέπει να είναι τύπου jpg, jpeg, png, webp ή gif",
'user_details' => "Λεπτομέρειες χρήστη",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Αποθηκεύστε αυτούς τους κωδικούς ανάκτησης σε ένα ασφαλές μέρος. Θα χρειαστείτε έναν από αυτούς τους κωδικούς ανάκτησης για να αποκτήσετε πρόσβαση στο λογαριασμό σας σε περίπτωση που χάσετε τη συσκευή σας.",
"disable_two_factor_authentication" => "Απενεργοποίηση διπλής πιστοποίησης",
"totp_code" => "Κωδικός TOTP",
+ "api_key" => "API κλειδί",
+ "regenerate" => "Επαναδημιουργία",
+ "api_key_info" => "Το API κλειδί χρησιμοποιείται για την επικοινωνία με το API του Wallos. Μην αποκαλύπτετε το API κλειδί σας σε κανέναν.",
+ // Settings page
"monthly_budget" => "Μηνιαίος προϋπολογισμός",
"budget_info" => "Ο μηνιαίος προϋπολογισμός χρησιμοποιείται για τον υπολογισμό των στατιστικών",
"household" => "Νοικοκυριό",
diff --git a/includes/i18n/en.php b/includes/i18n/en.php
index 056bd4c2c..7900289f7 100644
--- a/includes/i18n/en.php
+++ b/includes/i18n/en.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Reset email sent. Please check your email.",
'password_reset_successful' => "Password reset successful",
// Header
+ 'profile' => "Profile",
'subscriptions' => "Subscriptions",
'stats' => "Statistics",
'settings' => "Settings",
@@ -116,7 +117,7 @@
'the_author' => "The author",
'icons' => "Icons",
'payment_icons' => "Payment Icons",
- // Settings page
+ // Profile page
'upload_avatar' => "Upload Avatar",
'file_type_error' => "The file type supplied is not supported.",
'user_details' => "User Details",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "These codes can be used to login if you lose access to your authenticator app.",
"disable_two_factor_authentication" => "Disable Two Factor Authentication",
"totp_code" => "TOTP Code",
+ "api_key" => "API Key",
+ "regenerate" => "Regenerate",
+ "api_key_info" => "The API key is used to access the API. Keep it secret.",
+ // Settings page
"monthly_budget" => "Monthly Budget",
"budget_info" => "Monthly budget is used to calculate statistics",
"household" => "Household",
diff --git a/includes/i18n/es.php b/includes/i18n/es.php
index 09b479fbd..ae7b0b94e 100644
--- a/includes/i18n/es.php
+++ b/includes/i18n/es.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Se ha enviado un correo electrónico con instrucciones para restablecer la contraseña. Por favor, compruebe su correo electrónico.",
'password_reset_successful' => "Contraseña restablecida con éxito",
// Header
+ 'profile' => "Perfil",
'subscriptions' => "Suscripciones",
'stats' => "Estadísticas",
'settings' => "Configuración",
@@ -116,7 +117,7 @@
'the_author' => "El autor",
'icons' => "Iconos",
'payment_icons' => "Iconos de Pago",
- // Settings page
+ // Profile page
'upload_avatar' => "Subir avatar",
'file_type_error' => "El archivo debe ser una imagen en formato PNG, JPG, WEBP o SVG",
'user_details' => "Detalles del Usuario",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Guarda estos códigos en un lugar seguro. Puedes usarlos si pierdes acceso a tu aplicación de autenticación.",
"disable_two_factor_authentication" => "Desactivar Autenticación de Dos Factores",
"totp_code" => "Código TOTP",
+ "api_key" => "Clave API",
+ "regenerate" => "Regenerar",
+ "api_key_info" => "La clave API se utiliza para acceder a la API de Wallos. No compartas esta clave con nadie.",
+ // Settings page
"monthly_budget" => "Presupuesto Mensual",
"budget_info" => "El presupuesto mensual se utiliza para calcular las estadísticas. Si no deseas utilizar esta función, déjalo en 0.",
"household" => "Hogar",
diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php
index 163339c0e..8c84ecb8e 100644
--- a/includes/i18n/fr.php
+++ b/includes/i18n/fr.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Un courriel a été envoyé à l'adresse fournie. Vérifiez votre boîte de réception.",
'password_reset_successful' => "Réinitialisation du mot de passe réussie",
// En-tête
+ 'profile' => "Profil",
'subscriptions' => "Abonnements",
'stats' => "Statistiques",
'settings' => "Paramètres",
@@ -116,7 +117,7 @@
'the_author' => "L'auteur",
'icons' => "Icônes",
'payment_icons' => "Icônes de paiement",
- // Page de paramètres
+ // Page de profil
'upload_avatar' => "Télécharger un Avatar",
'file_type_error' => "Le type de fichier n'est pas pris en charge",
'user_details' => "Détails de l'utilisateur",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Conservez ces codes en lieu sûr. Vous ne pourrez pas les récupérer plus tard.",
"disable_two_factor_authentication" => "Désactiver l'authentification à deux facteurs",
"totp_code" => "Code TOTP",
+ "api_key" => "Clé API",
+ "regenerate" => "Régénérer",
+ "api_key_info" => "La clé API est utilisée pour les applications tierces et les intégrations. Ne la partagez pas.",
+ // Page de paramètres
"monthly_budget" => "Budget mensuel",
"budget_info" => "Le budget mensuel est utilisé pour calculer les statistiques. Laissez vide pour désactiver.",
"household" => "Ménage",
diff --git a/includes/i18n/it.php b/includes/i18n/it.php
index bf7191605..153f5034a 100644
--- a/includes/i18n/it.php
+++ b/includes/i18n/it.php
@@ -31,6 +31,7 @@
'password_reset_successful' => "La password è stata reimpostata con successo",
// Header
+ 'profile' => 'Profilo',
'subscriptions' => 'Abbonamenti',
'stats' => 'Statistiche',
'settings' => 'Impostazioni',
@@ -124,7 +125,7 @@
'icons' => 'Icone',
'payment_icons' => 'Icone di pagamento',
- // Settings
+ // Profile
'upload_avatar' => 'Carica avatar',
'file_type_error' => 'Il tipo di file fornito non è supportato.',
'user_details' => 'Dettagli utente',
@@ -140,6 +141,10 @@
"totp_backup_codes_info" => "I codici di backup possono essere utilizzati per accedere al tuo account se non hai accesso al tuo dispositivo di autenticazione a due fattori.",
"disable_two_factor_authentication" => "Disabilita l'autenticazione a due fattori",
"totp_code" => "Codice TOTP",
+ "api_key" => "Chiave API",
+ "regenerate" => "Rigenera",
+ "api_key_info" => "La chiave API viene utilizzata per accedere ai dati tramite l'API di Wallos. Non condividere la tua chiave API con nessuno.",
+ // Settings
"monthly_budget" => "Budget mensile",
"budget_info" => "Il budget mensile viene utilizzato per calcolare le statistiche. Se non si desidera utilizzare questa funzionalità, impostare il budget su 0.",
'household' => 'Nucleo familiare',
diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php
index e176d84f9..fe151f4b8 100644
--- a/includes/i18n/jp.php
+++ b/includes/i18n/jp.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "パスワードリセットリンクが送信されました。メールを確認してください。",
'password_reset_successful' => "パスワードリセットに成功",
// Header
+ 'profile' => "プロフィール",
'subscriptions' => "定期購入",
'stats' => "統計",
'settings' => "設定",
@@ -116,7 +117,7 @@
'the_author' => "著者",
'icons' => "アイコン",
'payment_icons' => "支払いアイコン",
- // Settings page
+ // Profile page
'upload_avatar' => "アバターをアップロードする",
'file_type_error' => "ファイルタイプが許可されていません",
'user_details' => "ユーザー詳細",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "これらのコードは、2要素認証アプリが利用できない場合に使用します。コードは一度しか表示されません。",
"disable_two_factor_authentication" => "二要素認証を無効にする",
"totp_code" => "TOTPコード",
+ "api_key" => "APIキー",
+ "regenerate" => "再生成",
+ "api_key_info" => "APIキーは、WallosのAPIを使用するために必要です。APIキーを再生成すると、以前のキーは無効になります。",
+ // Settings page
"monthly_budget" => "月間予算",
"budget_info" => "予算を設定すると、統計ページで予算と実際の支出を比較できます。",
"household" => "世帯",
diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php
index ddf6728d7..dffb86c57 100644
--- a/includes/i18n/ko.php
+++ b/includes/i18n/ko.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "비밀번호 재설정 이메일이 전송되었습니다. 이메일을 확인해 주세요.",
'password_reset_successful' => "비밀번호 재설정 성공",
// Header
+ 'profile' => "프로필",
'subscriptions' => "구독",
'stats' => "통계",
'settings' => "설정",
@@ -116,7 +117,7 @@
'the_author' => "제작자",
'icons' => "아이콘",
'payment_icons' => "지불 방식 아이콘",
- // Settings page
+ // Profile page
'upload_avatar' => "아바타 업로드",
'file_type_error' => "제공된 파일이 지원하지 않는 타입입니다.",
'user_details' => "유저 상세",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "이 코드는 계정에 대한 백업 코드입니다. 이 코드를 안전한 곳에 보관하세요. 이 코드는 한 번만 사용할 수 있습니다.",
"disable_two_factor_authentication" => "2단계 인증 비활성화",
"totp_code" => "TOTP 코드",
+ "api_key" => "API 키",
+ "regenerate" => "재생성",
+ "api_key_info" => "API 키는 외부 애플리케이션과 통신할 때 사용됩니다. API 키를 재생성하면 이전 키는 더 이상 유효하지 않습니다.",
+ // Settings page
"monthly_budget" => "월간 예산",
"budget_info" => "예산을 설정하면 통계 페이지에서 예산과 실제 지출을 비교할 수 있습니다.",
"household" => "가구",
diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php
index b85485689..e70f69c42 100644
--- a/includes/i18n/pl.php
+++ b/includes/i18n/pl.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Link do zresetowania hasła został wysłany na Twój adres e-mail",
'password_reset_successful' => "Hasło zostało zresetowane pomyślnie",
// Header
+ 'profile' => "Profil",
'subscriptions' => "Subskrypcje",
'stats' => "Statystyki",
'settings' => "Ustawienia",
@@ -116,7 +117,7 @@
'the_author' => "Autor",
'icons' => "Ikony",
'payment_icons' => "Ikony płatności",
- // Settings page
+ // Profile page
'upload_avatar' => "Prześlij awatar",
'file_type_error' => "Podany typ pliku nie jest obsługiwany.",
'user_details' => "Szczegóły użytkownika",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Kody zapasowe są jednorazowe i można je użyć do zalogowania się, jeśli nie masz dostępu do aplikacji uwierzytelniającej.",
"disable_two_factor_authentication" => "Wyłącz uwierzytelnianie dwuskładnikowe",
"totp_code" => "Kod TOTP",
+ "api_key" => "Klucz API",
+ "regenerate" => "Wygeneruj ponownie",
+ "api_key_info" => "Klucz API jest używany do integracji z innymi aplikacjami. Nie udostępniaj go nikomu.",
+ // Settings page
"monthly_budget" => "Miesięczny budżet",
"budget_info" => "Jeśli ustawisz budżet, zobaczysz pasek postępu na stronie głównej.",
"household" => "Gospodarstwo domowe",
diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php
index c817e7acf..e62bf3a5e 100644
--- a/includes/i18n/pt.php
+++ b/includes/i18n/pt.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Pedido de reposição de password enviado. Verifique o seu email.",
'password_reset_successful' => "Password reposta com sucesso",
// Header
+ 'profile' => "Perfil",
'subscriptions' => "Subscrições",
'stats' => "Estatísticas",
'settings' => "Definições",
@@ -116,7 +117,7 @@
'the_author' => "O Autor",
'icons' => "Ícones",
'payment_icons' => "Ícones de Pagamentos",
- // Settings page
+ // Profile page
'upload_avatar' => "Enviar avatar",
'file_type_error' => "Tipo de ficheiro não permitido",
'user_details' => "Detalhes do utilizador",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Guarde estes códigos num local seguro. Pode usá-los para aceder à sua conta se perder o acesso ao seu dispositivo de autenticação.",
"disable_two_factor_authentication" => "Desactivar autenticação de dois factores",
"totp_code" => "Código TOTP",
+ "api_key" => "API Key",
+ "regenerate" => "Regenerar",
+ "api_key_info" => "A sua API Key é usada para aceder à API do Wallos. Não a partilhe com ninguém.",
+ // Settings page
"monthly_budget" => "Orçamento Mensal",
"budget_info" => "Ao definir um orçamento pode comparar com os gastos reais na página de estatísticas.",
"household" => "Agregado",
diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php
index 0b9501939..0ae5399d8 100644
--- a/includes/i18n/pt_br.php
+++ b/includes/i18n/pt_br.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Redefinição de senha enviada. Por favor, verifique seu email",
'password_reset_successful' => "Senha redefinida com sucesso",
// Header
+ 'profile' => "Perfil",
'subscriptions' => "Assinaturas",
'stats' => "Estatísticas",
'settings' => "Configurações",
@@ -116,7 +117,7 @@
'the_author' => "O autor",
'icons' => "Ícones",
'payment_icons' => "Ícones de pagamento",
- // Settings page
+ // Profile page
'upload_avatar' => "Carregar avatar",
'file_type_error' => "Tipo de arquivo não permitido",
'user_details' => "Informações do Usuário",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Guarde esses códigos em um lugar seguro. Eles podem ser usados para acessar sua conta se você perder o acesso ao aplicativo de autenticação.",
"disable_two_factor_authentication" => "Desativar autenticação de dois fatores",
"totp_code" => "Código TOTP",
+ "api_key" => "Chave da API",
+ "regenerate" => "Regenerar",
+ "api_key_info" => "A chave da API é usada para acessar a API do Wallos. Não compartilhe sua chave com ninguém.",
+ // Settings page
"monthly_budget" => "Orçamento mensal",
"budget_info" => "O orçamento mensal é usado para calcular estatísticas",
"household" => "Membros",
diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php
index b0af9db5e..7a99a0f9e 100644
--- a/includes/i18n/ru.php
+++ b/includes/i18n/ru.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Ссылка для сброса пароля отправлена на вашу электронную почту",
'password_reset_successful' => "Пароль успешно сброшен",
// Header
+ 'profile' => "Профиль",
'subscriptions' => "Подписки",
'stats' => "Статистика",
'settings' => "Настройки",
@@ -116,7 +117,7 @@
'the_author' => "Автор",
'icons' => "Значки",
'payment_icons' => "Значки способов оплаты",
- // Settings page
+ // Profile page
'upload_avatar' => "Загрузить аватар",
'file_type_error' => "Указанный тип файла не поддерживается.",
'user_details' => "Данные пользователя",
@@ -133,6 +134,10 @@
"disable_two_factor_authentication" => "Отключить двухфакторную аутентификацию",
"totp_code" => "Код TOTP",
"monthly_budget" => "Ежемесячный бюджет",
+ "api_key" => "API ключ",
+ "regenerate" => "Сгенерировать",
+ "api_key_info" => "API ключ используется для доступа к вашим данным через API. Не передавайте его третьим лицам.",
+ // Settings page
"budget_info" => "Если вы укажете бюджет, Wallos будет отображать вашу текущую стоимость подписок в сравнении с вашим бюджетом.",
"household" => "Семья",
"save_member" => "Сохранить члена семьи",
diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php
index 0d1773414..241b1b7dd 100644
--- a/includes/i18n/sl.php
+++ b/includes/i18n/sl.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "E-pošta ponastavitev gesla je bila poslana. Prosim, preglejte vašo e-pošto.",
'password_reset_successful' => "Ponastavitev gesla je uspela",
// Header
+ 'profile' => "Profil",
'subscriptions' => "Naročnine",
'stats' => "Statistika",
'settings' => "Nastavitve",
@@ -116,7 +117,7 @@
'the_author' => "Avtor",
'icons' => "Ikone",
'payment_icons' => "Ikone plačil",
- // Settings page
+ // Profile page
'upload_avatar' => "Naloži avatar",
'file_type_error' => "Vrsta datoteke ni podprta.",
'user_details' => "Podrobnosti o uporabniku",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Shranite te rezervne kode na varno mesto. Uporabite jih lahko, če izgubite dostop do svoje aplikacije za preverjanje pristnosti.",
"disable_two_factor_authentication" => "Onemogoči dvostopenjsko preverjanje pristnosti",
"totp_code" => "TOTP koda",
+ "api_key" => "API ključ",
+ "regenerate" => "Ponovno generiraj",
+ "api_key_info" => "API ključ se uporablja za dostop do vaših podatkov prek API-ja. Če mislite, da je vaš ključ kompromitiran, ga lahko ponovno generirate.",
+ // Settings page
"monthly_budget" => "Mesečni proračun",
"budget_info" => "Mesečni proračun se uporablja za izračun statistike",
"household" => "Gospodinjstvo",
diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php
index abc6cc526..cb99cb6f9 100644
--- a/includes/i18n/sr.php
+++ b/includes/i18n/sr.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Ресетовање лозинке је послато на вашу е-пошту",
'password_reset_successful' => "Ресетовање лозинке је успешно",
// Header
+ 'profile' => "Профил",
'subscriptions' => "Претплате",
'stats' => "Статистике",
'settings' => "Подешавања",
@@ -116,7 +117,7 @@
'the_author' => "Аутор",
'icons' => "Иконе",
'payment_icons' => "Иконе плаћања",
- // Страница са подешавањима
+ // Profile page
'upload_avatar' => "Постави аватар",
'file_type_error' => "Датотека није у подржаном формату.",
'user_details' => "Кориснички детаљи",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Сачувајте ове кодове на безбедно место. Користићете их када изгубите приступ апликацији за аутентификацију.",
"disable_two_factor_authentication" => "Онемогући двофакторску аутентикацију",
"totp_code" => "ТОТП код",
+ "api_key" => "API кључ",
+ "regenerate" => "Генериши",
+ "api_key_info" => "API кључ је потребан за коришћење API-ја за приступ вашим подацима. Не делите овај кључ са другима.",
+ // Страница са подешавањима
"monthly_budget" => "Месечни буџет",
"budget_info" => "Унесите месечни буџет да бисте видели препоручену максималну цену претплате на почетној страници.",
"household" => "Домаћинство",
diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php
index be9101245..50fc4dfdf 100644
--- a/includes/i18n/sr_lat.php
+++ b/includes/i18n/sr_lat.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Poslali smo vam e-poštu sa uputstvima za resetovanje lozinke",
'password_reset_successful' => "Lozinka uspešno resetovana",
// Header
+ 'profile' => "Profil",
'subscriptions' => "Pretplate",
'stats' => "Statistike",
'settings' => "Podešavanja",
@@ -116,7 +117,7 @@
'the_author' => "Autor",
'icons' => "Ikone",
'payment_icons' => "Ikone za plaćanje",
- // Stranica sa podešavanjima
+ // Stranica sa profilom
'upload_avatar' => "Učitaj avatar",
'file_type_error' => "Tip datoteke koji ste priložili nije podržan.",
'user_details' => "Detalji korisnika",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Ovo su vaši rezervni kodovi za dvofaktorsku autentifikaciju. Sačuvajte ih na sigurnom mestu.",
"disable_two_factor_authentication" => "Onemogući dvofaktorsku autentifikaciju",
"totp_code" => "Kod za dvofaktorsku autentifikaciju",
+ "api_key" => "API ključ",
+ "regenerate" => "Regeneriši",
+ "api_key_info" => "API ključ se koristi za pristup Wallos API-ju. Ako mislite da je vaš ključ kompromitovan, možete ga regenerisati.",
+ // Stranica sa podešavanjima
"monthly_budget" => "Mesečni budžet",
"budget_info" => "Ovo je vaš mesečni budžet za sve pretplate. Ovo je samo informativno i ne ograničava vas.",
"household" => "Domaćinstvo",
diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php
index 8a47f8c2d..b89f0e963 100644
--- a/includes/i18n/tr.php
+++ b/includes/i18n/tr.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "Şifre sıfırlama bağlantısı e-posta adresinize gönderildi. Lütfen e-postanızı kontrol edin.",
'password_reset_successful' => "Şifre sıfırlama başarılı",
// Header
+ 'profile' => "Profil",
'subscriptions' => "Abonelikler",
'stats' => "İstatistikler",
'settings' => "Ayarlar",
@@ -116,7 +117,7 @@
'the_author' => "Yazar",
'icons' => "İkonlar",
'payment_icons' => "Ödeme İkonları",
- // Settings page
+ // Profile page
'upload_avatar' => "Avatarı yükle",
'file_type_error' => "Dosya türü izin verilmiyor",
'user_details' => "Kullanıcı Detayları",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "Yedek kodları güvenli bir yerde saklayın. Her biri yalnızca bir kez kullanılabilir.",
"disable_two_factor_authentication" => "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak",
"totp_code" => "TOTP Kodu",
+ "api_key" => "API Anahtarı",
+ "regenerate" => "Yeniden Oluştur",
+ "api_key_info" => "API Anahtarı, Wallos'un API'sine erişmek için kullanılır. Bu anahtarı kimseyle paylaşmayın.",
+ // Settings page
"monthly_budget" => "Aylık Bütçe",
"budget_info" => "Bir bütçe belirlemek, istatistik sayfasında bütçe ve gerçek harcamaları karşılaştırmanıza olanak tanır.",
"household" => "Hane",
diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php
index 77cd39570..d6255dfc7 100644
--- a/includes/i18n/zh_cn.php
+++ b/includes/i18n/zh_cn.php
@@ -31,6 +31,7 @@
'password_reset_successful' => "密码重置成功",
// 页眉
+ 'profile' => "个人资料",
'subscriptions' => "订阅",
'stats' => "统计",
'settings' => "设置",
@@ -124,7 +125,7 @@
'icons' => "图标",
'payment_icons' => "支付图标",
- // 设置页面
+ // Profile Page
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "用户详情",
@@ -140,6 +141,10 @@
"totp_backup_codes_info" => "请务必保存这些备份代码。如果您丢失了双因素身份验证设备,您将需要这些备份代码来登录。",
"disable_two_factor_authentication" => "禁用双因素身份验证",
"totp_code" => "TOTP 代码",
+ "api_key" => "API 密钥",
+ "regenerate" => "重新生成",
+ "api_key_info" => "API 密钥用于与 Wallos API 通信。请勿将此密钥分享给任何人。",
+ // 设置页面
"monthly_budget" => "每月预算",
"budget_info" => "设置预算后,您可以在统计页面上比较预算和实际支出。",
"household" => "家庭",
diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php
index b93940875..9ff75a31b 100644
--- a/includes/i18n/zh_tw.php
+++ b/includes/i18n/zh_tw.php
@@ -28,6 +28,7 @@
'reset_sent_check_email' => "重設密碼的電子郵件已發送,請檢查您的電子郵件",
'password_reset_successful' => "密碼重設成功",
// 頁首
+ 'profile' => "個人資料",
'subscriptions' => "訂閱",
'stats' => "統計",
'settings' => "設定",
@@ -116,7 +117,7 @@
'the_author' => "作者",
'icons' => "圖示",
'payment_icons' => "付款圖示",
- // 設定頁面
+ // Profile page
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "使用者詳細資訊",
@@ -132,6 +133,10 @@
"totp_backup_codes_info" => "請妥善保管這些代碼。當您無法使用雙因素驗證應用程式時,您可以使用這些代碼來登入。",
"disable_two_factor_authentication" => "停用雙因素驗證",
"totp_code" => "TOTP 驗證碼",
+ "api_key" => "API 金鑰",
+ "regenerate" => "重新生成",
+ "api_key_info" => "API 金鑰用於與 Wallos API 進行通信。請勿將此金鑰分享給任何人。",
+ // 設定頁面
"monthly_budget" => "每月預算",
"budget_info" => "設定預算後,您可以在統計頁面上比較預算和實際支出。",
"household" => "家庭",
diff --git a/migrations/000029.php b/migrations/000029.php
new file mode 100644
index 000000000..658971dd3
--- /dev/null
+++ b/migrations/000029.php
@@ -0,0 +1,21 @@
+query("SELECT * FROM pragma_table_info('user') where name='api_key'");
+$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;
+
+if ($columnRequired) {
+ $db->exec('ALTER TABLE user ADD COLUMN api_key TEXT');
+}
+
+/** @noinspection PhpUndefinedVariableInspection */
+$users = $db->query('SELECT * FROM user');
+while ($user = $users->fetchArray(SQLITE3_ASSOC)) {
+ if (empty($user['api_key'])) {
+ $apiKey = bin2hex(random_bytes(32));
+ $db->exec('UPDATE user SET api_key = "' . $apiKey . '" WHERE id = ' . $user['id']);
+ }
+}
diff --git a/profile.php b/profile.php
new file mode 100644
index 000000000..b15199458
--- /dev/null
+++ b/profile.php
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+ = translate('user_details', $i18n) ?>
+
+
+
+
+ prepare($sql);
+ $result = $stmt->execute();
+ $row = $result->fetchArray(SQLITE3_ASSOC);
+ $loginDisabled = $row['login_disabled'];
+
+ $showTotpSection = true;
+ if ($loginDisabled && !$userData['totp_enabled']) {
+ $showTotpSection = false;
+ }
+
+ if ($showTotpSection) {
+ ?>
+
+
+ = translate("two_factor_authentication", $i18n) ?>
+
+
+
+
+
+
+
+ = translate('api_key', $i18n) ?>
+
+
+
+
+
+
+
+
+ = translate('api_key_info', $i18n) ?>
+
+
+
+
+
+
+
+ = translate('account', $i18n) ?>
+
+
+
+
+
= translate('danger_zone', $i18n) ?>
+
+
+
+
+
+
+ = translate('delete_account_info', $i18n) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/profile.js b/scripts/profile.js
new file mode 100644
index 000000000..5ca60ba27
--- /dev/null
+++ b/scripts/profile.js
@@ -0,0 +1,356 @@
+document.addEventListener('DOMContentLoaded', function () {
+
+ document.getElementById("userForm").addEventListener("submit", function (event) {
+ event.preventDefault();
+ document.getElementById("userSubmit").disabled = true;
+ const formData = new FormData(event.target);
+ fetch("endpoints/user/save_user.php", {
+ method: "POST",
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ document.getElementById("avatar").src = document.getElementById("avatarImg").src;
+ var newUsername = document.getElementById("username").value;
+ document.getElementById("user").textContent = newUsername;
+ showSuccessMessage(data.message);
+ if (data.reload) {
+ location.reload();
+ }
+ } else {
+ showErrorMessage(data.errorMessage);
+ }
+ document.getElementById("userSubmit").disabled = false;
+ })
+ .catch(error => {
+ showErrorMessage(translate('unknown_error'));
+ });
+ });
+
+});
+
+function toggleAvatarSelect() {
+ var avatarSelect = document.getElementById("avatarSelect");
+ if (avatarSelect.classList.contains("is-open")) {
+ avatarSelect.classList.remove("is-open");
+ } else {
+ avatarSelect.classList.add("is-open");
+ }
+}
+
+function closeAvatarSelect() {
+ var avatarSelect = document.getElementById("avatarSelect");
+ avatarSelect.classList.remove("is-open");
+}
+
+document.querySelectorAll('.avatar-option').forEach((avatar) => {
+ avatar.addEventListener("click", () => {
+ changeAvatar(avatar.src);
+ document.getElementById('avatarUser').value = avatar.getAttribute('data-src');
+ closeAvatarSelect();
+ })
+});
+
+function changeAvatar(src) {
+ document.getElementById("avatarImg").src = src;
+}
+
+function successfulUpload(field, msg) {
+ var reader = new FileReader();
+
+ if (field.files.length === 0) {
+ return;
+ }
+
+ if (!['image/jpeg', 'image/png', 'image/gif', 'image/jtif', 'image/webp'].includes(field.files[0]['type'])) {
+ showErrorMessage(msg);
+ return;
+ }
+
+ reader.onload = function () {
+ changeAvatar(reader.result);
+ };
+
+ reader.readAsDataURL(field.files[0]);
+ closeAvatarSelect();
+}
+
+function deleteAvatar(path) {
+ fetch('/endpoints/user/delete_avatar.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ avatar: path }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ var avatarContainer = document.querySelector(`.avatar-container[data-src="${path}"]`);
+ if (avatarContainer) {
+ avatarContainer.remove();
+ }
+ showSuccessMessage();
+ } else {
+ showErrorMessage();
+ }
+ })
+ .catch((error) => {
+ console.error('Error:', error);
+ });
+}
+
+function enableTotp() {
+ const totpSecret = document.querySelector('#totp-secret');
+ const totpSecretCode = document.querySelector('#totp-secret-code');
+ const qrCode = document.getElementById('totp-qr-code');
+ totpSecret.value = '';
+ totpSecretCode.textContent = '';
+ qrCode.innerHTML = '';
+
+ fetch('endpoints/user/enable_totp.php?generate=true', {
+ method: 'GET'
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ totpSecret.value = data.secret;
+ totpSecretCode.textContent = data.secret;
+ new QRCode(qrCode, data.qrCodeUrl);
+
+ openTotpPopup();
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch(error => {
+ showErrorMessage(error);
+ });
+}
+
+function openTotpPopup() {
+ const enableTotpButton = document.getElementById('enableTotp');
+ enableTotpButton.disabled = true;
+
+ const totpPopup = document.getElementById('totp-popup');
+ totpPopup.classList.add('is-open');
+}
+
+function closeTotpPopup() {
+ const enableTotpButton = document.getElementById('enableTotp');
+ enableTotpButton.disabled = false;
+ const totpPopup = document.getElementById('totp-popup');
+ totpPopup.classList.remove('is-open');
+
+ const totpBackupCodes = document.getElementById('totp-backup-codes');
+ if (!totpBackupCodes.classList.contains('hide')) {
+ location.reload();
+ }
+}
+
+function submitTotp() {
+ const totpCode = document.getElementById('totp').value;
+ const totpSecret = document.getElementById('totp-secret').value;
+
+ fetch('endpoints/user/enable_totp.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ totpCode: totpCode, totpSecret: totpSecret }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ showSuccessMessage(data.message);
+ const backupCodes = data.backupCodes;
+ const backupCodesList = document.getElementById('backup-codes');
+ backupCodesList.innerHTML = '';
+ backupCodes.forEach(code => {
+ const li = document.createElement('li');
+ li.textContent = code;
+ backupCodesList.appendChild(li);
+ });
+
+ const totpSetup = document.getElementById('totp-setup');
+ const totpBackupCodes = document.getElementById('totp-backup-codes');
+
+ totpSetup.classList.add('hide');
+ totpBackupCodes.classList.remove('hide');
+ } else {
+ showErrorMessage(data.message);
+ console.log(error);
+ }
+ })
+ .catch(error => {
+ showErrorMessage(error);
+ console.log(error);
+ });
+}
+
+function copyBackupCodes() {
+ const backupCodes = document.querySelectorAll('#backup-codes li');
+ const codes = Array.from(backupCodes).map(code => code.textContent).join('\n');
+
+ navigator.clipboard.writeText(codes)
+ .then(() => {
+ showSuccessMessage(translate('copied_to_clipboard'));
+ })
+ .catch(() => {
+ showErrorMessage(translate('unknown_error'));
+ });
+}
+
+function downloadBackupCodes() {
+ const backupCodes = document.querySelectorAll('#backup-codes li');
+ const codes = Array.from(backupCodes).map(code => code.textContent).join('\n');
+ const element = document.createElement('a');
+
+ element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(codes));
+ element.setAttribute('download', 'wallos-backup-codes.txt');
+ element.style.display = 'none';
+ document.body.appendChild(element);
+
+ element.click();
+
+ document.body.removeChild(element);
+}
+
+function closeTotpDisablePopup() {
+ const totpPopup = document.getElementById('totp-disable-popup');
+ totpPopup.classList.remove('is-open');
+}
+
+function disableTotp() {
+ const totpPopup = document.getElementById('totp-disable-popup');
+ totpPopup.classList.add('is-open');
+}
+
+function submitDisableTotp() {
+ const totpCode = document.getElementById('totp-disable').value;
+
+ fetch('endpoints/user/disable_totp.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ totpCode: totpCode }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ showSuccessMessage(data.message);
+ if (data.reload) {
+ location.reload();
+ }
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch(error => {
+ showErrorMessage(error);
+ });
+}
+
+function regenerateApiKey() {
+ const regenerateButton = document.getElementById('regenerateApiKey');
+ regenerateButton.disabled = true;
+
+ fetch('endpoints/user/regenerateapikey.php', {
+ method: 'POST',
+ })
+ .then(response => response.json())
+ .then(data => {
+ regenerateButton.disabled = false;
+ if (data.success) {
+ const newApiKey = data.apiKey;
+ document.getElementById('apikey').value = newApiKey;
+ showSuccessMessage(data.message);
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch(error => {
+ regenerateButton.disabled = false;
+ showErrorMessage(error);
+ });
+}
+
+function exportAsJson() {
+ fetch("endpoints/subscriptions/export.php")
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ const subscriptions = JSON.stringify(data.subscriptions);
+ const element = document.createElement('a');
+ element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(subscriptions));
+ element.setAttribute('download', 'subscriptions.json');
+ element.style.display = 'none';
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch(error => {
+ console.log(error);
+ showErrorMessage(translate('unknown_error'));
+ });
+}
+
+function exportAsCsv() {
+ fetch("endpoints/subscriptions/export.php")
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ const subscriptions = data.subscriptions;
+ const header = Object.keys(subscriptions[0]).join(',');
+ const csv = subscriptions.map(subscription => Object.values(subscription).join(',')).join('\n');
+ const csvWithHeader = header + '\n' + csv;
+ const element = document.createElement('a');
+ element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csvWithHeader));
+ element.setAttribute('download', 'subscriptions.csv');
+ element.style.display = 'none';
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch(error => {
+ showErrorMessage(translate('unknown_error'));
+ });
+}
+
+function deleteAccount(userId) {
+ if (!confirm(translate('delete_account_confirmation'))) {
+ return;
+ }
+
+ if (!confirm(translate('this_will_delete_all_data'))) {
+ return;
+ }
+
+ fetch('endpoints/settings/deleteaccount.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId: userId }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ window.location.href = 'logout.php';
+ } else {
+ showErrorMessage(data.message);
+ }
+ })
+ .catch((error) => {
+ showErrorMessage(translate('unknown_error'));
+ });
+}
+
diff --git a/scripts/settings.js b/scripts/settings.js
index e482c81dd..bb42344be 100644
--- a/scripts/settings.js
+++ b/scripts/settings.js
@@ -18,77 +18,6 @@ const editSvgContent = ``;
-function toggleAvatarSelect() {
- var avatarSelect = document.getElementById("avatarSelect");
- if (avatarSelect.classList.contains("is-open")) {
- avatarSelect.classList.remove("is-open");
- } else {
- avatarSelect.classList.add("is-open");
- }
-}
-
-function closeAvatarSelect() {
- var avatarSelect = document.getElementById("avatarSelect");
- avatarSelect.classList.remove("is-open");
-}
-
-document.querySelectorAll('.avatar-option').forEach((avatar) => {
- avatar.addEventListener("click", () => {
- changeAvatar(avatar.src);
- document.getElementById('avatarUser').value = avatar.getAttribute('data-src');
- closeAvatarSelect();
- })
-});
-
-function changeAvatar(src) {
- document.getElementById("avatarImg").src = src;
-}
-
-function successfulUpload(field, msg) {
- var reader = new FileReader();
-
- if (field.files.length === 0) {
- return;
- }
-
- if (!['image/jpeg', 'image/png', 'image/gif', 'image/jtif', 'image/webp'].includes(field.files[0]['type'])) {
- showErrorMessage(msg);
- return;
- }
-
- reader.onload = function () {
- changeAvatar(reader.result);
- };
-
- reader.readAsDataURL(field.files[0]);
- closeAvatarSelect();
-}
-
-function deleteAvatar(path) {
- fetch('/endpoints/user/delete_avatar.php', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ avatar: path }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- var avatarContainer = document.querySelector(`.avatar-container[data-src="${path}"]`);
- if (avatarContainer) {
- avatarContainer.remove();
- }
- showSuccessMessage();
- } else {
- showErrorMessage();
- }
- })
- .catch((error) => {
- console.error('Error:', error);
- });
-}
-
function saveBudget() {
const button = document.getElementById("saveBudget");
button.disabled = true;
@@ -803,34 +732,6 @@ var sortable = Sortable.create(el, {
document.addEventListener('DOMContentLoaded', function () {
- document.getElementById("userForm").addEventListener("submit", function (event) {
- event.preventDefault();
- document.getElementById("userSubmit").disabled = true;
- const formData = new FormData(event.target);
- fetch("endpoints/user/save_user.php", {
- method: "POST",
- body: formData
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- document.getElementById("avatar").src = document.getElementById("avatarImg").src;
- var newUsername = document.getElementById("username").value;
- document.getElementById("user").textContent = newUsername;
- showSuccessMessage(data.message);
- if (data.reload) {
- location.reload();
- }
- } else {
- showErrorMessage(data.errorMessage);
- }
- document.getElementById("userSubmit").disabled = false;
- })
- .catch(error => {
- showErrorMessage(translate('unknown_error'));
- });
- });
-
var removePaymentButtons = document.querySelectorAll(".delete-payment-method");
removePaymentButtons.forEach(function (button) {
button.addEventListener('click', function (event) {
@@ -874,6 +775,7 @@ function addFixerKeyButton() {
document.getElementById("addFixerKey").disabled = false;
});
}
+
function storeSettingsOnDB(endpoint, value) {
fetch('endpoints/settings/' + endpoint + '.php', {
method: 'POST',
@@ -978,232 +880,3 @@ var sortable = Sortable.create(el, {
saveCategorySorting();
},
});
-
-function exportAsJson() {
- fetch("endpoints/subscriptions/export.php")
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- const subscriptions = JSON.stringify(data.subscriptions);
- const element = document.createElement('a');
- element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(subscriptions));
- element.setAttribute('download', 'subscriptions.json');
- element.style.display = 'none';
- document.body.appendChild(element);
- element.click();
- document.body.removeChild(element);
- } else {
- showErrorMessage(data.message);
- }
- })
- .catch(error => {
- console.log(error);
- showErrorMessage(translate('unknown_error'));
- });
-}
-
-function exportAsCsv() {
- fetch("endpoints/subscriptions/export.php")
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- const subscriptions = data.subscriptions;
- const header = Object.keys(subscriptions[0]).join(',');
- const csv = subscriptions.map(subscription => Object.values(subscription).join(',')).join('\n');
- const csvWithHeader = header + '\n' + csv;
- const element = document.createElement('a');
- element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csvWithHeader));
- element.setAttribute('download', 'subscriptions.csv');
- element.style.display = 'none';
- document.body.appendChild(element);
- element.click();
- document.body.removeChild(element);
- } else {
- showErrorMessage(data.message);
- }
- })
- .catch(error => {
- showErrorMessage(translate('unknown_error'));
- });
-}
-
-function deleteAccount(userId) {
- if (!confirm(translate('delete_account_confirmation'))) {
- return;
- }
-
- if (!confirm(translate('this_will_delete_all_data'))) {
- return;
- }
-
- fetch('endpoints/settings/deleteaccount.php', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ userId: userId }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- window.location.href = 'logout.php';
- } else {
- showErrorMessage(data.message);
- }
- })
- .catch((error) => {
- showErrorMessage(translate('unknown_error'));
- });
-}
-
-function enableTotp() {
- const totpSecret = document.querySelector('#totp-secret');
- const totpSecretCode = document.querySelector('#totp-secret-code');
- const qrCode = document.getElementById('totp-qr-code');
- totpSecret.value = '';
- totpSecretCode.textContent = '';
- qrCode.innerHTML = '';
-
- fetch('endpoints/user/enable_totp.php?generate=true', {
- method: 'GET'
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- totpSecret.value = data.secret;
- totpSecretCode.textContent = data.secret;
- new QRCode(qrCode, data.qrCodeUrl);
-
- openTotpPopup();
- } else {
- showErrorMessage(data.message);
- }
- })
- .catch(error => {
- showErrorMessage(error);
- });
-}
-
-function openTotpPopup() {
- const enableTotpButton = document.getElementById('enableTotp');
- enableTotpButton.disabled = true;
-
- const totpPopup = document.getElementById('totp-popup');
- totpPopup.classList.add('is-open');
-}
-
-function closeTotpPopup() {
- const enableTotpButton = document.getElementById('enableTotp');
- enableTotpButton.disabled = false;
- const totpPopup = document.getElementById('totp-popup');
- totpPopup.classList.remove('is-open');
-
- const totpBackupCodes = document.getElementById('totp-backup-codes');
- if (!totpBackupCodes.classList.contains('hide')) {
- location.reload();
- }
-}
-
-function submitTotp() {
- const totpCode = document.getElementById('totp').value;
- const totpSecret = document.getElementById('totp-secret').value;
-
- fetch('endpoints/user/enable_totp.php', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ totpCode: totpCode, totpSecret: totpSecret }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showSuccessMessage(data.message);
- const backupCodes = data.backupCodes;
- const backupCodesList = document.getElementById('backup-codes');
- backupCodesList.innerHTML = '';
- backupCodes.forEach(code => {
- const li = document.createElement('li');
- li.textContent = code;
- backupCodesList.appendChild(li);
- });
-
- const totpSetup = document.getElementById('totp-setup');
- const totpBackupCodes = document.getElementById('totp-backup-codes');
-
- totpSetup.classList.add('hide');
- totpBackupCodes.classList.remove('hide');
- } else {
- showErrorMessage(data.message);
- console.log(error);
- }
- })
- .catch(error => {
- showErrorMessage(error);
- console.log(error);
- });
-}
-
-function copyBackupCodes() {
- const backupCodes = document.querySelectorAll('#backup-codes li');
- const codes = Array.from(backupCodes).map(code => code.textContent).join('\n');
-
- navigator.clipboard.writeText(codes)
- .then(() => {
- showSuccessMessage(translate('copied_to_clipboard'));
- })
- .catch(() => {
- showErrorMessage(translate('unknown_error'));
- });
-}
-
-function downloadBackupCodes() {
- const backupCodes = document.querySelectorAll('#backup-codes li');
- const codes = Array.from(backupCodes).map(code => code.textContent).join('\n');
- const element = document.createElement('a');
-
- element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(codes));
- element.setAttribute('download', 'wallos-backup-codes.txt');
- element.style.display = 'none';
- document.body.appendChild(element);
-
- element.click();
-
- document.body.removeChild(element);
-}
-
-function closeTotpDisablePopup() {
- const totpPopup = document.getElementById('totp-disable-popup');
- totpPopup.classList.remove('is-open');
-}
-
-function disableTotp() {
- const totpPopup = document.getElementById('totp-disable-popup');
- totpPopup.classList.add('is-open');
-}
-
-function submitDisableTotp() {
- const totpCode = document.getElementById('totp-disable').value;
-
- fetch('endpoints/user/disable_totp.php', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ totpCode: totpCode }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showSuccessMessage(data.message);
- if (data.reload) {
- location.reload();
- }
- } else {
- showErrorMessage(data.message);
- }
- })
- .catch(error => {
- showErrorMessage(error);
- });
-}
\ No newline at end of file
diff --git a/settings.php b/settings.php
index 2c484a819..1b1a93f98 100644
--- a/settings.php
+++ b/settings.php
@@ -1,10 +1,17 @@
prepare($query);
+$query->bindValue(':userId', $userId, SQLITE3_INTEGER);
+$result = $query->execute();
+while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
+ $currencyId = $row['id'];
+ $currencies[$currencyId] = $row;
+}
+$userData['currency_symbol'] = "€";
+
?>
@@ -15,229 +22,7 @@
}
-
-
- = translate('user_details', $i18n) ?>
-
-
-
-
- prepare($sql);
- $result = $stmt->execute();
- $row = $result->fetchArray(SQLITE3_ASSOC);
- $loginDisabled = $row['login_disabled'];
-
- $showTotpSection = true;
- if ($loginDisabled && !$userData['totp_enabled']) {
- $showTotpSection = false;
- }
-
- if ($showTotpSection) {
- ?>
-
-
- = translate("two_factor_authentication", $i18n) ?>
-
-
-
-
-
+
= translate('monthly_budget', $i18n) ?>
@@ -1407,42 +1192,6 @@ class="thin">= $settings['customCss'] ?? "" ?>
-
-
- = translate('account', $i18n) ?>
-
-
-
-
-
= translate('danger_zone', $i18n) ?>
-
-
-
-
-
-
- = translate('delete_account_info', $i18n) ?>
-
-
-
-
-
-
diff --git a/styles/styles.css b/styles/styles.css
index 2b4edae44..517b41832 100644
--- a/styles/styles.css
+++ b/styles/styles.css
@@ -1677,11 +1677,15 @@ button.dark-theme-button i {
@media (max-width: 768px) {
.toast {
- bottom: 70px;
+ bottom: 0px;
right: 0px;
left: 0px;
width: 100%;
}
+
+ .mobile-navivagtion .toast {
+ bottom: 70px;
+ }
}
.toast.active {
From d601921dd4ffcd785536ce48235cf6108f39ec8a Mon Sep 17 00:00:00 2001
From: Miguel Ribeiro
Date: Fri, 4 Oct 2024 15:07:20 +0200
Subject: [PATCH 2/7] remove duplicate translation key
---
includes/i18n/de.php | 1 -
includes/i18n/el.php | 1 -
includes/i18n/en.php | 1 -
includes/i18n/es.php | 1 -
includes/i18n/fr.php | 1 -
includes/i18n/it.php | 1 -
includes/i18n/jp.php | 1 -
includes/i18n/ko.php | 1 -
includes/i18n/pl.php | 1 -
includes/i18n/pt.php | 1 -
includes/i18n/pt_br.php | 1 -
includes/i18n/ru.php | 1 -
includes/i18n/sl.php | 1 -
includes/i18n/sr.php | 1 -
includes/i18n/sr_lat.php | 1 -
includes/i18n/tr.php | 1 -
includes/i18n/zh_cn.php | 1 -
includes/i18n/zh_tw.php | 1 -
18 files changed, 18 deletions(-)
diff --git a/includes/i18n/de.php b/includes/i18n/de.php
index ad9d7e108..2fe140b8f 100644
--- a/includes/i18n/de.php
+++ b/includes/i18n/de.php
@@ -191,7 +191,6 @@
"currency_info" => "Finde die unterstützten Währungen und korrekten Währungscodes auf",
"currency_performance" => "Aus Gründen der Performance wähle bitte ausschließlich die genutzen Währungen.",
"fixer_api_key" => "Fixer API Key",
- "api_key" => "API Key",
"provider" => "Anbieter",
"fixer_info" => "Falls du mehrere Währungen nutzt und genaue Statistiken und die Sortierungsfunktion nutzen möchtest, wird ein kostenfreier API Key von Fixer benötigt.",
"get_key" => "Erhalte deinen key bei",
diff --git a/includes/i18n/el.php b/includes/i18n/el.php
index 530627cbb..de41bd6c7 100644
--- a/includes/i18n/el.php
+++ b/includes/i18n/el.php
@@ -191,7 +191,6 @@
"currency_info" => "Βρες τα υποστηριζόμενα νομίσματα και τους σωστούς κωδικούς νομίσματος στο",
"currency_performance" => "Για βελτιωμένη απόδοση κράτησε μόνο τα νομίσματα που χρησιμοποιείς.",
"fixer_api_key" => "Fixer API κλειδί",
- "api_key" => "API κλειδί",
"provider" => "Πάροχος",
"fixer_info" => "Εάν χρησιμοποιείς πολλαπλά νομίσματα και θέλεις ακριβή στατιστικά στοιχεία και ταξινόμηση των συνδρομών, είναι απαραίτητο ένα ΔΩΡΕΑΝ κλειδί API από το Fixer.",
"get_key" => "Απόκτησε το κλειδί στο",
diff --git a/includes/i18n/en.php b/includes/i18n/en.php
index 7900289f7..abeb0648b 100644
--- a/includes/i18n/en.php
+++ b/includes/i18n/en.php
@@ -192,7 +192,6 @@
"currency_info" => "Find the supported currencies and correct currency codes on",
"currency_performance" => "For improved performance keep only the currencies you use.",
"fixer_api_key" => "Fixer API Key",
- "api_key" => "API Key",
"provider" => "Provider",
"fixer_info" => "If you use multiple currencies, and want accurate statistics and sorting on the subscriptions, a FREE API Key from Fixer is necessary.",
"get_key" => "Get your key at",
diff --git a/includes/i18n/es.php b/includes/i18n/es.php
index ae7b0b94e..e79e354f2 100644
--- a/includes/i18n/es.php
+++ b/includes/i18n/es.php
@@ -191,7 +191,6 @@
"currency_info" => "Encuentra las monedas admitidas y los códigos de moneda correctos en",
"currency_performance" => "Para un rendimiento mejorado, guarda solo las monedas que uses.",
"fixer_api_key" => "API Key de Fixer",
- "api_key" => "API Key",
"provider" => "Proveedor",
"fixer_info" => "Si usas varias monedas y deseas estadísticas y orden precisos en las suscripciones, es necesaria una API KEY gratuita de Fixer.",
"get_key" => "Obtén tu clave en",
diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php
index 8c84ecb8e..79a63f252 100644
--- a/includes/i18n/fr.php
+++ b/includes/i18n/fr.php
@@ -191,7 +191,6 @@
"currency_info" => "Trouvez les devises prises en charge et les codes de devise corrects sur",
"currency_performance" => "Pour des performances améliorées, ne conservez que les devises que vous utilisez.",
"fixer_api_key" => "Clé API de Fixer",
- "api_key" => "Clé API",
"provider" => "Fournisseur",
"fixer_info" => "Si vous utilisez plusieurs devises et souhaitez des statistiques et un tri précis sur les abonnements, une clé API GRATUITE de Fixer est nécessaire.",
"get_key" => "Obtenez votre clé sur",
diff --git a/includes/i18n/it.php b/includes/i18n/it.php
index 153f5034a..9f1b685cb 100644
--- a/includes/i18n/it.php
+++ b/includes/i18n/it.php
@@ -198,7 +198,6 @@
'currency_info' => 'Trova le valute supportate e i codici valuta corretti su',
'currency_performance' => 'Per garantire prestazioni migliori, tieni solo le valute che utilizzi.',
'fixer_api_key' => 'Chiave API di Fixer',
- 'api_key' => 'Chiave API',
'provider' => 'Fornitore',
'fixer_info' => 'Se utilizzi più valute e desideri visualizzare statistiche e ordinamenti accurati sugli abbonamenti, è necessaria una chiave API (Gratuita) da Fixer.',
'get_key' => 'Ottieni la tua chiave su',
diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php
index fe151f4b8..7ba8a3741 100644
--- a/includes/i18n/jp.php
+++ b/includes/i18n/jp.php
@@ -191,7 +191,6 @@
"currency_info" => "サポートされている通貨と正しい通貨コードを見つける",
"currency_performance" => "Fパフォーマンスを向上させるには、使用する通貨のみを保持してください。",
"fixer_api_key" => "FixerのAPIキー",
- "api_key" => "APIキー",
"provider" => "プロバイダ",
"fixer_info" => "複数の通貨を使用し、定期購入に関する正確な統計と並べ替えが必要な場合は、Fixerからの無料APIキーが必要です。",
"get_key" => "キーを入手する",
diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php
index dffb86c57..339211976 100644
--- a/includes/i18n/ko.php
+++ b/includes/i18n/ko.php
@@ -192,7 +192,6 @@
"currency_info" => "지원하는 통화와 정확한 통화 코드 찾기",
"currency_performance" => "성능을 향상시키기 위해서는 사용할 통화들만 유지하세요.",
"fixer_api_key" => "Fixer API 키",
- "api_key" => "API 키",
"provider" => "제공자",
"fixer_info" => "여러 통화를 사용하고, 정확한 통계와 구독별 정렬을 원하시느 경우에는, Fixer에서 발급받은 무료 API 키가 필요합니다.",
"get_key" => "키 얻기",
diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php
index e70f69c42..41d9684fb 100644
--- a/includes/i18n/pl.php
+++ b/includes/i18n/pl.php
@@ -191,7 +191,6 @@
"currency_info" => "Znajdź obsługiwane waluty i prawidłowe kody walut na",
"currency_performance" => "W celu poprawy wydajności przechowuj tylko te waluty, których używasz.",
"fixer_api_key" => "Klucz API Fixer'a",
- "api_key" => "Klucz API",
"provider" => "Dostawca",
"fixer_info" => "Jeśli używasz wielu walut i chcesz dokładnych statystyk i sortowania subskrypcji, niezbędny jest DARMOWY klucz API z Fixer'a.",
"get_key" => "Zdobądź klucz na stronie",
diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php
index e62bf3a5e..eb2050fa1 100644
--- a/includes/i18n/pt.php
+++ b/includes/i18n/pt.php
@@ -191,7 +191,6 @@
"currency_info" => "Encontre a lista de moedas e os respectivos códigos em",
"currency_performance" => "Por motivos de desempenho mantenha apenas as moedas que usa.",
"fixer_api_key" => "Fixer API Key",
- "api_key" => "API Key",
"provider" => "Fornecedor",
"fixer_info" => "Se usa multiplas moedas e deseja estatísticas correctas é necessário uma API Key grátis do Fixer.",
"get_key" => "Obtenha a sua API Key em",
diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php
index 0ae5399d8..63f7eac49 100644
--- a/includes/i18n/pt_br.php
+++ b/includes/i18n/pt_br.php
@@ -191,7 +191,6 @@
"currency_info" => "Encontre as moedas suportadas e os códigos de moeda em",
"currency_performance" => "Para um melhor desempenho, mantenha apenas as moedas que você utiliza.",
"fixer_api_key" => "Chave da API do Fixer",
- "api_key" => "Chave da API",
"provider" => "Fornecedor",
"fixer_info" => "Se você utiliza múltiplas moedas e deseja ter estatísticas precisas e ordenação das assinaturas, uma chave GRATUÍTA da API do Fixer é necessária.",
"get_key" => "Obtenha a sua chave em",
diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php
index 7a99a0f9e..7f00ce9cb 100644
--- a/includes/i18n/ru.php
+++ b/includes/i18n/ru.php
@@ -191,7 +191,6 @@
"currency_info" => "Найдите поддерживаемые валюты и правильные коды валют на",
"currency_performance" => "Для повышения производительности сохраняйте только те валюты, которые вы используете.",
"fixer_api_key" => "Ключ Fixer API",
- "api_key" => "API ключ",
"provider" => "Провайдер",
"fixer_info" => "Если вы используете несколько валют и хотите получить точную статистику и сортировку подписок, вам необходим БЕСПЛАТНЫЙ ключ API от Fixer.",
"get_key" => "Получите ключ по адресу",
diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php
index 241b1b7dd..d4e05ab15 100644
--- a/includes/i18n/sl.php
+++ b/includes/i18n/sl.php
@@ -191,7 +191,6 @@
"currency_info" => "Poiščite podprte valute in pravilne kode valut na",
"currency_performance" => "Za izboljšano delovanje obdržite samo valute, ki jih uporabljate.",
"fixer_api_key" => "API ključ za Fixer",
- "api_key" => "Fixer API",
"provider" => "Ponudnik",
"fixer_info" => "Če uporabljate več valut in želite natančno statistiko in razvrščanje naročnin, potrebujete BREZPLAČNI API ključ od Fixerja.",
"get_key" => "Pridobite svoj ključ pri",
diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php
index cb99cb6f9..775c2fa01 100644
--- a/includes/i18n/sr.php
+++ b/includes/i18n/sr.php
@@ -191,7 +191,6 @@
"currency_info" => "Пронађите подржане валуте и исправне кодове валута на",
"currency_performance" => "За побољшану перформансу, задржите само валуте које користите.",
"fixer_api_key" => "Fixer API кључ",
- "api_key" => "API кључ",
"provider" => "Провајдер",
"fixer_info" => "Ако користите више валута и желите тачне статистике и сортирање претплата, неопходан је БЕСПЛАТНИ API кључ од Fixer.",
"get_key" => "Добијте свој кључ на",
diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php
index 50fc4dfdf..45ed458fd 100644
--- a/includes/i18n/sr_lat.php
+++ b/includes/i18n/sr_lat.php
@@ -191,7 +191,6 @@
"currency_info" => "Pronađite podržane valute i ispravne kodove valuta na",
"currency_performance" => "Za poboljšanu performansu, zadržite samo valute koje koristite.",
"fixer_api_key" => "Fixer API ključ",
- "api_key" => "API ključ",
"provider" => "Provajder",
"fixer_info" => "Ako koristite više valuta i želite tačne statistike i sortiranje pretplata, neophodan je BESPLATNI API ključ sa Fixer-a.",
"get_key" => "Pronađite svoj ključ na",
diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php
index b89f0e963..85facc83d 100644
--- a/includes/i18n/tr.php
+++ b/includes/i18n/tr.php
@@ -191,7 +191,6 @@
"currency_info" => "Desteklenen para birimlerini ve doğru para birimi kodlarını burada bulun",
"currency_performance" => "Performansı artırmak için sadece kullandığınız para birimlerini tutun.",
"fixer_api_key" => "Fixer API Anahtarı",
- "api_key" => "API Anahtarı",
"provider" => "Sağlayıcı",
"fixer_info" => "Birden fazla para birimi kullanıyorsanız ve aboneliklerde doğru istatistikler ve sıralama istiyorsanız, Fixer'dan ÜCRETSİZ bir API Anahtarı gereklidir.",
"get_key" => "Anahtarınızı şuradan alın",
diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php
index d6255dfc7..08e1d6025 100644
--- a/includes/i18n/zh_cn.php
+++ b/includes/i18n/zh_cn.php
@@ -199,7 +199,6 @@
"currency_info" => "如要查找支持的货币与对应代码,请前往",
"currency_performance" => "为提高性能,建议您只保留常用货币。",
"fixer_api_key" => "Fixer API 密钥",
- "api_key" => "API 密钥",
"provider" => "提供商",
"fixer_info" => "如果您使用多种货币,希望统计信息和订阅排序更精确,则需要 Fixer API 密钥来查询汇率(可免费申请)。",
"get_key" => "申请密钥",
diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php
index 9ff75a31b..54cc72b63 100644
--- a/includes/i18n/zh_tw.php
+++ b/includes/i18n/zh_tw.php
@@ -191,7 +191,6 @@
"currency_info" => "如要查詢支援的貨幣與相對應的貨幣代碼,請前往",
"currency_performance" => "為提高效能,建議您只保留常用的貨幣。",
"fixer_api_key" => "Fixer API 金鑰",
- "api_key" => "API 金鑰",
"provider" => "提供者",
"fixer_info" => "如果您使用多種貨幣單位,且希望統計資訊和訂閱排序更加精確,則需要 Fixer API 金鑰來查詢匯率(可免費申請)。",
"get_key" => "申請金鑰",
From 11c21929dde2cdffa40dd50c9a54b3b6f9d8fbe1 Mon Sep 17 00:00:00 2001
From: Miguel Ribeiro
Date: Fri, 4 Oct 2024 15:31:19 +0200
Subject: [PATCH 3/7] add new profile page into the mobile navigation menu
---
includes/header.php | 40 ++++++++++++++--------------------------
index.php | 2 +-
styles/styles.css | 10 +++++++++-
3 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/includes/header.php b/includes/header.php
index e140a6827..c5c13bfc8 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -206,33 +206,21 @@ class="fa-solid fa-arrow-right-from-bracket">= translate('logout', $i18n)
if ($settings['mobile_nav'] == 1) {
?>
-
+
+ Icons by icons8:
+
+ https://icons8.com/
+
+
+
+
+
diff --git a/images/siteicons/svg/mobile-menu/calendar.php b/images/siteicons/svg/mobile-menu/calendar.php
new file mode 100644
index 000000000..cc9b50d5b
--- /dev/null
+++ b/images/siteicons/svg/mobile-menu/calendar.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/images/siteicons/svg/mobile-menu/home.php b/images/siteicons/svg/mobile-menu/home.php
new file mode 100644
index 000000000..af054b52f
--- /dev/null
+++ b/images/siteicons/svg/mobile-menu/home.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/images/siteicons/svg/mobile-menu/profile.php b/images/siteicons/svg/mobile-menu/profile.php
new file mode 100644
index 000000000..831ce536d
--- /dev/null
+++ b/images/siteicons/svg/mobile-menu/profile.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/images/siteicons/svg/mobile-menu/settings.php b/images/siteicons/svg/mobile-menu/settings.php
new file mode 100644
index 000000000..8aa56fd95
--- /dev/null
+++ b/images/siteicons/svg/mobile-menu/settings.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/images/siteicons/svg/mobile-menu/statistics.php b/images/siteicons/svg/mobile-menu/statistics.php
new file mode 100644
index 000000000..94306005a
--- /dev/null
+++ b/images/siteicons/svg/mobile-menu/statistics.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/includes/header.php b/includes/header.php
index c5c13bfc8..9adfdbc2a 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -200,6 +200,7 @@ class="fa-solid fa-arrow-right-from-bracket">= translate('logout', $i18n)
$calendarClass = $page === 'calendar.php' ? 'active' : '';
$statsClass = $page === 'stats.php' ? 'active' : '';
$settingsClass = $page === 'settings.php' ? 'active' : '';
+ $profileClass = $page === 'profile.php' ? 'active' : '';
?>
= translate('logout', $i18n)
?>
a {
- padding: 0px 10px;
+ color: #909090;
}
- .mobile-nav>a>i {
- color: #777;
- }
-
- .mobile-nav>a.active>i {
+ .mobile-nav>a.active {
color: #f0f0F0;
}
}
\ No newline at end of file
diff --git a/styles/styles.css b/styles/styles.css
index 19e51e474..dcc1cea72 100644
--- a/styles/styles.css
+++ b/styles/styles.css
@@ -2587,45 +2587,34 @@ input[type="radio"]:checked+label::after {
flex-direction: row;
justify-content: space-around;
z-index: 2;
- padding: 5px 0px;
- font-size: 28px;
+ padding: 7px 0px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
- align-items: baseline;
+ align-items: center;
}
.mobile-nav>a {
- flex-basis: 20%;
- flex-grow: 0;
+ flex-grow: 1;
flex-shrink: 0;
text-align: center;
- padding: 5px 0px 15px 0px;
- }
-
- .mobile-nav>a>i {
+ padding: 5px 0px 10px 0px;
color: #AAA;
- }
-
- .mobile-nav>a.active>i {
- color: #202020;
- }
-
- .mobile-nav>a.secondary,
- .mobile-nav>button {
- background-color: var(--main-color);
- border: none;
- border-radius: 35px;
- padding: 3px 8px;
+ font-size: 10px;
+ text-decoration: none;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
}
- .mobile-nav>button {
- border: none;
- font-size: 28px;
+ .mobile-nav>a>svg {
+ width: 30px;
+ height: 30px;
+ max-width: 85%;
}
-
- .mobile-nav>a.secondary>i,
- .mobile-nav>button>i {
- color: #fff;
+
+ .mobile-nav>a.active{
+ color: #202020;
}
.mobile-navivagtion .mobileNavigationHideOnMobile {
From 900f6321a500f3ec541a23dd8fe37b38c4114d9f Mon Sep 17 00:00:00 2001
From: Miguel Ribeiro
Date: Fri, 4 Oct 2024 17:19:34 +0200
Subject: [PATCH 6/7] fix button not full width on mobile
---
profile.php | 6 +++---
styles/styles.css | 7 ++++++-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/profile.php b/profile.php
index b15199458..29e664a19 100644
--- a/profile.php
+++ b/profile.php
@@ -151,12 +151,12 @@ class="thin mobile-grow" />
= translate("two_factor_authentication", $i18n) ?>