diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0e2679..b325b43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.5.0 - 2024-08-06 + +### Added + +- Auto JSON backup +- JSON backup encryption +- Settings to customize the swipe actions +- Setting to hide the app from recent apps and prevent screenshots +- Setting to show the checklist button in the toolbar if not shown in the app bar +- Enable high refresh rate +- Russian localization +- Show localizations completion as a percentage + +## Changed + +- New settings page layout that divides settings into separate pages +- Disable undo/redo buttons in the editor's app bar if they can't be used +- Enable swipe actions in grid mode + +### Fixed + +- Notes tiles: use rounded corners when background is not shown, use correct background color in list view +- Keyboard opening when toggling a checkbox +- Keyboard popping back up after using the back gesture +- App closing when going back while the selection mode is active instead of exiting it +- Strings inconsistencies + +### Removed + +- Swipe actions in the bin + ## 1.4.0 - 2024-06-27 ### Added diff --git a/README.md b/README.md index d3774f46..729ebbb4 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,13 @@ Simple, local, material design notes. - Share text from other applications to add it directly to a note - Share your notes as text -- Export your notes as JSON and import them back +- Export your notes as JSON, manually or automatically, and import them back - Export your notes as Markdown ### Protect - Never worry about how your data is handled: it cannot leave your device as the application doesn't have any internet permissions +- Encrypt your JSON exports ### Customize diff --git a/RELEASING.md b/RELEASING.md index 6a272fcf..a684fbe4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,11 +2,12 @@ ## Checklist +- [ ] Upgrade Flutter - [ ] Bump application version - [ ] Bump dependencies versions - [ ] Update `CHANGELOG.md` - [ ] Update `REAMDE.md` - [ ] Update fastlane descriptions - [ ] Update fastlane changelogs -- [ ] Update [data_screenshots.json](docs/data/data_screenshots.json) +- [ ] Update [screenshots.json](docs/data/screenshots.json) - [ ] Update fastlane screenshots diff --git a/android/app/build.gradle b/android/app/build.gradle index cdc7ebd5..505f9e15 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -69,8 +69,8 @@ android { defaultConfig { applicationId "com.maelchiotti.localmaterialnotes" - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + minSdkVersion 21 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 322c0d7d..7e49964e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ + android:enableOnBackInvokedCallback="false" + android:allowBackup="false"> Material Notes ist eine Anwendung zur Erstellung von Textnotizen, die auf Einfachheit ausgerichtet ist. Sie verwendet das Material Design. Sie speichert die Notizen lokal und hat keine Internetberechtigung, sodass nur Sie auf die Notizen zugreifen können.

Notizen machen

  • Verfassen von Textnotizen (Titel und Inhalt)
  • Nutzen Sie die erweiterten Formatierungsoptionen, einschließlich Checklisten
  • Rückgängig machen und Wiederherstellen von Änderungen während der Bearbeitung
  • Verwenden Sie die Schnellaktion auf Ihrem Startbildschirm, um schnell eine Notiz hinzuzufügen

Organisieren Sie

  • Durchsuchen Sie Ihre Notizen
  • Sortieren Sie Ihre Notizen nach Datum oder Titel, in aufsteigender oder absteigender Reihenfolge
  • Anzeige Ihrer Notizen in einer Liste oder einem Raster
  • Anheften von Notizen
  • Wiederherstellen gelöschter Notizen aus dem Papierkorb

Freigeben & Sichern

  • Teilen Sie Text aus anderen Anwendungen, um ihn direkt in eine Notiz einzufügen
  • Teilen Sie Ihre Notizen als Text
  • Exportieren Sie Ihre Notizen als JSON und importieren Sie sie wieder
  • Exportieren Sie Ihre Notizen als Markdown

Schützen Sie

  • Machen Sie sich keine Gedanken darüber, wie Ihre Daten gehandhabt werden: Sie können Ihr Gerät nicht verlassen, da die Anwendung keine Internetberechtigungen hat.

Anpassen von

  • Wählen Sie Ihre Sprache
  • Wählen Sie Ihr Thema (hell, dunkel oder schwarz)
  • Legen Sie fest, ob Ihr Thema dynamisch sein soll (verwenden Sie die Farben Ihres Hintergrunds)
  • Wählen Sie, ob Sie die erweiterte Formatierung, nur die Checklisten oder nur die einfachen Notizen aktivieren möchten
\ No newline at end of file diff --git a/fastlane/metadata/android/de-DE/full_description.yaml b/fastlane/metadata/android/de-DE/full_description.yaml deleted file mode 100644 index bb469061..00000000 --- a/fastlane/metadata/android/de-DE/full_description.yaml +++ /dev/null @@ -1,55 +0,0 @@ -full_description: | -

- Material Notes ist eine Anwendung zur Erstellung von Textnotizen, die auf Einfachheit ausgerichtet ist. - Sie verwendet das Material Design. Sie speichert die Notizen lokal und hat keine Internetberechtigung, - sodass nur Sie auf die Notizen zugreifen können. -

- -

- Notizen machen -

-
    -
  • Verfassen von Textnotizen (Titel und Inhalt)
  • -
  • Nutzen Sie die erweiterten Formatierungsoptionen, einschließlich Checklisten
  • -
  • Rückgängig machen und Wiederherstellen von Änderungen während der Bearbeitung
  • -
  • Verwenden Sie die Schnellaktion auf Ihrem Startbildschirm, um schnell eine Notiz hinzuzufügen
  • -
- -

- Organisieren Sie -

-
    -
  • Durchsuchen Sie Ihre Notizen
  • -
  • Sortieren Sie Ihre Notizen nach Datum oder Titel, in aufsteigender oder absteigender Reihenfolge
  • -
  • Anzeige Ihrer Notizen in einer Liste oder einem Raster
  • -
  • Anheften von Notizen
  • -
  • Wiederherstellen gelöschter Notizen aus dem Papierkorb
  • -
- -

- Freigeben & Sichern -

-
    -
  • Teilen Sie Text aus anderen Anwendungen, um ihn direkt in eine Notiz einzufügen
  • -
  • Teilen Sie Ihre Notizen als Text
  • -
  • Exportieren Sie Ihre Notizen als JSON und importieren Sie sie wieder
  • -
  • Exportieren Sie Ihre Notizen als Markdown
  • -
- -

- Schützen Sie -

-
    -
  • Machen Sie sich keine Gedanken darüber, wie Ihre Daten gehandhabt werden: - Sie können Ihr Gerät nicht verlassen, da die Anwendung keine Internetberechtigungen hat.
  • -
- -

- Anpassen von -

-
    -
  • Wählen Sie Ihre Sprache
  • -
  • Wählen Sie Ihr Thema (hell, dunkel oder schwarz)
  • -
  • Legen Sie fest, ob Ihr Thema dynamisch sein soll (verwenden Sie die Farben Ihres Hintergrunds)
  • -
  • Wählen Sie, ob Sie die erweiterte Formatierung, nur die Checklisten oder nur die einfachen Notizen aktivieren möchten
  • -
diff --git a/fastlane/metadata/android/en-US/changelogs/100.txt b/fastlane/metadata/android/en-US/changelogs/100.txt new file mode 100644 index 00000000..8f9c59e9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/100.txt @@ -0,0 +1,20 @@ +ADDED +- Auto JSON backup +- JSON backup encryption +- Customize swipe actions +- Hide app from recent apps and prevent screenshots +- Show checklist button in the toolbar +- Enable high refresh rate +- Russian localization + +CHANGED +- New settings page layout + +FIXED +- Notes tiles issues +- Keyboard opening when toggling a checkbox +- Keyboard popping back up after using the back gesture +- Exit selection mode on back action + +REMOVED +- Swipe actions in the bin \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index a25da4e2..cc536e20 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1 +1 @@ -

Material Notes is a text-based note-taking application, aimed at simplicity. It embraces Material Design. It stores the notes locally and doesn't have any internet permissions, so you are the only one that can access the notes.

Take notes

  • Write text notes (title and content)
  • Take advantage of the advanced formatting options, including checklists
  • Undo and redo your changes while editing
  • Use the quick action from your home screen to quickly add a note

Organize

  • Search though your notes
  • Sort your notes by date or title, in ascending or descending order
  • Display your notes in a list or a grid view
  • Pin your notes
  • Recover your deleted notes from the bin

Share & backup

  • Share text from other applications to add it directly to a note
  • Share your notes as text
  • Export your notes as JSON and import them back
  • Export your notes as Markdown

Protect

  • Never worry about how your data is handled: it cannot leave your device as the application doesn't have any internet permissions

Customize

  • Choose your language
  • Choose your theme (light, dark or black)
  • Choose if you want your theme to be dynamic (use colors from your background)
  • Choose if you want to enable the advanced formatting, only the checklists or keep your notes basic
\ No newline at end of file +

Material Notes is a text-based note-taking application, aimed at simplicity. It embraces Material Design. It stores the notes locally and doesn't have any internet permissions, so you are the only one that can access the notes.

Take notes

  • Write text notes (title and content)
  • Take advantage of the advanced formatting options, including checklists
  • Undo and redo your changes while editing
  • Use the quick action from your home screen to quickly add a note

Organize

  • Search though your notes
  • Sort your notes by date or title, in ascending or descending order
  • Display your notes in a list or a grid view
  • Pin your notes
  • Recover your deleted notes from the bin

Share & backup

  • Share text from other applications to add it directly to a note
  • Share your notes as text
  • Export your notes as JSON, manually or automatically, and import them back
  • Export your notes as Markdown

Protect

  • Never worry about how your data is handled: it cannot leave your device as the application doesn't have any internet permissions
  • Encrypt your JSON exports

Customize

  • Choose your language
  • Choose your theme (light, dark or black)
  • Choose if you want your theme to be dynamic (use colors from your background)
  • Choose if you want to enable the advanced formatting, only the checklists or keep your notes basic
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.yaml b/fastlane/metadata/android/en-US/full_description.yaml index be475e43..7034556e 100644 --- a/fastlane/metadata/android/en-US/full_description.yaml +++ b/fastlane/metadata/android/en-US/full_description.yaml @@ -1,54 +1,55 @@ full_description: | -

- Material Notes is a text-based note-taking application, aimed at simplicity. It embraces Material Design. - It stores the notes locally and doesn't have any internet permissions, so you are the only one that can access the notes. -

+

+ Material Notes is a text-based note-taking application, aimed at simplicity. It embraces Material Design. + It stores the notes locally and doesn't have any internet permissions, so you are the only one that can access the notes. +

-

- Take notes -

-
    -
  • Write text notes (title and content)
  • -
  • Take advantage of the advanced formatting options, including checklists
  • -
  • Undo and redo your changes while editing
  • -
  • Use the quick action from your home screen to quickly add a note
  • -
+

+ Take notes +

+
    +
  • Write text notes (title and content)
  • +
  • Take advantage of the advanced formatting options, including checklists
  • +
  • Undo and redo your changes while editing
  • +
  • Use the quick action from your home screen to quickly add a note
  • +
-

- Organize -

-
    -
  • Search though your notes
  • -
  • Sort your notes by date or title, in ascending or descending order
  • -
  • Display your notes in a list or a grid view
  • -
  • Pin your notes
  • -
  • Recover your deleted notes from the bin
  • -
+

+ Organize +

+
    +
  • Search though your notes
  • +
  • Sort your notes by date or title, in ascending or descending order
  • +
  • Display your notes in a list or a grid view
  • +
  • Pin your notes
  • +
  • Recover your deleted notes from the bin
  • +
-

- Share & backup -

-
    -
  • Share text from other applications to add it directly to a note
  • -
  • Share your notes as text
  • -
  • Export your notes as JSON and import them back
  • -
  • Export your notes as Markdown
  • -
+

+ Share & backup +

+
    +
  • Share text from other applications to add it directly to a note
  • +
  • Share your notes as text
  • +
  • Export your notes as JSON, manually or automatically, and import them back
  • +
  • Export your notes as Markdown
  • +
-

- Protect -

-
    -
  • Never worry about how your data is handled: it cannot leave your device - as the application doesn't have any internet permissions
  • -
+

+ Protect +

+
    +
  • Never worry about how your data is handled: it cannot leave your device + as the application doesn't have any internet permissions
  • +
  • Encrypt your JSON exports
  • +
-

- Customize -

-
    -
  • Choose your language
  • -
  • Choose your theme (light, dark or black)
  • -
  • Choose if you want your theme to be dynamic (use colors from your background)
  • -
  • Choose if you want to enable the advanced formatting, only the checklists or keep your notes basic
  • -
+

+ Customize +

+
    +
  • Choose your language
  • +
  • Choose your theme (light, dark or black)
  • +
  • Choose if you want your theme to be dynamic (use colors from your background)
  • +
  • Choose if you want to enable the advanced formatting, only the checklists or keep your notes basic
  • +
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpg deleted file mode 100644 index b0f3249b..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png new file mode 100644 index 00000000..6f84343e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpg deleted file mode 100644 index 510198b3..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png new file mode 100644 index 00000000..ac1b853e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpg deleted file mode 100644 index 676ba1d3..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png new file mode 100644 index 00000000..7e076a3e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpg deleted file mode 100644 index b3e73667..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png new file mode 100644 index 00000000..505add17 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpg deleted file mode 100644 index 267d2663..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png new file mode 100644 index 00000000..2dcc3a94 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpg deleted file mode 100644 index dd4fb364..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png new file mode 100644 index 00000000..cab7c4ec Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpg deleted file mode 100644 index 0e72a2d3..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png new file mode 100644 index 00000000..abda9d12 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpg deleted file mode 100644 index 151951f1..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png new file mode 100644 index 00000000..a357d43d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png differ diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt deleted file mode 100644 index 19086b1c..00000000 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ /dev/null @@ -1 +0,0 @@ -

Material Notes es una aplicación de toma de notas basadas en texto, orientada a la simplicidad, diseñada adoptando Material Design. Material Notes almacena las notas localmente y no requiere ningún permiso de internet, siendo tú el único que puede acceder a las notas.

Tomar notas

  • Escriba notas de texto (título y contenido)
  • Aproveche las opciones avanzadas de formato, incluidas las listas de comprobación
  • Deshacer y rehacer los cambios durante la edición
  • Utiliza la acción rápida de la pantalla de inicio para añadir rápidamente una nota

Organiza

  • Busca en tus notas
  • Ordena tus notas por fecha o título, en orden ascendente o descendente
  • Visualiza tus notas en una lista o en una cuadrícula
  • Anclar tus notas
  • Recupera tus notas borradas de la papelera

Compartir y hacer copias de seguridad

  • Comparte texto desde otras aplicaciones para añadirlo directamente a una nota
  • Comparte tus notas como texto
  • Exporta tus notas como JSON e impórtalas de nuevo
  • Exporta tus notas como Markdown

Protege

  • No te preocupes por el tratamiento de tus datos: no pueden salir de tu dispositivo, ya que la aplicación no tiene permisos de Internet

Personaliza

  • Elige tu idioma
  • Elige tu tema (claro, oscuro o negro)
  • Elige si quieres que tu tema sea dinámico (usa los colores de tu fondo)
  • Elige si quieres activar el formato avanzado, sólo las listas de control o mantener tus notas básicas
\ No newline at end of file diff --git a/fastlane/metadata/android/es-ES/full_description.yaml b/fastlane/metadata/android/es-ES/full_description.yaml deleted file mode 100644 index 40014083..00000000 --- a/fastlane/metadata/android/es-ES/full_description.yaml +++ /dev/null @@ -1,54 +0,0 @@ -full_description: | -

- Material Notes es una aplicación de toma de notas basadas en texto, orientada a la simplicidad, diseñada adoptando Material Design. - Material Notes almacena las notas localmente y no requiere ningún permiso de internet, siendo tú el único que puede acceder a las notas. -

- -

- Tomar notas -

-
    -
  • Escriba notas de texto (título y contenido)
  • -
  • Aproveche las opciones avanzadas de formato, incluidas las listas de comprobación
  • -
  • Deshacer y rehacer los cambios durante la edición
  • -
  • Utiliza la acción rápida de la pantalla de inicio para añadir rápidamente una nota
  • -
- -

- Organiza -

-
    -
  • Busca en tus notas
  • -
  • Ordena tus notas por fecha o título, en orden ascendente o descendente
  • -
  • Visualiza tus notas en una lista o en una cuadrícula
  • -
  • Anclar tus notas
  • -
  • Recupera tus notas borradas de la papelera
  • -
- -

- Compartir y hacer copias de seguridad -

-
    -
  • Comparte texto desde otras aplicaciones para añadirlo directamente a una nota
  • -
  • Comparte tus notas como texto
  • -
  • Exporta tus notas como JSON e impórtalas de nuevo
  • -
  • Exporta tus notas como Markdown
  • -
- -

- Protege -

-
    -
  • No te preocupes por el tratamiento de tus datos: no pueden salir de tu dispositivo, - ya que la aplicación no tiene permisos de Internet
  • -
- -

- Personaliza -

-
    -
  • Elige tu idioma
  • -
  • Elige tu tema (claro, oscuro o negro)
  • -
  • Elige si quieres que tu tema sea dinámico (usa los colores de tu fondo)
  • -
  • Elige si quieres activar el formato avanzado, sólo las listas de control o mantener tus notas básicas
  • -
diff --git a/fastlane/metadata/android/fr-FR/100.txt b/fastlane/metadata/android/fr-FR/100.txt new file mode 100644 index 00000000..d582d675 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/100.txt @@ -0,0 +1,20 @@ +AJOUTÉ +- Sauvegarde auto en JSON +- Chiffrage de la sauvegarde JSON +- Personnaliser les balayages +- Masquer dans applications récentes et empêcher captures +- Afficher bouton cases à cocher dans barre d'outils +- Activer taux de rafraîchissement élevé +- Traduction en russe + +MODIFIÉ +- Nouvelle disposition des paramètres + +CORRIGÉ +- Problèmes des tuiles +- Ouverture du clavier involontaires +- Incohérences des textes + +SUPPRIMÉ +- Actions de balayage dans la corbeille +- Compatibilité avec anciens exports \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/100.txt b/fastlane/metadata/android/fr-FR/changelogs/100.txt new file mode 100644 index 00000000..570d3212 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/100.txt @@ -0,0 +1,19 @@ +AJOUTÉ +- Sauvegarde auto en JSON +- Chiffrage de la sauvegarde JSON +- Personnaliser les balayages +- Masquer dans applications récentes et empêcher captures +- Afficher bouton cases à cocher dans barre d'outils +- Activer taux de rafraîchissement élevé +- Traduction en russe + +MODIFIÉ +- Nouvelle disposition des paramètres + +CORRIGÉ +- Problèmes des tuiles +- Ouverture du clavier involontaires (cases à cocher, retour) +- Quitter mode sélection après retour + +SUPPRIMÉ +- Actions de balayage dans la corbeille \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index afc11367..1cca4443 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -1 +1 @@ -

Material Notes est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. Elle stocke les notes localement et n'a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes.

Prenez des notes

  • Rédigez des notes textuelles (titre et contenu)
  • Tirez parti des options de formatage avancées, y compris les checklists
  • Annulez et rétablissez vos modifications pendant l'édition
  • Utilisez l'action rapide de votre écran d'accueil pour ajouter rapidement une note.

Organisez

  • Recherchez dans vos notes
  • Triez vos notes par date ou par titre, par ordre croissant ou décroissant
  • Affichez vos notes sous forme de liste ou de grille
  • Épinglez vos notes
  • Récupérez vos notes supprimées de la corbeille

Partagez & sauvegardez

  • Partagez du texte à partir d'autres applications pour l'ajouter directement à une note
  • Partagez vos notes sous forme de texte
  • Exportez vos notes au format JSON et importez-les à nouveau
  • Exportez vos notes au format Markdown

Protégez

  • Ne vous inquiétez pas de la façon dont vos données sont traitées : elles ne peuvent pas quitter votre appareil car l'application n'a pas d'autorisations Internet

Personnalisez

  • Choisissez votre langue
  • Choisissez votre thème (clair, foncé ou noir)
  • Choisissez si vous voulez que votre thème soit dynamique (utilise les couleurs de votre arrière-plan)
  • Choisissez si vous voulez activer le formatage avancé, seulement les checklists ou garder vos notes basiques
\ No newline at end of file +

Material Notes est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. Elle stocke les notes localement et n'a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes.

Prenez des notes

  • Rédigez des notes textuelles (titre et contenu)
  • Tirez parti des options de formatage avancées, y compris les checklists
  • Annulez et rétablissez vos modifications pendant l'édition
  • Utilisez l'action rapide de votre écran d'accueil pour ajouter rapidement une note.

Organisez

  • Recherchez dans vos notes
  • Triez vos notes par date ou par titre, par ordre croissant ou décroissant
  • Affichez vos notes sous forme de liste ou de grille
  • Épinglez vos notes
  • Récupérez vos notes supprimées de la corbeille

Partagez & sauvegardez

  • Partagez du texte à partir d'autres applications pour l'ajouter directement à une note
  • Partagez vos notes sous forme de texte
  • Exportez vos notes au format JSON, manuellement ou automatiquement, et importez-les à nouveau
  • Exportez vos notes au format Markdown

Protégez

  • Ne vous inquiétez pas de la façon dont vos données sont traitées : elles ne peuvent pas quitter votre appareil car l'application n'a pas d'autorisations Internet
  • Chiffrez vos exports

Personnalisez

  • Choisissez votre langue
  • Choisissez votre thème (clair, foncé ou noir)
  • Choisissez si vous voulez que votre thème soit dynamique (utilise les couleurs de votre arrière-plan)
  • Choisissez si vous voulez activer le formatage avancé, seulement les checklists ou garder vos notes basiques
\ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/full_description.yaml b/fastlane/metadata/android/fr-FR/full_description.yaml index fb16d056..ed2f9d37 100644 --- a/fastlane/metadata/android/fr-FR/full_description.yaml +++ b/fastlane/metadata/android/fr-FR/full_description.yaml @@ -1,54 +1,55 @@ full_description: | -

- Material Notes est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. - Elle stocke les notes localement et n'a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes. -

+

+ Material Notes est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. + Elle stocke les notes localement et n'a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes. +

-

- Prenez des notes -

-
    -
  • Rédigez des notes textuelles (titre et contenu)
  • -
  • Tirez parti des options de formatage avancées, y compris les checklists
  • -
  • Annulez et rétablissez vos modifications pendant l'édition
  • -
  • Utilisez l'action rapide de votre écran d'accueil pour ajouter rapidement une note.
  • -
+

+ Prenez des notes +

+
    +
  • Rédigez des notes textuelles (titre et contenu)
  • +
  • Tirez parti des options de formatage avancées, y compris les checklists
  • +
  • Annulez et rétablissez vos modifications pendant l'édition
  • +
  • Utilisez l'action rapide de votre écran d'accueil pour ajouter rapidement une note.
  • +
-

- Organisez -

-
    -
  • Recherchez dans vos notes
  • -
  • Triez vos notes par date ou par titre, par ordre croissant ou décroissant
  • -
  • Affichez vos notes sous forme de liste ou de grille
  • -
  • Épinglez vos notes
  • -
  • Récupérez vos notes supprimées de la corbeille
  • -
+

+ Organisez +

+
    +
  • Recherchez dans vos notes
  • +
  • Triez vos notes par date ou par titre, par ordre croissant ou décroissant
  • +
  • Affichez vos notes sous forme de liste ou de grille
  • +
  • Épinglez vos notes
  • +
  • Récupérez vos notes supprimées de la corbeille
  • +
-

- Partagez & sauvegardez -

-
    -
  • Partagez du texte à partir d'autres applications pour l'ajouter directement à une note
  • -
  • Partagez vos notes sous forme de texte
  • -
  • Exportez vos notes au format JSON et importez-les à nouveau
  • -
  • Exportez vos notes au format Markdown
  • -
+

+ Partagez & sauvegardez +

+
    +
  • Partagez du texte à partir d'autres applications pour l'ajouter directement à une note
  • +
  • Partagez vos notes sous forme de texte
  • +
  • Exportez vos notes au format JSON, manuellement ou automatiquement, et importez-les à nouveau
  • +
  • Exportez vos notes au format Markdown
  • +
-

- Protégez -

-
    -
  • Ne vous inquiétez pas de la façon dont vos données sont traitées : - elles ne peuvent pas quitter votre appareil car l'application n'a pas d'autorisations Internet
  • -
+

+ Protégez +

+
    +
  • Ne vous inquiétez pas de la façon dont vos données sont traitées : + elles ne peuvent pas quitter votre appareil car l'application n'a pas d'autorisations Internet
  • +
  • Chiffrez vos exports
  • +
-

- Personnalisez -

-
    -
  • Choisissez votre langue
  • -
  • Choisissez votre thème (clair, foncé ou noir)
  • -
  • Choisissez si vous voulez que votre thème soit dynamique (utilise les couleurs de votre arrière-plan)
  • -
  • Choisissez si vous voulez activer le formatage avancé, seulement les checklists ou garder vos notes basiques
  • -
+

+ Personnalisez +

+
    +
  • Choisissez votre langue
  • +
  • Choisissez votre thème (clair, foncé ou noir)
  • +
  • Choisissez si vous voulez que votre thème soit dynamique (utilise les couleurs de votre arrière-plan)
  • +
  • Choisissez si vous voulez activer le formatage avancé, seulement les checklists ou garder vos notes basiques
  • +
diff --git a/fastlane/metadata/android/generate_full_description.py b/fastlane/metadata/android/generate_full_description.py index 9dcd0fde..fb82b844 100644 --- a/fastlane/metadata/android/generate_full_description.py +++ b/fastlane/metadata/android/generate_full_description.py @@ -14,7 +14,7 @@ if not os.path.isfile(yaml_filepath): continue - with open(yaml_filepath) as yaml_stream: + with open(yaml_filepath, mode="r", encoding="utf-8") as yaml_stream: try: # Read and minimize the description inside the YAML file yaml_file = yaml.safe_load(yaml_stream) @@ -25,7 +25,7 @@ # Write the description into the text file text_filepath = os.path.join(root, language, "full_description.txt") - with open(text_filepath, mode="w") as text_file: + with open(text_filepath, mode="w", encoding="utf-8") as text_file: text_file.write(full_description_minified) print("Generated full_description.txt for language " + language) diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..799fdd57 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1 @@ +

Material Notes — это приложение для создания текстовых заметок, ориентированное на простоту. Оно использует Material Design. Заметки хранятся локально и не имеют разрешений на выход в интернет, так что только вы можете получить доступ к своим заметкам.

Делайте заметки

  • Пишите текстовые заметки (заголовок и содержание)
  • Используйте расширенные параметры форматирования, включая списки
  • Отменяйте и повторяйте изменения во время редактирования
  • Используйте ярлык с главного экрана для быстрого добавления заметки

Организуйте

  • Ищите свои заметки
  • Сортируйте свои заметки по дате или заголовку, в порядке возрастания или убывания
  • Отображайте свои заметки в виде списка или в сеточном представлении
  • Закрепляйте свои заметки
  • Восстанавливайте удаленные заметки из корзины

Делитесь и создавайте резервные копии

  • Делитесь текстом из других приложений, чтобы добавить его прямо в заметку
  • Делитесь своими заметками в виде текста
  • Экспортируйте свои заметки в формате JSON, вручную или автоматически, и импортируйте их обратно
  • Экспортируйте свои заметки в формате Markdown

Защита

  • Никогда не беспокойтесь о том, как обрабатываются ваши данные: они не могут покинуть ваше устройство, так как приложение не имеет разрешений на выход в интернет
  • Шифруйте свои экспортированные JSON-данные

Настройки

  • Выберите свой язык
  • Выберите свою тему (светлая, темная или черная)
  • Выберите, хотите ли вы, чтобы ваша тема была динамичной (использовать системные цвета)
  • Выберите, хотите ли вы включить расширенное форматирование, только списки или оставить ваши заметки простыми
\ No newline at end of file diff --git a/fastlane/metadata/android/ru-RU/full_description.yaml b/fastlane/metadata/android/ru-RU/full_description.yaml new file mode 100644 index 00000000..0711aa4a --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.yaml @@ -0,0 +1,54 @@ +full_description: | +

+ Material Notes — это приложение для создания текстовых заметок, ориентированное на простоту. Оно использует Material Design. + Заметки хранятся локально и не имеют разрешений на выход в интернет, так что только вы можете получить доступ к своим заметкам. +

+ +

+ Делайте заметки +

+
    +
  • Пишите текстовые заметки (заголовок и содержание)
  • +
  • Используйте расширенные параметры форматирования, включая списки
  • +
  • Отменяйте и повторяйте изменения во время редактирования
  • +
  • Используйте ярлык с главного экрана для быстрого добавления заметки
  • +
+ +

+ Организуйте +

+
    +
  • Ищите свои заметки
  • +
  • Сортируйте свои заметки по дате или заголовку, в порядке возрастания или убывания
  • +
  • Отображайте свои заметки в виде списка или в сеточном представлении
  • +
  • Закрепляйте свои заметки
  • +
  • Восстанавливайте удаленные заметки из корзины
  • +
+ +

+ Делитесь и создавайте резервные копии +

+
    +
  • Делитесь текстом из других приложений, чтобы добавить его прямо в заметку
  • +
  • Делитесь своими заметками в виде текста
  • +
  • Экспортируйте свои заметки в формате JSON, вручную или автоматически, и импортируйте их обратно
  • +
  • Экспортируйте свои заметки в формате Markdown
  • +
+ +

+ Защита +

+
    +
  • Никогда не беспокойтесь о том, как обрабатываются ваши данные: они не могут покинуть ваше устройство, так как приложение не имеет разрешений на выход в интернет
  • +
  • Шифруйте свои экспортированные JSON-данные
  • +
+ +

+ Настройки +

+
    +
  • Выберите свой язык
  • +
  • Выберите свою тему (светлая, темная или черная)
  • +
  • Выберите, хотите ли вы, чтобы ваша тема была динамичной (использовать системные цвета)
  • +
  • Выберите, хотите ли вы включить расширенное форматирование, только списки или оставить ваши заметки простыми
  • +
diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..9899dc95 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Простые, локальные заметки в стиле Material Design \ No newline at end of file diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..7e3764fc --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Material Notes \ No newline at end of file diff --git a/fastlane/metadata/android/tr-TR/full_description.txt b/fastlane/metadata/android/tr-TR/full_description.txt deleted file mode 100644 index 0bd3bd6d..00000000 --- a/fastlane/metadata/android/tr-TR/full_description.txt +++ /dev/null @@ -1 +0,0 @@ -

Material Notes basitliği hedefleyen metin tabanlı bir not alma uygulamasıdır. Materyal Tasarımı benimser. Notları yerel olarak saklar ve internet izni yoktur, böylece notlara erişebilen tek kişi sizsiniz.

Not alın

  • Metin notları yazın (başlık ve içerik)
  • Kontrol listeleri de dahil olmak üzere gelişmiş biçimlendirme seçeneklerinden yararlanın
  • Düzenleme sırasında değişikliklerinizi geri alma ve yineleme
  • Hızlı bir şekilde not eklemek için ana ekranınızdaki hızlı eylemi kullanın

Organize Et

  • Notlarınızda arama yapın
  • Notlarınızı tarihe veya başlığa göre, artan veya azalan sırada sıralayın
  • Notlarınızı bir liste veya ızgara görünümünde görüntüleyin
  • Notlarınızı sabitleyin
  • Silinen notlarınızı çöp kutusundan kurtarın

Paylaş ve yedekle

  • Doğrudan bir nota eklemek için diğer uygulamalardan metin paylaşın
  • Notlarınızı metin olarak paylaşın
  • Notlarınızı JSON olarak dışa aktarın ve geri alın
  • Notlarınızı Markdown olarak dışa aktarın

Koruyun

  • Verilerinizin nasıl işlendiği konusunda asla endişelenmeyin: uygulama herhangi bir internet iznine sahip olmadığı için cihazınızdan ayrılamaz

Özelleştirme

  • Dilinizi seçin
  • Temanızı seçin (açık, koyu veya siyah)
  • Temanızın dinamik olmasını isteyip istemediğinizi seçin (arka planınızdaki renkleri kullanın)
  • Gelişmiş biçimlendirmeyi, yalnızca kontrol listelerini etkinleştirmek veya notlarınızı basit tutmak isteyip istemediğinizi seçin
\ No newline at end of file diff --git a/fastlane/metadata/android/tr-TR/full_description.yaml b/fastlane/metadata/android/tr-TR/full_description.yaml deleted file mode 100644 index daa4e68f..00000000 --- a/fastlane/metadata/android/tr-TR/full_description.yaml +++ /dev/null @@ -1,55 +0,0 @@ -full_description: | -

- Material Notes basitliği hedefleyen metin tabanlı bir not alma uygulamasıdır. Materyal Tasarımı benimser. - Notları yerel olarak saklar ve internet izni yoktur, böylece notlara erişebilen tek kişi sizsiniz. -

- -

- Not alın -

-
    -
  • Metin notları yazın (başlık ve içerik)
  • -
  • Kontrol listeleri de dahil olmak üzere gelişmiş biçimlendirme seçeneklerinden yararlanın
  • -
  • Düzenleme sırasında değişikliklerinizi geri alma ve yineleme
  • -
  • Hızlı bir şekilde not eklemek için ana ekranınızdaki hızlı eylemi kullanın
  • -
- -

- Organize Et -

-
    -
  • Notlarınızda arama yapın
  • -
  • Notlarınızı tarihe veya başlığa göre, artan veya azalan sırada sıralayın
  • -
  • Notlarınızı bir liste veya ızgara görünümünde görüntüleyin
  • -
  • Notlarınızı sabitleyin
  • -
  • Silinen notlarınızı çöp kutusundan kurtarın
  • -
- -

- Paylaş ve yedekle -

-
    -
  • Doğrudan bir nota eklemek için diğer uygulamalardan metin paylaşın
  • -
  • Notlarınızı metin olarak paylaşın
  • -
  • Notlarınızı JSON olarak dışa aktarın ve geri alın
  • -
  • Notlarınızı Markdown olarak dışa aktarın
  • -
- -

- Koruyun -

-
    -
  • Verilerinizin nasıl işlendiği konusunda asla endişelenmeyin: - uygulama herhangi bir internet iznine sahip olmadığı için cihazınızdan ayrılamaz
  • -
- -

- Özelleştirme -

-
    -
  • Dilinizi seçin
  • -
  • Temanızı seçin (açık, koyu veya siyah)
  • -
  • Temanızın dinamik olmasını isteyip istemediğinizi seçin (arka planınızdaki renkleri kullanın)
  • -
  • Gelişmiş biçimlendirmeyi, yalnızca kontrol listelerini etkinleştirmek veya notlarınızı basit - tutmak isteyip istemediğinizi seçin
  • -
diff --git a/l10n.yaml b/l10n.yaml index 23a2cdda..084487d6 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,5 +1,6 @@ format: true use-escaping: true +nullable-getter: false synthetic-package: false arb-dir: lib/l10n diff --git a/lib/app.dart b/lib/app.dart index 25f57614..5ea2fd70 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:localmaterialnotes/common/routing/router.dart'; import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/extensions/locale_extension.dart'; import 'package:localmaterialnotes/utils/locale_utils.dart'; @@ -47,13 +48,13 @@ class _AppState extends ConsumerState with AfterLayoutMixin { builder: (lightDynamicColorScheme, darkDynamicColorScheme) { return ValueListenableBuilder( valueListenable: dynamicThemingNotifier, - builder: (_, __, ___) { + builder: (context, dynamicTheming, child) { return ValueListenableBuilder( valueListenable: blackThemingNotifier, - builder: (_, __, ___) { + builder: (context, blackTheming, child) { return ValueListenableBuilder( valueListenable: themeModeNotifier, - builder: (_, themeMode, ___) { + builder: (context, themeMode, child) { return MaterialApp.router( title: 'Material Notes', builder: (context, child) { diff --git a/lib/common/actions/add.dart b/lib/common/actions/add.dart index fa8bbdad..c2ec0913 100644 --- a/lib/common/actions/add.dart +++ b/lib/common/actions/add.dart @@ -5,8 +5,8 @@ import 'package:localmaterialnotes/common/actions/select.dart'; import 'package:localmaterialnotes/common/routing/router.dart'; import 'package:localmaterialnotes/common/routing/router_route.dart'; import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; Future addNote(BuildContext context, WidgetRef ref, {String? content}) async { exitSelectionMode(ref); @@ -18,7 +18,7 @@ Future addNote(BuildContext context, WidgetRef ref, {String? content}) asy ref.read(notesProvider.notifier).edit(note); } - ref.read(currentNoteProvider.notifier).set(note); + currentNoteNotifier.value = note; if (!context.mounted) { return; diff --git a/lib/common/actions/back.dart b/lib/common/actions/back.dart index a9e2aa52..2b526c3d 100644 --- a/lib/common/actions/back.dart +++ b/lib/common/actions/back.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; -import 'package:localmaterialnotes/providers/editor_controller/editor_controller_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; void back(BuildContext context) { context.pop(); } void backFromEditor(BuildContext context, WidgetRef ref) { - ref.read(currentNoteProvider.notifier).reset(); - ref.read(editorControllerProvider.notifier).unset(); + currentNoteNotifier.value = null; + fleatherControllerNotifier.value = null; context.pop(); } diff --git a/lib/common/actions/delete.dart b/lib/common/actions/delete.dart index 945d10c0..f4796748 100644 --- a/lib/common/actions/delete.dart +++ b/lib/common/actions/delete.dart @@ -4,9 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:localmaterialnotes/common/dialogs/confirmation_dialog.dart'; import 'package:localmaterialnotes/models/note/note.dart'; import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/providers/selection_mode/selection_mode_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; Future deleteNote(BuildContext context, WidgetRef ref, Note? note) async { @@ -19,7 +18,8 @@ Future deleteNote(BuildContext context, WidgetRef ref, Note? note) async { localizations.dialog_delete_body_single, localizations.dialog_delete, )) { - ref.read(currentNoteProvider.notifier).reset(); + currentNoteNotifier.value = null; + await ref.read(notesProvider.notifier).delete(note); if (context.mounted && context.canPop()) { @@ -53,7 +53,7 @@ Future permanentlyDeleteNote(BuildContext context, WidgetRef ref, Note? no localizations.dialog_permanently_delete, irreversible: true, )) { - ref.read(currentNoteProvider.notifier).reset(); + currentNoteNotifier.value = null; await ref.read(binProvider.notifier).permanentlyDelete(note); @@ -85,7 +85,7 @@ Future emptyBin(WidgetRef ref) async { localizations.dialog_empty_bin, irreversible: true, )) { - ref.read(selectionModeProvider.notifier).exitSelectionMode(); + isSelectionModeNotifier.value = false; await ref.read(binProvider.notifier).empty(); } diff --git a/lib/common/actions/restore.dart b/lib/common/actions/restore.dart index 03be7e69..cb566b8a 100644 --- a/lib/common/actions/restore.dart +++ b/lib/common/actions/restore.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:localmaterialnotes/common/dialogs/confirmation_dialog.dart'; import 'package:localmaterialnotes/models/note/note.dart'; import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; Future restoreNote(BuildContext context, WidgetRef ref, Note? note) async { @@ -17,7 +17,7 @@ Future restoreNote(BuildContext context, WidgetRef ref, Note? note) async localizations.dialog_restore_body_single, localizations.dialog_restore, )) { - ref.read(currentNoteProvider.notifier).reset(); + currentNoteNotifier.value = null; await ref.read(binProvider.notifier).restore(note); diff --git a/lib/common/actions/select.dart b/lib/common/actions/select.dart index 6843b902..1516cd06 100644 --- a/lib/common/actions/select.dart +++ b/lib/common/actions/select.dart @@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:localmaterialnotes/common/routing/router_route.dart'; import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/providers/selection_mode/selection_mode_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; void selectAll(WidgetRef ref) { RouterRoute.isBin ? ref.read(binProvider.notifier).selectAll() : ref.read(notesProvider.notifier).selectAll(); @@ -15,5 +15,5 @@ void unselectAll(WidgetRef ref) { void exitSelectionMode(WidgetRef ref) { unselectAll(ref); - ref.read(selectionModeProvider.notifier).exitSelectionMode(); + isSelectionModeNotifier.value = false; } diff --git a/lib/common/dialogs/confirmation_dialog.dart b/lib/common/dialogs/confirmation_dialog.dart index 2d467b57..566aa4fb 100644 --- a/lib/common/dialogs/confirmation_dialog.dart +++ b/lib/common/dialogs/confirmation_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; -import 'package:localmaterialnotes/utils/preferences/confirmations.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/confirmations.dart'; Future _showConfirmationDialog( String title, @@ -10,7 +10,7 @@ Future _showConfirmationDialog( return await showAdaptiveDialog( context: navigatorKey.currentContext!, builder: (context) { - return AlertDialog( + return AlertDialog.adaptive( title: Text(title), content: SingleChildScrollView( child: Column( @@ -21,15 +21,11 @@ Future _showConfirmationDialog( ), actions: [ TextButton( - onPressed: () { - Navigator.pop(context, false); - }, + onPressed: () => Navigator.pop(context, false), child: Text(localizations.button_cancel), ), TextButton( - onPressed: () { - Navigator.pop(context, true); - }, + onPressed: () => Navigator.pop(context, true), child: Text(confirmText), ), ], diff --git a/lib/common/navigation/app_bars/back_app_bar.dart b/lib/common/navigation/app_bars/back_app_bar.dart deleted file mode 100644 index 1359faa1..00000000 --- a/lib/common/navigation/app_bars/back_app_bar.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:localmaterialnotes/common/actions/back.dart'; - -class BackAppBar extends ConsumerWidget { - const BackAppBar(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AppBar( - leading: BackButton( - onPressed: () => back(context), - ), - ); - } -} diff --git a/lib/common/navigation/app_bars/basic_app_bar.dart b/lib/common/navigation/app_bars/basic_app_bar.dart new file mode 100644 index 00000000..8580e6d7 --- /dev/null +++ b/lib/common/navigation/app_bars/basic_app_bar.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:localmaterialnotes/common/actions/back.dart'; +import 'package:localmaterialnotes/common/routing/router_route.dart'; + +class BasicAppBar extends ConsumerWidget { + const BasicAppBar() : showBack = false; + + const BasicAppBar.back() : showBack = true; + + final bool showBack; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppBar( + leading: showBack ? BackButton(onPressed: () => back(context)) : null, + title: Text(RouterRoute.currentRoute.title), + ); + } +} diff --git a/lib/common/navigation/app_bars/editor_app_bar.dart b/lib/common/navigation/app_bars/editor_app_bar.dart index f9c092b8..7c16d178 100644 --- a/lib/common/navigation/app_bars/editor_app_bar.dart +++ b/lib/common/navigation/app_bars/editor_app_bar.dart @@ -5,11 +5,10 @@ import 'package:localmaterialnotes/common/actions/back.dart'; import 'package:localmaterialnotes/common/actions/delete.dart'; import 'package:localmaterialnotes/common/actions/pin.dart'; import 'package:localmaterialnotes/common/actions/restore.dart'; -import 'package:localmaterialnotes/common/navigation/app_bars/menu_options.dart'; +import 'package:localmaterialnotes/common/navigation/menu_options.dart'; import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:localmaterialnotes/pages/editor/about_sheet.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; -import 'package:localmaterialnotes/providers/editor_controller/editor_controller_provider.dart'; +import 'package:localmaterialnotes/pages/editor/sheets/about_sheet.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/constants/paddings.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; @@ -27,7 +26,7 @@ class _BackAppBarState extends ConsumerState { // Manually close the keyboard FocusManager.instance.primaryFocus?.unfocus(); - final note = ref.read(currentNoteProvider); + final note = currentNoteNotifier.value; if (note == null) { return; @@ -57,7 +56,7 @@ class _BackAppBarState extends ConsumerState { } void _undo() { - final editorController = ref.read(editorControllerProvider); + final editorController = fleatherControllerNotifier.value; if (editorController == null || !editorController.canUndo) { return; @@ -67,7 +66,7 @@ class _BackAppBarState extends ConsumerState { } void _redo() { - final editorController = ref.read(editorControllerProvider); + final editorController = fleatherControllerNotifier.value; if (editorController == null || !editorController.canRedo) { return; @@ -77,7 +76,7 @@ class _BackAppBarState extends ConsumerState { } void _toggleChecklist() { - final editorController = ref.read(editorControllerProvider); + final editorController = fleatherControllerNotifier.value; if (editorController == null) { return; @@ -95,14 +94,15 @@ class _BackAppBarState extends ConsumerState { @override Widget build(BuildContext context) { - final note = ref.read(currentNoteProvider); + final note = currentNoteNotifier.value; + final editorController = fleatherControllerNotifier.value; final showUndoRedoButtons = PreferenceKey.showUndoRedoButtons.getPreferenceOrDefault(); final showChecklistButton = PreferenceKey.showChecklistButton.getPreferenceOrDefault(); return ValueListenableBuilder( valueListenable: fleatherFieldHasFocusNotifier, - builder: (_, hasFocus, ___) { + builder: (context, hasFocus, child) { return AppBar( leading: BackButton( onPressed: () => backFromEditor(context, ref), @@ -112,16 +112,28 @@ class _BackAppBarState extends ConsumerState { : [ if (!note.deleted) ...[ if (showUndoRedoButtons) - IconButton( - icon: const Icon(Icons.undo), - tooltip: localizations.tooltip_toggle_checkbox, - onPressed: hasFocus ? _undo : null, + ValueListenableBuilder( + valueListenable: fleatherControllerCanUndoNotifier, + builder: (context, canUndo, child) { + return IconButton( + icon: const Icon(Icons.undo), + tooltip: localizations.tooltip_toggle_checkbox, + onPressed: hasFocus && canUndo && editorController != null && editorController.canUndo + ? _undo + : null, + ); + }, ), if (showUndoRedoButtons) - IconButton( - icon: const Icon(Icons.redo), - tooltip: localizations.tooltip_toggle_checkbox, - onPressed: hasFocus ? _redo : null, + ValueListenableBuilder( + valueListenable: fleatherControllerCanRedoNotifier, + builder: (context, canRedo, child) { + return IconButton( + icon: const Icon(Icons.redo), + tooltip: localizations.tooltip_toggle_checkbox, + onPressed: hasFocus && canRedo ? _redo : null, + ); + }, ), if (showChecklistButton) IconButton( diff --git a/lib/common/navigation/app_bars/empty_app_bar.dart b/lib/common/navigation/app_bars/empty_app_bar.dart deleted file mode 100644 index 97159968..00000000 --- a/lib/common/navigation/app_bars/empty_app_bar.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -// ignore: always_use_package_imports -import '../../routing/router_route.dart'; - -class EmptyAppBar extends StatelessWidget { - const EmptyAppBar(); - - @override - Widget build(BuildContext context) { - return AppBar( - title: Text(RouterRoute.currentRoute.title), - ); - } -} diff --git a/lib/common/navigation/app_bars/notes_app_bar.dart b/lib/common/navigation/app_bars/notes_app_bar.dart index f71cc3e2..f5952c92 100644 --- a/lib/common/navigation/app_bars/notes_app_bar.dart +++ b/lib/common/navigation/app_bars/notes_app_bar.dart @@ -6,14 +6,14 @@ import 'package:localmaterialnotes/common/routing/router_route.dart'; import 'package:localmaterialnotes/common/widgets/note_tile.dart'; import 'package:localmaterialnotes/models/note/note.dart'; import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/layout/layout_provider.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/preferences/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/sort_method.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; -import 'package:localmaterialnotes/utils/preferences/sort_method.dart'; class NotesAppBar extends ConsumerStatefulWidget { const NotesAppBar({super.key}); @@ -27,7 +27,6 @@ class _SearchAppBarState extends ConsumerState { final provider = RouterRoute.currentRoute == RouterRoute.notes ? notesProvider : binProvider; - late Layout layout; SortMethod sortMethod = SortMethod.fromPreference(); bool sortAscending = PreferenceKey.sortAscending.getPreferenceOrDefault(); @@ -37,11 +36,11 @@ class _SearchAppBarState extends ConsumerState { } void _toggleLayout() { - final newLayout = layout == Layout.list ? Layout.grid : Layout.list; + final newLayout = layoutNotifier.value == Layout.list ? Layout.grid : Layout.list; PreferencesUtils().set(PreferenceKey.layout.name, newLayout.name); - ref.read(layoutStateProvider.notifier).set(newLayout); + layoutNotifier.value = newLayout; } void _sort({SortMethod? method, bool? ascending}) { @@ -84,8 +83,6 @@ class _SearchAppBarState extends ConsumerState { @override Widget build(BuildContext context) { - layout = ref.watch(layoutStateProvider) ?? Layout.fromPreference(); - final searchButtonPlaceholder = IconButton( onPressed: null, icon: const Icon(Icons.search), @@ -100,8 +97,10 @@ class _SearchAppBarState extends ConsumerState { actions: [ IconButton( onPressed: _toggleLayout, - tooltip: layout == Layout.list ? localizations.tooltip_layout_grid : localizations.tooltip_layout_list, - icon: Icon(layout == Layout.list ? Icons.grid_view : Icons.view_list_outlined), + tooltip: layoutNotifier.value == Layout.list + ? localizations.tooltip_layout_grid + : localizations.tooltip_layout_list, + icon: Icon(layoutNotifier.value == Layout.list ? Icons.grid_view : Icons.view_list_outlined), ), PopupMenuButton( icon: const Icon(Icons.sort), diff --git a/lib/common/navigation/app_bars/menu_options.dart b/lib/common/navigation/menu_options.dart similarity index 100% rename from lib/common/navigation/app_bars/menu_options.dart rename to lib/common/navigation/menu_options.dart diff --git a/lib/common/navigation/side_navigation.dart b/lib/common/navigation/side_navigation.dart index 2ba8574d..3c591e7f 100644 --- a/lib/common/navigation/side_navigation.dart +++ b/lib/common/navigation/side_navigation.dart @@ -17,6 +17,13 @@ class _SideNavigationState extends State { int _index = RouterRoute.currentDrawerIndex; void _navigate(int newIndex) { + // If the new route is the same as the current one, just close the drawer + if (_index == newIndex) { + Navigator.of(context).pop(); + + return; + } + setState(() { _index = newIndex; }); @@ -44,7 +51,6 @@ class _SideNavigationState extends State { children: [ Image.asset( Asset.icon.path, - filterQuality: FilterQuality.medium, fit: BoxFit.fitWidth, width: Sizes.size64.size, ), diff --git a/lib/common/navigation/top_navigation.dart b/lib/common/navigation/top_navigation.dart index 0c096e54..851e73e1 100644 --- a/lib/common/navigation/top_navigation.dart +++ b/lib/common/navigation/top_navigation.dart @@ -1,57 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:localmaterialnotes/common/navigation/app_bars/back_app_bar.dart'; -import 'package:localmaterialnotes/common/navigation/app_bars/editor_app_bar.dart'; -import 'package:localmaterialnotes/common/navigation/app_bars/empty_app_bar.dart'; -import 'package:localmaterialnotes/common/navigation/app_bars/notes_app_bar.dart'; import 'package:localmaterialnotes/common/navigation/app_bars/selection_app_bar.dart'; -import 'package:localmaterialnotes/providers/selection_mode/selection_mode_provider.dart'; -import 'package:localmaterialnotes/utils/constants/sizes.dart'; - -enum TopNavigationStyle { - empty, - back, - backMenu, - searchSort, -} +import 'package:localmaterialnotes/providers/notifiers.dart'; class TopNavigation extends ConsumerWidget implements PreferredSizeWidget { - const TopNavigation.empty() : topNavigationStyle = TopNavigationStyle.empty; - - const TopNavigation.back() : topNavigationStyle = TopNavigationStyle.back; - - const TopNavigation.backMenu() : topNavigationStyle = TopNavigationStyle.backMenu; + const TopNavigation({super.key, required this.appbar}); - const TopNavigation.searchSort({super.key}) : topNavigationStyle = TopNavigationStyle.searchSort; - - final TopNavigationStyle topNavigationStyle; + final Widget appbar; @override Size get preferredSize { - var height = kToolbarHeight; - - if (topNavigationStyle == TopNavigationStyle.searchSort) { - height += Sizes.custom.searchAppBar; - } - - return Size.fromHeight(height); + return const Size.fromHeight(kToolbarHeight); } @override Widget build(BuildContext context, WidgetRef ref) { - if (ref.watch(selectionModeProvider)) { - return const SelectionAppBar(); - } - - switch (topNavigationStyle) { - case TopNavigationStyle.empty: - return const EmptyAppBar(); - case TopNavigationStyle.back: - return const BackAppBar(); - case TopNavigationStyle.backMenu: - return const EditorAppBar(); - case TopNavigationStyle.searchSort: - return NotesAppBar(key: super.key); - } + return ValueListenableBuilder( + valueListenable: isSelectionModeNotifier, + builder: (BuildContext context, isSelectionMode, Widget? child) { + // If the selection mode is enabled, return the selection app bar + return isSelectionMode ? const SelectionAppBar() : appbar; + }, + ); } } diff --git a/lib/common/routing/router.dart b/lib/common/routing/router.dart index 6bc5b684..891af3e0 100644 --- a/lib/common/routing/router.dart +++ b/lib/common/routing/router.dart @@ -3,13 +3,21 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:go_router/go_router.dart'; import 'package:localmaterialnotes/common/fabs/fab_add_note.dart'; import 'package:localmaterialnotes/common/fabs/fab_empty_bin.dart'; +import 'package:localmaterialnotes/common/navigation/app_bars/basic_app_bar.dart'; +import 'package:localmaterialnotes/common/navigation/app_bars/editor_app_bar.dart'; +import 'package:localmaterialnotes/common/navigation/app_bars/notes_app_bar.dart'; import 'package:localmaterialnotes/common/navigation/side_navigation.dart'; import 'package:localmaterialnotes/common/navigation/top_navigation.dart'; import 'package:localmaterialnotes/common/routing/router_route.dart'; import 'package:localmaterialnotes/pages/bin/bin_page.dart'; import 'package:localmaterialnotes/pages/editor/editor_page.dart'; import 'package:localmaterialnotes/pages/notes/notes_page.dart'; -import 'package:localmaterialnotes/pages/settings/settings_page.dart'; +import 'package:localmaterialnotes/pages/settings/pages/settings_about_page.dart'; +import 'package:localmaterialnotes/pages/settings/pages/settings_appearance_page.dart'; +import 'package:localmaterialnotes/pages/settings/pages/settings_backup_page.dart'; +import 'package:localmaterialnotes/pages/settings/pages/settings_behavior_page.dart'; +import 'package:localmaterialnotes/pages/settings/pages/settings_editor_page.dart'; +import 'package:localmaterialnotes/pages/settings/settings_main_page.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; typedef EditorParameters = Map?; @@ -18,11 +26,26 @@ PreferredSizeWidget? _getAppBar(BuildContext context) { switch (RouterRoute.currentRoute) { case RouterRoute.notes: case RouterRoute.bin: - return TopNavigation.searchSort(key: UniqueKey()); + return TopNavigation( + key: UniqueKey(), + appbar: const NotesAppBar(), + ); case RouterRoute.editor: - return const TopNavigation.backMenu(); + return const TopNavigation( + appbar: EditorAppBar(), + ); case RouterRoute.settings: - return const TopNavigation.empty(); + return const TopNavigation( + appbar: BasicAppBar(), + ); + case RouterRoute.settingsAppearance: + case RouterRoute.settingsBehavior: + case RouterRoute.settingsEditor: + case RouterRoute.settingsBackup: + case RouterRoute.settingsAbout: + return const TopNavigation( + appbar: BasicAppBar.back(), + ); default: return null; } @@ -82,7 +105,29 @@ final router = GoRouter( ), GoRoute( path: RouterRoute.settings.path, - builder: (context, state) => const SettingsPage(), + builder: (context, state) => const SettingsMainPage(), + routes: [ + GoRoute( + path: RouterRoute.settingsAppearance.path, + builder: (context, state) => const SettingsAppearancePage(), + ), + GoRoute( + path: RouterRoute.settingsBehavior.path, + builder: (context, state) => const SettingsBehaviorPage(), + ), + GoRoute( + path: RouterRoute.settingsEditor.path, + builder: (context, state) => const SettingsEditorPage(), + ), + GoRoute( + path: RouterRoute.settingsBackup.path, + builder: (context, state) => const SettingsBackupPage(), + ), + GoRoute( + path: RouterRoute.settingsAbout.path, + builder: (context, state) => const SettingsAboutPage(), + ), + ], ), ], ), diff --git a/lib/common/routing/router_route.dart b/lib/common/routing/router_route.dart index 189f3765..dcfe4997 100644 --- a/lib/common/routing/router_route.dart +++ b/lib/common/routing/router_route.dart @@ -4,10 +4,20 @@ import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/extensions/go_router_extension.dart'; enum RouterRoute { + // Notes notes('/notes', drawerIndex: 0), editor('editor', fullPath: '/notes/editor'), + + // Bin bin('/bin', drawerIndex: 1), + + // Settings settings('/settings', drawerIndex: 2), + settingsAppearance('appearance', fullPath: '/settings/appearance'), + settingsBehavior('behavior', fullPath: '/settings/behavior'), + settingsEditor('editor', fullPath: '/settings/editor'), + settingsBackup('backup', fullPath: '/settings/backup'), + settingsAbout('about', fullPath: '/settings/about'), ; final String path; @@ -24,8 +34,18 @@ enum RouterRoute { return localizations.navigation_bin; case settings: return localizations.navigation_settings; + case settingsAppearance: + return localizations.navigation_settings_appearance; + case settingsBehavior: + return localizations.navigation_settings_behavior; + case settingsEditor: + return localizations.navigation_settings_editor; + case settingsBackup: + return localizations.navigation_settings_backup; + case settingsAbout: + return localizations.navigation_settings_about; default: - throw Exception('Unexpected route: $this'); + throw Exception('Unexpected route while getting its title: $this'); } } @@ -50,18 +70,28 @@ enum RouterRoute { } static RouterRoute get currentRoute { - final location = GoRouter.of(navigatorKey.currentContext!).location(); + final location = GoRouter.of(navigatorKey.currentContext!).location; if (location == notes.path) { return notes; - } else if (location.contains(editor.path)) { + } else if (location == editor.fullPath) { return editor; } else if (location == bin.path) { return bin; } else if (location == settings.path) { return settings; + } else if (location == settingsAppearance.fullPath) { + return settingsAppearance; + } else if (location == settingsBehavior.fullPath) { + return settingsBehavior; + } else if (location == settingsEditor.fullPath) { + return settingsEditor; + } else if (location == settingsBackup.fullPath) { + return settingsBackup; + } else if (location == settingsAbout.fullPath) { + return settingsAbout; } else { - throw Exception('Unexpected route: $location'); + throw Exception('Unexpected current route: $location'); } } diff --git a/lib/common/widgets/dismissibleWidgets/dismissible_delete_widget.dart b/lib/common/widgets/dismissibleWidgets/dismissible_delete_widget.dart new file mode 100644 index 00000000..dce8c08b --- /dev/null +++ b/lib/common/widgets/dismissibleWidgets/dismissible_delete_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_direction.dart'; + +class DismissibleDeleteWidget extends StatelessWidget { + const DismissibleDeleteWidget({ + super.key, + required this.swipeDirection, + }); + + final SwipeDirection swipeDirection; + + @override + Widget build(BuildContext context) { + const icon = Icon(Icons.delete); + final text = Text( + localizations.dismiss_delete, + style: Theme.of(context).textTheme.titleMedium, + ); + + return ColoredBox( + color: Colors.red, + child: Padding( + padding: Paddings.padding16.horizontal, + child: Row( + mainAxisAlignment: swipeDirection == SwipeDirection.right ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if (swipeDirection == SwipeDirection.right) icon else text, + Padding(padding: Paddings.padding4.horizontal), + if (swipeDirection == SwipeDirection.right) text else icon, + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/dismissibleWidgets/dismissible_pin_widget.dart b/lib/common/widgets/dismissibleWidgets/dismissible_pin_widget.dart new file mode 100644 index 00000000..50774975 --- /dev/null +++ b/lib/common/widgets/dismissibleWidgets/dismissible_pin_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/models/note/note.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_direction.dart'; + +class DismissiblePinWidget extends StatelessWidget { + const DismissiblePinWidget({ + super.key, + required this.note, + required this.swipeDirection, + }); + + final Note note; + final SwipeDirection swipeDirection; + + @override + Widget build(BuildContext context) { + final icon = Icon(note.pinned ? Icons.push_pin_outlined : Icons.push_pin); + final text = Text( + note.pinned ? localizations.dismiss_unpin : localizations.dismiss_pin, + style: Theme.of(context).textTheme.titleMedium, + ); + + return ColoredBox( + color: Colors.blue, + child: Padding( + padding: Paddings.padding16.horizontal, + child: Row( + mainAxisAlignment: swipeDirection == SwipeDirection.right ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if (swipeDirection == SwipeDirection.right) icon else text, + Padding(padding: Paddings.padding4.horizontal), + if (swipeDirection == SwipeDirection.right) text else icon, + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/encrypt_password_form.dart b/lib/common/widgets/encrypt_password_form.dart new file mode 100644 index 00000000..c20f09c2 --- /dev/null +++ b/lib/common/widgets/encrypt_password_form.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/common/widgets/password_field.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; + +class EncryptionPasswordForm extends StatefulWidget { + const EncryptionPasswordForm({ + super.key, + required this.secondaryDescription, + required this.onChanged, + required this.onEditingComplete, + }); + + final String secondaryDescription; + + final Function(bool, String?) onChanged; + final Function() onEditingComplete; + + @override + State createState() => _EncryptionPasswordFormState(); +} + +class _EncryptionPasswordFormState extends State { + bool _encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault(); + + void _toggleEncrypt(_) { + setState(() { + _encrypt = !_encrypt; + }); + + _onChanged(null); + } + + void _onChanged(String? password) { + widget.onChanged(_encrypt, password); + } + + void _onEditingComplete() { + widget.onEditingComplete(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text(localizations.dialog_export_encryption_switch), + ), + Switch( + value: _encrypt, + onChanged: _toggleEncrypt, + ), + ], + ), + if (_encrypt) ...[ + Padding(padding: Paddings.padding8.vertical), + PasswordField( + description: localizations.dialog_export_encryption_description, + secondaryDescription: widget.secondaryDescription, + onChanged: _onChanged, + onEditingComplete: _onEditingComplete, + ), + ], + ], + ); + } +} diff --git a/lib/common/widgets/note_tile.dart b/lib/common/widgets/note_tile.dart index 57fa0b7b..bf2a59a2 100644 --- a/lib/common/widgets/note_tile.dart +++ b/lib/common/widgets/note_tile.dart @@ -3,28 +3,35 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:localmaterialnotes/common/actions/delete.dart'; import 'package:localmaterialnotes/common/actions/pin.dart'; -import 'package:localmaterialnotes/common/actions/restore.dart'; import 'package:localmaterialnotes/common/routing/router.dart'; import 'package:localmaterialnotes/common/routing/router_route.dart'; +import 'package:localmaterialnotes/common/widgets/dismissibleWidgets/dismissible_delete_widget.dart'; +import 'package:localmaterialnotes/common/widgets/dismissibleWidgets/dismissible_pin_widget.dart'; import 'package:localmaterialnotes/models/note/note.dart'; import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/providers/selection_mode/selection_mode_provider.dart'; -import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/paddings.dart'; import 'package:localmaterialnotes/utils/constants/radiuses.dart'; import 'package:localmaterialnotes/utils/constants/sizes.dart'; -import 'package:localmaterialnotes/utils/preferences/layout.dart'; -import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_direction.dart'; -/// Creates a custom `ListTile` because the default one doesn't allow not displaying a title. +/// List tile that displays the main info about a note. +/// +/// A custom `ListTile` widget is created because the default one doesn't allow not displaying a title. class NoteTile extends ConsumerStatefulWidget { + /// Note tile shown in the notes list or the bin. const NoteTile(this.note) : searchView = false; + /// Note tile shown in the search view. const NoteTile.searchView(this.note) : searchView = true; + /// Note to display. final Note note; + + /// Whether the note tile is shown in the search view. final bool searchView; @override @@ -32,15 +39,128 @@ class NoteTile extends ConsumerStatefulWidget { } class _NoteTileState extends ConsumerState { + /// Returns the background color of the note tile. + /// + /// The background color depends on: + /// - Whether the tile is shown in the search view. + /// - Whether the note is selected. + /// - Whether the [showTilesBackground] setting is enabled. + /// + /// If none are `true`, then `null` is returned. + Color? _backgroundColor(bool showTilesBackground) { + if (widget.searchView) { + return Theme.of(context).colorScheme.surfaceContainerHigh; + } else if (widget.note.selected) { + return Theme.of(context).colorScheme.secondaryContainer; + } else if (showTilesBackground) { + return Theme.of(context).colorScheme.surfaceContainerHighest; + } else { + return null; + } + } + + /// Returns the border radius of the note tile. + /// + /// The border radius depends on: + /// - Whether the layout is [Layout.grid]. + /// - Whether the [showTilesBackground] setting is enabled. + /// + /// If neither are `true`, then [BorderRadius.zero] is returned. + BorderRadius _borderRadius(Layout layout, bool showTilesBackground) { + return layout == Layout.grid || showTilesBackground ? Radiuses.radius16.circular : BorderRadius.zero; + } + + /// Returns the [DismissDirection] of the note tile, depending on the [rightSwipeAction] and the [leftSwipeAction]. + /// + /// There are 4 possible outputs: + /// - right and left : [DismissDirection.horizontal] + /// - right only : [DismissDirection.startToEnd] + /// - left only : [DismissDirection.endToStart] + /// - none : [DismissDirection.none] + DismissDirection _dismissDirection(SwipeAction rightSwipeAction, SwipeAction leftSwipeAction) { + // If the note is deleted, no swipe actions are available + if (widget.note.deleted) { + return DismissDirection.none; + } + + if (rightSwipeAction.isEnabled && leftSwipeAction.isEnabled) { + return DismissDirection.horizontal; + } else if (rightSwipeAction.isEnabled && leftSwipeAction.isDisabled) { + return DismissDirection.startToEnd; + } else if (leftSwipeAction.isEnabled && rightSwipeAction.isDisabled) { + return DismissDirection.endToStart; + } else { + return DismissDirection.none; + } + } + + /// Returns the main and the secondary dismissible widgets, depending on the [direction]. + /// + /// There are 4 possible outputs: + /// - right and left : ([_rightDismissibleWidget], [_leftDismissibleWidget]) + /// - right only : ([_rightDismissibleWidget], null) + /// - left only : ([_leftDismissibleWidget], null) + /// - none : (null, null) + (Widget?, Widget?) _dismissibleWidgets( + DismissDirection direction, + SwipeAction rightSwipeAction, + SwipeAction leftSwipeAction, + ) { + // If the note is deleted, the secondary swipe action is not available + if (widget.note.deleted) { + return (null, null); + } + + switch (direction) { + case DismissDirection.none: + return (null, null); + case DismissDirection.horizontal: + return (_rightDismissibleWidget(rightSwipeAction), _leftDismissibleWidget(leftSwipeAction)); + case DismissDirection.startToEnd: + return (_rightDismissibleWidget(rightSwipeAction), null); + case DismissDirection.endToStart: + return (_leftDismissibleWidget(leftSwipeAction), null); + default: + throw Exception('Unexpected dismiss direction while building dismissible widgets: $direction'); + } + } + + /// Returns the right dismissible background widget, depending on the [rightSwipeAction]. + Widget _rightDismissibleWidget(SwipeAction rightSwipeAction) { + switch (rightSwipeAction) { + case SwipeAction.delete: + return const DismissibleDeleteWidget(swipeDirection: SwipeDirection.right); + case SwipeAction.pin: + return DismissiblePinWidget(note: widget.note, swipeDirection: SwipeDirection.right); + default: + throw Exception('Unexpected build of the right dismissible widget while it is disabled'); + } + } + + /// Returns the left dismissible background widget, depending the [leftSwipeAction]. + Widget _leftDismissibleWidget(SwipeAction leftSwipeAction) { + switch (leftSwipeAction) { + case SwipeAction.delete: + return const DismissibleDeleteWidget(swipeDirection: SwipeDirection.left); + case SwipeAction.pin: + return DismissiblePinWidget(note: widget.note, swipeDirection: SwipeDirection.left); + default: + throw Exception('Unexpected build of the left dismissible widget while it is disabled'); + } + } + + /// Enters the selection mode and selects this note. void _enterSelectionMode() { - ref.read(selectionModeProvider.notifier).enterSelectionMode(); + isSelectionModeNotifier.value = true; + widget.note.deleted ? ref.read(binProvider.notifier).select(widget.note) : ref.read(notesProvider.notifier).select(widget.note); } + /// Opens the editor for this note or selects this note, depending on whether the selection mode is enabled or not. void _openOrSelect() { - if (ref.watch(selectionModeProvider)) { + if (isSelectionModeNotifier.value) { if (widget.note.deleted) { widget.note.selected ? ref.read(binProvider.notifier).unselect(widget.note) @@ -51,169 +171,144 @@ class _NoteTileState extends ConsumerState { : ref.read(notesProvider.notifier).select(widget.note); } } else { - ref.read(currentNoteProvider.notifier).set(widget.note); + currentNoteNotifier.value = widget.note; context.push( RouterRoute.editor.fullPath!, extra: EditorParameters.from({'readonly': widget.note.deleted, 'autofocus': false}), ); + // If the note was opened from the search view, it need to be closed. if (widget.searchView) { context.pop(); } } } - Future _dismiss(DismissDirection direction) { + /// Executes the [rightSwipeAction] or the [leftSwipeAction] depending on the [direction] that has been swiped. + Future _dismiss(DismissDirection direction, SwipeAction rightSwipeAction, SwipeAction leftSwipeAction) { switch (direction) { case DismissDirection.startToEnd: - return widget.note.deleted - ? permanentlyDeleteNote(context, ref, widget.note) - : deleteNote(context, ref, widget.note); + switch (rightSwipeAction) { + case SwipeAction.delete: + return deleteNote(context, ref, widget.note); + case SwipeAction.pin: + return togglePinNote(context, ref, widget.note); + default: + throw Exception('Unexpected right swipe action when swiping on note tile: $rightSwipeAction'); + } case DismissDirection.endToStart: - return widget.note.deleted ? restoreNote(context, ref, widget.note) : togglePinNote(context, ref, widget.note); + switch (leftSwipeAction) { + case SwipeAction.delete: + return deleteNote(context, ref, widget.note); + case SwipeAction.pin: + return togglePinNote(context, ref, widget.note); + default: + throw Exception('Unexpected left swipe action when swiping on note tile: $rightSwipeAction'); + } default: - return Future.value(false); + throw Exception('Unexpected dismiss direction after swiping on note tile: $direction'); } } @override Widget build(BuildContext context) { - final layout = Layout.fromPreference(); - final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault(); - - Color? color; - if (widget.note.selected) { - color = Theme.of(context).colorScheme.secondaryContainer; - } else if (showTilesBackground) { - color = Theme.of(context).colorScheme.surfaceContainerHighest; - } - - // Wrap with Material to fix the tile background color not updating in real time - // when the tile is selected and the view is scrolled - // cf. https://github.com/flutter/flutter/issues/86584 - final tile = Material( - child: Ink( - color: color, - child: InkWell( - onTap: _openOrSelect, - onLongPress: widget.searchView ? null : _enterSelectionMode, - child: Padding( - padding: Paddings.padding16.all, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title - if (!widget.note.isTitleEmpty) - Text( - widget.note.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - // Subtitle - if (!widget.note.isContentEmpty && !widget.note.isContentPreviewEmpty) - Text( - widget.note.contentPreview, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(175), - ), - ), + final tile = ValueListenableBuilder( + valueListenable: showTilesBackgroundNotifier, + builder: (BuildContext context, showTilesBackground, Widget? child) { + // Wrap the custom tile with Material to fix the tile background color not updating in real time when the tile is selected and the view is scrolled + // See https://github.com/flutter/flutter/issues/86584 + return Material( + child: Ink( + color: _backgroundColor(showTilesBackground), + child: InkWell( + onTap: _openOrSelect, + onLongPress: widget.searchView ? null : _enterSelectionMode, + child: Padding( + padding: Paddings.padding16.all, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + if (!widget.note.isTitleEmpty) + Text( + widget.note.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + // Subtitle + if (!widget.note.isContentEmpty && !widget.note.isContentPreviewEmpty) + Text( + widget.note.contentPreview, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(175), + ), + ), + ], + ), + ), + // Trailing + if (widget.note.pinned && !widget.note.deleted) ...[ + Padding(padding: Paddings.padding2.horizontal), + Icon( + Icons.push_pin, + size: Sizes.size16.size, + ), ], - ), + ], ), - // Trailing - if (widget.note.pinned && !widget.note.deleted) ...[ - Padding(padding: Paddings.padding2.horizontal), - Icon( - Icons.push_pin, - size: Sizes.size16.size, - ), - ], - ], + ), ), ), - ), - ), + ); + }, ); - return ClipRRect( - borderRadius: showTilesBackground ? Radiuses.radius16.circular : BorderRadius.zero, - clipBehavior: showTilesBackground ? Clip.antiAlias : Clip.none, - child: layout == Layout.list - ? Dismissible( - key: Key(widget.note.id.toString()), - background: widget.note.deleted - ? ColoredBox( - color: Colors.red, - child: Row( - children: [ - Padding(padding: Paddings.padding8.horizontal), - const Icon(Icons.delete_forever), - Padding(padding: Paddings.padding4.horizontal), - Text( - localizations.dismiss_permanently_delete, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ) - : ColoredBox( - color: Colors.red, - child: Row( - children: [ - Padding(padding: Paddings.padding8.horizontal), - const Icon(Icons.delete), - Padding(padding: Paddings.padding4.horizontal), - Text( - localizations.dismiss_delete, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ), - secondaryBackground: widget.note.deleted - ? ColoredBox( - color: Colors.blue, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Icon(Icons.restore_from_trash), - Padding(padding: Paddings.padding4.horizontal), - Text( - localizations.dismiss_restore, - style: Theme.of(context).textTheme.titleMedium, - ), - Padding(padding: Paddings.padding8.horizontal), - ], - ), - ) - : ColoredBox( - color: Colors.blue, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - widget.note.pinned ? Icons.push_pin_outlined : Icons.push_pin, - ), - Padding(padding: Paddings.padding4.horizontal), - Text( - widget.note.pinned ? localizations.dismiss_unpin : localizations.dismiss_pin, - style: Theme.of(context).textTheme.titleMedium, - ), - Padding(padding: Paddings.padding8.horizontal), - ], - ), - ), - confirmDismiss: _dismiss, - child: tile, - ) - : tile, + // In search view, just return the plain tile, without the ClipRRect or the Dismissible widgets + if (widget.searchView) { + return tile; + } + + return ValueListenableBuilder( + valueListenable: layoutNotifier, + builder: (BuildContext context, layout, Widget? child) { + return ValueListenableBuilder( + valueListenable: showTilesBackgroundNotifier, + builder: (BuildContext context, showTilesBackground, Widget? child) { + return ValueListenableBuilder( + valueListenable: swipeActionsNotifier, + builder: (BuildContext context, swipeActions, Widget? child) { + final rightSwipeAction = swipeActions.$1; + final leftSwipeAction = swipeActions.$2; + + final direction = _dismissDirection(rightSwipeAction, leftSwipeAction); + + final dismissibleWidgets = _dismissibleWidgets(direction, rightSwipeAction, leftSwipeAction); + final dismissibleMainWidget = dismissibleWidgets.$1; + final dismissibleSecondaryWidget = dismissibleWidgets.$2; + + return ClipRRect( + borderRadius: _borderRadius(layout, showTilesBackground), + child: Dismissible( + key: Key(widget.note.id.toString()), + direction: direction, + background: dismissibleMainWidget, + secondaryBackground: dismissibleSecondaryWidget, + confirmDismiss: (dismissDirection) => _dismiss(dismissDirection, rightSwipeAction, leftSwipeAction), + child: tile, + ), + ); + }, + ); + }, + ); + }, ); } } diff --git a/lib/common/widgets/notes_list.dart b/lib/common/widgets/notes_list.dart new file mode 100644 index 00000000..e478ef7a --- /dev/null +++ b/lib/common/widgets/notes_list.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:localmaterialnotes/common/placeholders/empty_placeholder.dart'; +import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart'; +import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart'; +import 'package:localmaterialnotes/common/routing/router_route.dart'; +import 'package:localmaterialnotes/common/widgets/note_tile.dart'; +import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; +import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/constants/separators.dart'; +import 'package:localmaterialnotes/utils/constants/sizes.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/layout.dart'; + +class NotesList extends ConsumerStatefulWidget { + const NotesList.notes() : route = RouterRoute.notes; + + const NotesList.bin() : route = RouterRoute.bin; + + final RouterRoute route; + + @override + ConsumerState createState() => _NotesListState(); +} + +class _NotesListState extends ConsumerState { + @override + Widget build(BuildContext context) { + final isNotes = widget.route == RouterRoute.notes; + final provider = isNotes ? notesProvider : binProvider; + + return ref.watch(provider).when( + data: (notes) { + if (notes.isEmpty) { + return isNotes ? EmptyPlaceholder.notes() : EmptyPlaceholder.bin(); + } + + // Use at least 2 columns for the grid view + final columnsCount = MediaQuery.of(context).size.width ~/ Sizes.custom.gridLayoutColumnWidth; + final crossAxisCount = columnsCount > 2 ? columnsCount : 2; + + return ValueListenableBuilder( + valueListenable: layoutNotifier, + builder: (context, layout, child) { + return ValueListenableBuilder( + valueListenable: showTilesBackgroundNotifier, + builder: (context, showTilesBackground, child) { + return ValueListenableBuilder( + valueListenable: showSeparatorsNotifier, + builder: (context, showSeparators, child) { + return layout == Layout.list + ? ListView.separated( + padding: showTilesBackground ? Paddings.custom.notesWithBackground : Paddings.custom.fab, + itemCount: notes.length, + itemBuilder: (context, index) { + return NoteTile(notes[index]); + }, + separatorBuilder: (BuildContext context, int index) { + return Padding( + padding: showTilesBackground + ? Paddings.custom.notesListViewWithBackgroundSeparation + : EdgeInsetsDirectional.zero, + child: showSeparators ? Separator.divider1indent8.horizontal : null, + ); + }, + ) + : AlignedGridView.count( + padding: Paddings.custom.notesWithBackground, + mainAxisSpacing: Sizes.custom.notesGridViewSpacing, + crossAxisSpacing: Sizes.custom.notesGridViewSpacing, + crossAxisCount: crossAxisCount, + itemCount: notes.length, + itemBuilder: (context, index) { + return NoteTile(notes[index]); + }, + ); + }, + ); + }, + ); + }, + ); + }, + error: (error, stackTrace) { + return const ErrorPlaceholder(); + }, + loading: () { + return const LoadingPlaceholder(); + }, + ); + } +} diff --git a/lib/common/widgets/password_field.dart b/lib/common/widgets/password_field.dart new file mode 100644 index 00000000..771f283b --- /dev/null +++ b/lib/common/widgets/password_field.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; + +class PasswordField extends StatefulWidget { + const PasswordField({ + super.key, + this.description, + this.secondaryDescription, + required this.onChanged, + required this.onEditingComplete, + }); + + final String? description; + final String? secondaryDescription; + + final Function(String?) onChanged; + final Function() onEditingComplete; + + @override + State createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _obscurePassword = true; + + final _formKey = GlobalKey(); + final _passwordController = TextEditingController(); + + void _toggleObscurePassword() { + setState(() { + _obscurePassword = !_obscurePassword; + }); + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return null; + } + + if (!(value.length == 32) || !value.isStrongPassword) { + return localizations.dialog_export_encryption_password_invalid; + } + + return null; + } + + void _onChanged() { + if (!_formKey.currentState!.validate()) { + widget.onChanged(null); + + return; + } + + widget.onChanged(_passwordController.text); + } + + void _onEditingComplete() { + if (!_formKey.currentState!.validate()) { + return; + } + + widget.onEditingComplete(); + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + children: [ + if (widget.description != null) ...[ + Text(widget.description!), + Padding(padding: Paddings.padding8.vertical), + ], + if (widget.secondaryDescription != null) ...[ + Text(widget.secondaryDescription!), + Padding(padding: Paddings.padding8.vertical), + ], + AutofillGroup( + child: TextFormField( + controller: _passwordController, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.password], + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off), + onPressed: _toggleObscurePassword, + ), + hintText: localizations.dialog_export_encryption_password_hint, + ), + obscureText: _obscurePassword, + autocorrect: false, + enableSuggestions: false, + maxLength: 32, + validator: _validatePassword, + onChanged: (_) => _onChanged(), + onEditingComplete: _onEditingComplete, + ), + ), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 76319afe..366e29e3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2,51 +2,100 @@ "app_name": "Material Notes", "app_tagline": "Simple, local, material design notes", "app_about": "{appName} is a text-based note-taking application, aimed at simplicity. It embraces Material Design. It stores the notes locally and doesn''t have any internet permissions, so you are the only one that can access the notes.", - "navigation_notes": "Notes", - "navigation_bin": "Bin", - "navigation_settings": "Settings", "error_error": "Error", "error_permission": "Failed to get permission to write the file.", "error_read_file": "Failed to read the file.", + "navigation_notes": "Notes", + "navigation_bin": "Bin", + "navigation_settings": "Settings", + "navigation_settings_appearance": "Appearance", + "navigation_settings_behavior": "Behavior", + "navigation_settings_editor": "Editor", + "navigation_settings_backup": "Backup", + "navigation_settings_about": "About", + "button_ok": "Ok", + "button_close": "Close", + "button_cancel": "Cancel", + "button_add": "Add", "settings_appearance": "Appearance", + "settings_appearance_description": "Language, theme, notes tiles", + "settings_appearance_application": "Application", + "settings_appearance_notes_tiles": "Notes tiles", "settings_language": "Language", "settings_theme": "Theme", "settings_theme_system": "System", "settings_theme_light": "Light", "settings_theme_dark": "Dark", "settings_dynamic_theming": "Dynamic theming", - "settings_dynamic_theming_description": "Generate colors from your system", + "settings_dynamic_theming_description": "Generate colors from the system", "settings_black_theming": "Black theming", "settings_black_theming_description": "Use a black background in dark mode", + "settings_show_separators": "Separators", + "settings_show_separators_description": "Show a separator between the notes tiles to differentiate them easily", + "settings_show_tiles_background": "Background", + "settings_show_tiles_background_description": "Show the background of the notes tiles to differentiate them easily", + "settings_behavior": "Behavior", + "settings_behavior_application": "Application", + "settings_behavior_description": "Confirmations, swipe actions", + "settings_behavior_swipe_actions": "Swipe actions", + "settings_confirmations": "Confirmation dialogs", + "settings_confirmations_description": "Show the confirmation dialogs for actions such as pining and deleting notes", + "settings_swipe_action_right": "Right swipe action", + "settings_swipe_action_right_description": "Action to trigger when a right swipe is performed on the notes tiles", + "settings_swipe_action_left": "Left swipe action", + "settings_swipe_action_left_description": "Action to trigger when a left swipe is performed on the notes tiles", + "settings_flag_secure": "Flag the app as secure", + "settings_flag_secure_description": "Hide the app from the recent apps and prevent screenshots from being made", "settings_editor": "Editor", + "settings_editor_formatting": "Formatting", + "settings_editor_appearance": "Appearance", + "settings_editor_description": "Buttons, toolbar, spacing", "settings_show_undo_redo_buttons": "Undo/redo buttons", - "settings_show_undo_redo_buttons_description": "Show the buttons to undo and redo changes in the editor", + "settings_show_undo_redo_buttons_description": "Show the buttons to undo and redo changes in the editor''s app bar", "settings_show_checklist_button": "Checklist button", - "settings_show_checklist_button_description": "Show the button to toggle checklists in the editor", - "settings_show_toolbar": "Editor toolbar", - "settings_show_toolbar_description": "Show the editor toolbar to enable advanced text formatting", - "settings_show_separators": "Show the separators", - "settings_show_separators_description": "Show a separator between notes to differentiate them easily", - "settings_show_tiles_background": "Show the tiles background", - "settings_show_tiles_background_description": "Show the background of the notes tiles to differentiate them easily", - "settings_behavior": "Behavior", - "settings_confirmations": "Show confirmation dialogs", + "settings_show_checklist_button_description": "Show the button to toggle checklists in the editor''s app bar, hiding it from the editor''s toolbar if enabled", + "settings_show_toolbar": "Toolbar", + "settings_show_toolbar_description": "Show the editor''s toolbar to enable advanced text formatting", + "settings_use_paragraph_spacing": "Paragraph spacing", + "settings_use_paragraph_spacing_description": "Use spacing between paragraphs", "settings_backup": "Backup", + "settings_backup_description": "Export, import", + "settings_backup_export": "Export", + "settings_backup_import": "Import", + "settings_auto_export": "Auto export as JSON", + "settings_auto_export_description": "Automatically export the notes to a JSON file (bin included) that can be imported back", + "settings_auto_export_value": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}, {encrypt, select, true{encrypted} false{not encrypted} other{}}", + "settings_auto_export_disabled": "Disabled", + "settings_auto_export_directory": "Exports can be found in {directory}", + "settings_auto_export_dialog_description_disabled": "Auto export will be disabled.", + "settings_auto_export_dialog_description_enabled": "Auto export will be performed every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}. Set the frequency to 0 to disable it.", + "settings_auto_export_dialog_slider_label": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}", + "settings_export_success": "The notes were successfully exported.", "settings_export_json": "Export as JSON", + "settings_export_json_description": "Immediately export the notes to a JSON file (bin included) that can be imported back", "settings_export_markdown": "Export as Markdown", - "settings_export_json_description": "Export notes to a JSON file (bin included) that can be imported back", - "settings_export_markdown_description": "Export notes to a Markdown file (bin included)", - "settings_export_success": "The notes were successfully exported.", + "settings_export_markdown_description": "Immediately export the notes to a Markdown file (bin included)", "settings_import": "Import", "settings_import_description": "Import notes from a JSON file", "settings_import_success": "The notes were successfully imported.", "settings_about": "About", + "settings_about_application": "Application", + "settings_about_links": "Links", + "settings_about_help": "Help", + "settings_about_description": "Information, help, GitHub, license", + "settings_build_mode": "Build mode", + "settings_build_mode_release": "Release", + "settings_build_mode_debug": "Debug", "settings_github": "GitHub", "settings_github_description": "Take a look at the source code", "settings_licence": "License", "settings_licence_description": "AGPL-3.0", - "settings_issue": "Report a bug", - "settings_issue_description": "Report a bug by creating an issue on GitHub", + "settings_github_issues": "Report a bug", + "settings_github_issues_description": "Report a bug by creating a GitHub issue", + "settings_github_discussions": "Ask a question", + "settings_github_discussions_description": "Ask a question on GitHub discussions", + "settings_get_in_touch": "Contact the developer", + "settings_get_in_touch_description": "Contact the developer via mail at contact@maelchiotti.dev", "hint_title": "Title", "hint_note": "Note", "tooltip_fab_add_note": "Add a note", @@ -62,10 +111,6 @@ "tooltip_permanently_delete": "Permanently delete", "tooltip_restore": "Restore", "tooltip_toggle_pins": "Toggle pins", - "button_ok": "Ok", - "button_close": "Close", - "button_cancel": "Cancel", - "button_add": "Add", "dialog_delete": "Delete", "dialog_delete_body": "Do you really want to delete {count} {count, plural, zero{} one{note} other{notes}}? You can restore {count, plural, zero{} one{it} other{them}} from the bin.", "dialog_delete_body_single": "Do you really want to delete this note? You can restore it from the bin.", @@ -77,11 +122,19 @@ "dialog_restore_body_single": "Do you really want to restore this note?", "dialog_empty_bin": "Empty the bin", "dialog_empty_bin_body": "Do you really want to permanently empty the bin? You will not be able to restore the notes it contains.", + "dialog_export_encryption_switch": "Encrypt the JSON export", + "dialog_export_encryption_description": "The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.", + "dialog_export_encryption_secondary_description_auto": "This password will be used for all future auto exports.", + "dialog_export_encryption_secondary_description_manual": "This password will only be used for this export.", + "dialog_export_encryption_password_hint": "Password", + "dialog_export_encryption_password_invalid": "Invalid", + "dialog_import_encryption_password_description": "This export is encrypted. To import it, you need to provide the password used to encrypt it.", + "dialog_import_encryption_password_error": "the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.", "sort_date": "Date", "sort_title": "Title", "sort_ascending": "Ascending", "placeholder_notes": "No notes", - "placeholder_bin": "The bin is empty", + "placeholder_bin": "No deleted notes", "menu_pin": "Pin", "menu_share": "Share", "menu_unpin": "Unpin", @@ -92,11 +145,12 @@ "confirmations_title_none": "Never", "confirmations_title_irreversible": "Irreversible actions only", "confirmations_title_all": "Always", + "swipe_action_disabled": "Disabled", + "swipe_action_delete": "Delete", + "swipe_action_pin": "Pin", "dismiss_pin": "Pin", "dismiss_unpin": "Unpin", "dismiss_delete": "Delete", - "dismiss_permanently_delete": "Permanently delete", - "dismiss_restore": "Restore", "about_last_edited": "Last edited", "about_created": "Created", "about_words": "Words", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a5550703..edcabaa2 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2,49 +2,100 @@ "app_name": "Material Notes", "app_tagline": "Notas simples, locales, en Material Design", "app_about": "{appName} es una aplicación de toma de notas basadas en texto, orientada a la simplicidad, diseñada adoptando Material Design. {appName} almacena las notas localmente y no requiere ningún permiso de internet, siendo tú el único que puede acceder a las notas.", - "navigation_notes": "Notas", - "navigation_bin": "Papelera", - "navigation_settings": "Ajustes", "error_error": "Error", "error_permission": "Fallo al obtener permisos para guardar el archivo.", "error_read_file": "Fallo al leer el archivo.", + "navigation_notes": "Notas", + "navigation_bin": "Papelera", + "navigation_settings": "Ajustes", + "navigation_settings_appearance": "Appearance", + "navigation_settings_behavior": "Behavior", + "navigation_settings_editor": "Editor", + "navigation_settings_backup": "Backup", + "navigation_settings_about": "About", + "button_ok": "Aceptar", + "button_close": "Cerrar", + "button_cancel": "Cancelar", + "button_add": "Agregar", "settings_appearance": "Apariencia", + "settings_appearance_description": "Language, theme, notes tiles", + "settings_appearance_application": "Application", + "settings_appearance_notes_tiles": "Notes tiles", "settings_language": "Idioma", "settings_theme": "Tema", "settings_theme_system": "Sistema", "settings_theme_light": "Claro", "settings_theme_dark": "Oscuro", "settings_dynamic_theming": "Tema dinámico", - "settings_dynamic_theming_description": "Generar colores a partir del sistema", + "settings_dynamic_theming_description": "Generate colors from the system", "settings_black_theming": "Negro puro", "settings_black_theming_description": "Fondo negro puro para el tema oscuro", + "settings_show_separators": "Separators", + "settings_show_separators_description": "Show a separator between the notes tiles to differentiate them easily", + "settings_show_tiles_background": "Background", + "settings_show_tiles_background_description": "Show the background of the notes tiles to differentiate them easily", + "settings_behavior": "Comportamiento", + "settings_behavior_application": "Application", + "settings_behavior_description": "Confirmations, swipe actions", + "settings_behavior_swipe_actions": "Swipe actions", + "settings_confirmations": "Confirmation dialogs", + "settings_confirmations_description": "Show the confirmation dialogs for actions such as pining and deleting notes", + "settings_swipe_action_right": "Right swipe action", + "settings_swipe_action_right_description": "Action to trigger when a right swipe is performed on the notes tiles", + "settings_swipe_action_left": "Left swipe action", + "settings_swipe_action_left_description": "Action to trigger when a left swipe is performed on the notes tiles", + "settings_flag_secure": "Flag the app as secure", + "settings_flag_secure_description": "Hide the app from the recent apps and prevent screenshots from being made", "settings_editor": "Editor", + "settings_editor_formatting": "Formatting", + "settings_editor_appearance": "Appearance", + "settings_editor_description": "Buttons, toolbar, spacing", "settings_show_undo_redo_buttons": "Botones deshacer/rehacer", - "settings_show_undo_redo_buttons_description": "Mostrar los botones para deshacer y rehacer cambios en el editor", + "settings_show_undo_redo_buttons_description": "Show the buttons to undo and redo changes in the editor''s app bar", "settings_show_checklist_button": "Botón de casilla de verificación", - "settings_show_checklist_button_description": "Mostrar el botón para alternar casillas de verificación en el editor", - "settings_show_toolbar": "Barra de herramientas del editor", - "settings_show_toolbar_description": "Mostrar la barra de herramientas del editor para habilitar el formateado avanzado del texto.", - "settings_show_separators": "Mostrar separadores", - "settings_show_separators_description": "Mostrar un separador entre notas para diferenciarlas fácilmente", - "settings_behavior": "Comportamiento", - "settings_confirmations": "Mostrar diálogos de confirmación", + "settings_show_checklist_button_description": "Show the button to toggle checklists in the editor''s app bar, hiding it from the editor''s toolbar if enabled", + "settings_show_toolbar": "Toolbar", + "settings_show_toolbar_description": "Show the editor''s toolbar to enable advanced text formatting", + "settings_use_paragraph_spacing": "Paragraph spacing", + "settings_use_paragraph_spacing_description": "Use spacing between paragraphs", "settings_backup": "Respaldo", + "settings_backup_description": "Export, import", + "settings_backup_export": "Export", + "settings_backup_import": "Import", + "settings_auto_export": "Auto export as JSON", + "settings_auto_export_description": "Automatically export the notes to a JSON file (bin included) that can be imported back", + "settings_auto_export_value": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}, {encrypt, select, true{encrypted} false{not encrypted} other{}}", + "settings_auto_export_disabled": "Disabled", + "settings_auto_export_directory": "Exports can be found in {directory}", + "settings_auto_export_dialog_description_disabled": "Auto export will be disabled.", + "settings_auto_export_dialog_description_enabled": "Auto export will be performed every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}. Set the frequency to 0 to disable it.", + "settings_auto_export_dialog_slider_label": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}", + "settings_export_success": "Las notas fueron exportadas exitosamente.", "settings_export_json": "Exportar a JSON", + "settings_export_json_description": "Immediately export the notes to a JSON file (bin included) that can be imported back", "settings_export_markdown": "Exportar a Markdown", - "settings_export_json_description": "Exportar notas a un archivo JSON (incluyendo la papelera) que pueda ser importado de vuelta", - "settings_export_markdown_description": "Exportar notas a un archivo Markdown (incluyendo la papelera)", - "settings_export_success": "Las notas fueron exportadas exitosamente.", + "settings_export_markdown_description": "Immediately export the notes to a Markdown file (bin included)", "settings_import": "Importar", "settings_import_description": "Importar notas desde un archivo JSON", "settings_import_success": "Las notas fueron importadas exitosamente.", "settings_about": "Acerca de", + "settings_about_application": "Application", + "settings_about_links": "Links", + "settings_about_help": "Help", + "settings_about_description": "Information, help, GitHub, license", + "settings_build_mode": "Build mode", + "settings_build_mode_release": "Release", + "settings_build_mode_debug": "Debug", "settings_github": "GitHub", "settings_github_description": "Da un vistazo al código fuente", "settings_licence": "Licencia", "settings_licence_description": "AGPL-3.0", - "settings_issue": "Reportar un bug", - "settings_issue_description": "Reportar un bug creando un issue en GitHub", + "settings_github_issues": "Report a bug", + "settings_github_issues_description": "Report a bug by creating a GitHub issue", + "settings_github_discussions": "Ask a question", + "settings_github_discussions_description": "Ask a question on GitHub discussions", + "settings_get_in_touch": "Contact the developer", + "settings_get_in_touch_description": "Contact the developer via mail at contact@maelchiotti.dev", "hint_title": "Título", "hint_note": "Nota", "tooltip_fab_add_note": "Agregar una nota", @@ -60,10 +111,6 @@ "tooltip_permanently_delete": "Eliminar permanentemente", "tooltip_restore": "Restaurar", "tooltip_toggle_pins": "Alternar fijado", - "button_ok": "Aceptar", - "button_close": "Cerrar", - "button_cancel": "Cancelar", - "button_add": "Agregar", "dialog_delete": "Eliminar", "dialog_delete_body": "¿Realmente quieres eliminar {count} {count, plural, zero{} one{nota} other{notas}}? Puedes restaurar{count, plural, zero{} one{la} other{las}} desde la papelera.", "dialog_delete_body_single": "¿Realmente quierers eliminar esta nota? Puedes restaurarla desde la papelera.", @@ -75,11 +122,19 @@ "dialog_restore_body_single": "¿Realmente quieres restaurar esta nota?", "dialog_empty_bin": "Vaciar la papelera", "dialog_empty_bin_body": "¿Realmente quieres restaurar la papelera? No podrás restaurar las notas en ella.", + "dialog_export_encryption_switch": "Encrypt the JSON export", + "dialog_export_encryption_description": "The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.", + "dialog_export_encryption_secondary_description_auto": "This password will be used for all future auto exports.", + "dialog_export_encryption_secondary_description_manual": "This password will only be used for this export.", + "dialog_export_encryption_password_hint": "Password", + "dialog_export_encryption_password_invalid": "Invalid", + "dialog_import_encryption_password_description": "This export is encrypted. To import it, you need to provide the password used to encrypt it.", + "dialog_import_encryption_password_error": "the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.", "sort_date": "Fecha", "sort_title": "Título", "sort_ascending": "Ascendente", "placeholder_notes": "No hay notas", - "placeholder_bin": "La papelera está vacía", + "placeholder_bin": "No deleted notes", "menu_pin": "Fijar", "menu_share": "Compartir", "menu_unpin": "Desfijar", @@ -90,11 +145,12 @@ "confirmations_title_none": "Nunca", "confirmations_title_irreversible": "Sólo acciones irreversibles", "confirmations_title_all": "Siempre", + "swipe_action_disabled": "Disabled", + "swipe_action_delete": "Delete", + "swipe_action_pin": "Pin", "dismiss_pin": "Fijar", "dismiss_unpin": "Desfijar", "dismiss_delete": "Eliminar", - "dismiss_permanently_delete": "Eliminar permanentemente", - "dismiss_restore": "Restaurar", "about_last_edited": "Última edición", "about_created": "Creación", "about_words": "Palabras", @@ -103,4 +159,4 @@ "action_add_note_title": "Agregar una nota", "welcome_note_title": "Bienvenido a Material Notes !", "welcome_note_content": "Notas simples, locales, en Material Design" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 187d07e3..7d1314a2 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2,51 +2,100 @@ "app_name": "Material Notes", "app_tagline": "Notes simples, locales, en material design", "app_about": "{appName} est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. Elle stocke les notes localement et n''a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes.", - "navigation_notes": "Notes", - "navigation_bin": "Corbeille", - "navigation_settings": "Paramètres", "error_error": "Erreur", "error_permission": "Échec lors de la demande de permission pour écrire le fichier.", "error_read_file": "Échec lors de la lecture du fichier.", + "navigation_notes": "Notes", + "navigation_bin": "Corbeille", + "navigation_settings": "Paramètres", + "navigation_settings_appearance": "Apparence", + "navigation_settings_behavior": "Comportement", + "navigation_settings_editor": "Éditeur", + "navigation_settings_backup": "Sauvegarde", + "navigation_settings_about": "À propos", + "button_ok": "Ok", + "button_close": "Fermer", + "button_cancel": "Annuler", + "button_add": "Ajouter", "settings_appearance": "Apparence", + "settings_appearance_description": "Langue, thème, tuiles de notes", + "settings_appearance_application": "Application", + "settings_appearance_notes_tiles": "Tuiles de notes", "settings_language": "Langue", "settings_theme": "Thème", "settings_theme_system": "Système", "settings_theme_light": "Clair", "settings_theme_dark": "Sombre", "settings_dynamic_theming": "Thème dynamique", - "settings_dynamic_theming_description": "Génère des couleurs depuis votre système", + "settings_dynamic_theming_description": "Générer des couleurs depuis le système", "settings_black_theming": "Thème noir", "settings_black_theming_description": "Utilise un fond noir pour le thème sombre", + "settings_show_separators": "Séparateurs", + "settings_show_separators_description": "Afficher un séparateur entre les tuiles de notes pour les différencier facilement", + "settings_show_tiles_background": "Fond", + "settings_show_tiles_background_description": "Afficher l''arrière plan des tuiles des notes pour les différencier plus facilement", + "settings_behavior": "Comportement", + "settings_behavior_application": "Application", + "settings_behavior_description": "Confirmations, actions de balayage", + "settings_behavior_swipe_actions": "Actions de balayage", + "settings_confirmations": "Dialogues de confirmation", + "settings_confirmations_description": "Afficher les dialogues de confirmation pour les actions telles qu''épingler ou supprimer les notes", + "settings_swipe_action_right": "Action de balayage à droite", + "settings_swipe_action_right_description": "Action à déclencher lorsqu''un balayage vers la droite est effectué sur les tuiles de notes", + "settings_swipe_action_left": "Action de balayage à gauche", + "settings_swipe_action_left_description": "Action à déclencher lorsqu''un balayage vers la gauche est effectué sur les tuiles de notes", + "settings_flag_secure": "Marquer l''application comme sécurisée", + "settings_flag_secure_description": "Masquer l'application des applications récentes et empêcher la réalisation de captures d'écran", "settings_editor": "Éditeur", + "settings_editor_formatting": "Mise en forme", + "settings_editor_appearance": "Apparence", + "settings_editor_description": "Boutons, barre d''outils, espacement", "settings_show_undo_redo_buttons": "Boutons annuler/rétablir", - "settings_show_undo_redo_buttons_description": "Afficher les boutons pour annuler et rétablir les modifications dans l''éditeur", + "settings_show_undo_redo_buttons_description": "Afficher les boutons pour annuler et refaire les modifications dans la barre d''application de l''éditeur", "settings_show_checklist_button": "Bouton case à cocher", - "settings_show_checklist_button_description": "Afficher le bouton pour basculer les cases à cocher dans l''éditeur", - "settings_show_toolbar": "Barre d''outils de l''éditeur", - "settings_show_toolbar_description": "Afficher la barre d''outils de l''éditeur pour permettre le formatage textuel avancé", - "settings_show_separators": "Afficher les séparateurs", - "settings_show_separators_description": "Afficher un séparateur entre les notes pour les différencier plus facilement", - "settings_show_tiles_background": "Afficher l''arrière plan des tuiles", - "settings_show_tiles_background_description": "Afficher l''arrière plan des tuiles des notes pour les différencier plus facilement", - "settings_behavior": "Comportement", - "settings_confirmations": "Afficher les dialogues de confirmation", + "settings_show_checklist_button_description": "Afficher le bouton pour activer/désactiver les cases à cocher dans la barre d''application de l''éditeur, en le cachant de la barre d''outils de l''éditeur si activée", + "settings_show_toolbar": "Barre d''outils", + "settings_show_toolbar_description": "Afficher la barre d''outils de l''éditeur pour permettre la mise en forme textuelle avancée", + "settings_use_paragraph_spacing": "Espacement entre les paragraphes", + "settings_use_paragraph_spacing_description": "Utiliser l''espacement entre les paragraphes", "settings_backup": "Sauvegarde", + "settings_backup_description": "Exportation, importation", + "settings_backup_export": "Exporter", + "settings_backup_import": "Importer", + "settings_auto_export": "Export automatique en JSON", + "settings_auto_export_description": "Exporter les notes automatiquement dans un fichier JSON (corbeille incluse) qui peut être réimporté", + "settings_auto_export_value": "Tous les {frequency, select, 1{jour} 7{semaine} 14{2 semaines} 30{mois} other{{frequency} jours}}, {encrypt, select, true{chiffré} false{non chiffré} other{}}", + "settings_auto_export_disabled": "Désactivé", + "settings_auto_export_directory": "Les exports peuvent être trouvés dans {directory}", + "settings_auto_export_dialog_description_disabled": "L''export automatique sera désactivé.", + "settings_auto_export_dialog_description_enabled": "L''exportation automatique sera effectuée tous les {frequency, select, 1{jour} 7{semaine} 14{2 semaines} 30{mois} other{{frequency} jours}}. Mettez la fréquence à 0 pour la désactiver.", + "settings_auto_export_dialog_slider_label": "Tous les {frequency, select, 1{jour} 7{semaine} 14{2 semaines} 30{mois} other{{frequency} jours}}", + "settings_export_success": "Les notes ont bien été exportées.", "settings_export_json": "Exporter en JSON", + "settings_export_json_description": "Exporter immédiatement les notes vers un fichier JSON (corbeille incluse) qui peut être réimporté", "settings_export_markdown": "Exporter en Markdown", - "settings_export_json_description": "Exporter les notes dans un fichier JSON (corbeille incluse) qui peut être réimporté", - "settings_export_markdown_description": "Exporter les notes dans un fichier Markdown (corbeille incluse)", - "settings_export_success": "Les notes ont bien été exportées.", + "settings_export_markdown_description": "Exporter immédiatement les notes vers un fichier Markdown (corbeille incluse)", "settings_import": "Importer", "settings_import_description": "Importer les notes depuis un fichier JSON", "settings_import_success": "Les notes ont bien été importées.", "settings_about": "À propos", + "settings_about_application": "Application", + "settings_about_links": "Liens", + "settings_about_help": "Aide", + "settings_about_description": "Informations, aide, GitHub, licence", + "settings_build_mode": "Build mode", + "settings_build_mode_release": "Production", + "settings_build_mode_debug": "Debug", "settings_github": "GitHub", "settings_github_description": "Jeter un coup d''œil au code source", "settings_licence": "License", "settings_licence_description": "AGPL-3.0", - "settings_issue": "Signaler un bug", - "settings_issue_description": "Signaler un bug en créant une issue sur GitHub", + "settings_github_issues": "Signaler un bug", + "settings_github_issues_description": "Signaler un bug en créant une issue GitHub", + "settings_github_discussions": "Poser une question", + "settings_github_discussions_description": "Poser une question dans les discussions GitHub", + "settings_get_in_touch": "Contacter le développeur", + "settings_get_in_touch_description": "Contacter le développeur par mail à contact@maelchiotti.dev", "hint_title": "Titre", "hint_note": "Note", "tooltip_fab_add_note": "Ajouter une note", @@ -62,10 +111,6 @@ "tooltip_permanently_delete": "Supprimer définitivement", "tooltip_restore": "Restaurer", "tooltip_toggle_pins": "Basculer les épingles", - "button_ok": "Ok", - "button_close": "Fermer", - "button_cancel": "Annuler", - "button_add": "Ajouter", "dialog_delete": "Supprimer", "dialog_delete_body": "Voulez-vous vraiment supprimer {count} {count, plural, zero{} one{note} other{notes}} ? Vous pouvez {count, plural, zero{} one{la} other{les}} restaurer depuis la corbeille.", "dialog_delete_body_single": "Voulez-vous vraiment supprimer cette note ? Vous pouvez la restaurer depuis la corbeille.", @@ -77,11 +122,19 @@ "dialog_restore_body_single": "Voulez-vous vraiment restaurer cette note ?", "dialog_empty_bin": "Vider la corbeille", "dialog_empty_bin_body": "Voulez-vous vraiment vider définitivement la corbeille ? Vous ne pourrez pas restaurer les notes qu''elle contient.", + "dialog_export_encryption_switch": "Chiffrer l''export JSON", + "dialog_export_encryption_description": "Le titre et le contenu des notes seront chiffrés à l'aide de votre mot de passe. Il devrait être généré de façon aléatoire, d'exactement 32 caractères de long, fort (au moins 1 minuscule, 1 majuscule, 1 chiffre et 1 caractère spécial) et stocké de manière sécurisée.", + "dialog_export_encryption_secondary_description_auto": "Ce mot de passe sera utilisé pour tous les futurs exports automatiques.", + "dialog_export_encryption_secondary_description_manual": "Ce mot de passe ne sera utilisé que pour cet export.", + "dialog_export_encryption_password_hint": "Mot de passe", + "dialog_export_encryption_password_invalid": "Invalide", + "dialog_import_encryption_password_description": "Cet export est chiffré. Pour l''importer, vous devez fournir le mot de passe utilisé pour le chiffrer.", + "dialog_import_encryption_password_error": "le déchiffrement de l'export a échoué. Veuillez vérifier que vous avez fourni le même mot de passe que celui que vous avez utilisé pour chiffrer l'export.", "sort_date": "Date", "sort_title": "Titre", "sort_ascending": "Croissant", "placeholder_notes": "Pas de notes", - "placeholder_bin": "La corbeille est vide", + "placeholder_bin": "Aucune note supprimée", "menu_pin": "Épingler", "menu_share": "Partager", "menu_unpin": "Désépingler", @@ -92,11 +145,12 @@ "confirmations_title_none": "Jamais", "confirmations_title_irreversible": "Actions irréversibles uniquement", "confirmations_title_all": "Toujours", + "swipe_action_disabled": "Désactivé", + "swipe_action_delete": "Supprimer", + "swipe_action_pin": "Épingler", "dismiss_pin": "Épingler", "dismiss_unpin": "Désépingler", "dismiss_delete": "Supprimer", - "dismiss_permanently_delete": "Supprimer définitivement", - "dismiss_restore": "Restaurer", "about_last_edited": "Dernière modification", "about_created": "Création", "about_words": "Mots", diff --git a/lib/l10n/app_localizations/app_localizations.g.dart b/lib/l10n/app_localizations/app_localizations.g.dart index daf9d1da..8a01ad8a 100644 --- a/lib/l10n/app_localizations/app_localizations.g.dart +++ b/lib/l10n/app_localizations/app_localizations.g.dart @@ -8,6 +8,7 @@ import 'package:intl/intl.dart' as intl; import 'app_localizations_en.g.dart'; import 'app_localizations_es.g.dart'; import 'app_localizations_fr.g.dart'; +import 'app_localizations_ru.g.dart'; import 'app_localizations_tr.g.dart'; /// Callers can lookup localized strings with an instance of AppLocalizations @@ -66,8 +67,8 @@ abstract class AppLocalizations { final String localeName; - static AppLocalizations? of(BuildContext context) { - return Localizations.of(context, AppLocalizations); + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; } static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); @@ -90,7 +91,13 @@ abstract class AppLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [Locale('en'), Locale('es'), Locale('fr'), Locale('tr')]; + static const List supportedLocales = [ + Locale('en'), + Locale('es'), + Locale('fr'), + Locale('ru'), + Locale('tr') + ]; /// No description provided for @app_name. /// @@ -110,6 +117,24 @@ abstract class AppLocalizations { /// **'{appName} is a text-based note-taking application, aimed at simplicity. It embraces Material Design. It stores the notes locally and doesn\'\'t have any internet permissions, so you are the only one that can access the notes.'** String app_about(Object appName); + /// No description provided for @error_error. + /// + /// In en, this message translates to: + /// **'Error'** + String get error_error; + + /// No description provided for @error_permission. + /// + /// In en, this message translates to: + /// **'Failed to get permission to write the file.'** + String get error_permission; + + /// No description provided for @error_read_file. + /// + /// In en, this message translates to: + /// **'Failed to read the file.'** + String get error_read_file; + /// No description provided for @navigation_notes. /// /// In en, this message translates to: @@ -128,23 +153,59 @@ abstract class AppLocalizations { /// **'Settings'** String get navigation_settings; - /// No description provided for @error_error. + /// No description provided for @navigation_settings_appearance. /// /// In en, this message translates to: - /// **'Error'** - String get error_error; + /// **'Appearance'** + String get navigation_settings_appearance; - /// No description provided for @error_permission. + /// No description provided for @navigation_settings_behavior. /// /// In en, this message translates to: - /// **'Failed to get permission to write the file.'** - String get error_permission; + /// **'Behavior'** + String get navigation_settings_behavior; - /// No description provided for @error_read_file. + /// No description provided for @navigation_settings_editor. /// /// In en, this message translates to: - /// **'Failed to read the file.'** - String get error_read_file; + /// **'Editor'** + String get navigation_settings_editor; + + /// No description provided for @navigation_settings_backup. + /// + /// In en, this message translates to: + /// **'Backup'** + String get navigation_settings_backup; + + /// No description provided for @navigation_settings_about. + /// + /// In en, this message translates to: + /// **'About'** + String get navigation_settings_about; + + /// No description provided for @button_ok. + /// + /// In en, this message translates to: + /// **'Ok'** + String get button_ok; + + /// No description provided for @button_close. + /// + /// In en, this message translates to: + /// **'Close'** + String get button_close; + + /// No description provided for @button_cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get button_cancel; + + /// No description provided for @button_add. + /// + /// In en, this message translates to: + /// **'Add'** + String get button_add; /// No description provided for @settings_appearance. /// @@ -152,6 +213,24 @@ abstract class AppLocalizations { /// **'Appearance'** String get settings_appearance; + /// No description provided for @settings_appearance_description. + /// + /// In en, this message translates to: + /// **'Language, theme, notes tiles'** + String get settings_appearance_description; + + /// No description provided for @settings_appearance_application. + /// + /// In en, this message translates to: + /// **'Application'** + String get settings_appearance_application; + + /// No description provided for @settings_appearance_notes_tiles. + /// + /// In en, this message translates to: + /// **'Notes tiles'** + String get settings_appearance_notes_tiles; + /// No description provided for @settings_language. /// /// In en, this message translates to: @@ -191,7 +270,7 @@ abstract class AppLocalizations { /// No description provided for @settings_dynamic_theming_description. /// /// In en, this message translates to: - /// **'Generate colors from your system'** + /// **'Generate colors from the system'** String get settings_dynamic_theming_description; /// No description provided for @settings_black_theming. @@ -206,12 +285,126 @@ abstract class AppLocalizations { /// **'Use a black background in dark mode'** String get settings_black_theming_description; + /// No description provided for @settings_show_separators. + /// + /// In en, this message translates to: + /// **'Separators'** + String get settings_show_separators; + + /// No description provided for @settings_show_separators_description. + /// + /// In en, this message translates to: + /// **'Show a separator between the notes tiles to differentiate them easily'** + String get settings_show_separators_description; + + /// No description provided for @settings_show_tiles_background. + /// + /// In en, this message translates to: + /// **'Background'** + String get settings_show_tiles_background; + + /// No description provided for @settings_show_tiles_background_description. + /// + /// In en, this message translates to: + /// **'Show the background of the notes tiles to differentiate them easily'** + String get settings_show_tiles_background_description; + + /// No description provided for @settings_behavior. + /// + /// In en, this message translates to: + /// **'Behavior'** + String get settings_behavior; + + /// No description provided for @settings_behavior_application. + /// + /// In en, this message translates to: + /// **'Application'** + String get settings_behavior_application; + + /// No description provided for @settings_behavior_description. + /// + /// In en, this message translates to: + /// **'Confirmations, swipe actions'** + String get settings_behavior_description; + + /// No description provided for @settings_behavior_swipe_actions. + /// + /// In en, this message translates to: + /// **'Swipe actions'** + String get settings_behavior_swipe_actions; + + /// No description provided for @settings_confirmations. + /// + /// In en, this message translates to: + /// **'Confirmation dialogs'** + String get settings_confirmations; + + /// No description provided for @settings_confirmations_description. + /// + /// In en, this message translates to: + /// **'Show the confirmation dialogs for actions such as pining and deleting notes'** + String get settings_confirmations_description; + + /// No description provided for @settings_swipe_action_right. + /// + /// In en, this message translates to: + /// **'Right swipe action'** + String get settings_swipe_action_right; + + /// No description provided for @settings_swipe_action_right_description. + /// + /// In en, this message translates to: + /// **'Action to trigger when a right swipe is performed on the notes tiles'** + String get settings_swipe_action_right_description; + + /// No description provided for @settings_swipe_action_left. + /// + /// In en, this message translates to: + /// **'Left swipe action'** + String get settings_swipe_action_left; + + /// No description provided for @settings_swipe_action_left_description. + /// + /// In en, this message translates to: + /// **'Action to trigger when a left swipe is performed on the notes tiles'** + String get settings_swipe_action_left_description; + + /// No description provided for @settings_flag_secure. + /// + /// In en, this message translates to: + /// **'Flag the app as secure'** + String get settings_flag_secure; + + /// No description provided for @settings_flag_secure_description. + /// + /// In en, this message translates to: + /// **'Hide the app from the recent apps and prevent screenshots from being made'** + String get settings_flag_secure_description; + /// No description provided for @settings_editor. /// /// In en, this message translates to: /// **'Editor'** String get settings_editor; + /// No description provided for @settings_editor_formatting. + /// + /// In en, this message translates to: + /// **'Formatting'** + String get settings_editor_formatting; + + /// No description provided for @settings_editor_appearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get settings_editor_appearance; + + /// No description provided for @settings_editor_description. + /// + /// In en, this message translates to: + /// **'Buttons, toolbar, spacing'** + String get settings_editor_description; + /// No description provided for @settings_show_undo_redo_buttons. /// /// In en, this message translates to: @@ -221,7 +414,7 @@ abstract class AppLocalizations { /// No description provided for @settings_show_undo_redo_buttons_description. /// /// In en, this message translates to: - /// **'Show the buttons to undo and redo changes in the editor'** + /// **'Show the buttons to undo and redo changes in the editor\'\'s app bar'** String get settings_show_undo_redo_buttons_description; /// No description provided for @settings_show_checklist_button. @@ -233,86 +426,104 @@ abstract class AppLocalizations { /// No description provided for @settings_show_checklist_button_description. /// /// In en, this message translates to: - /// **'Show the button to toggle checklists in the editor'** + /// **'Show the button to toggle checklists in the editor\'\'s app bar, hiding it from the editor\'\'s toolbar if enabled'** String get settings_show_checklist_button_description; /// No description provided for @settings_show_toolbar. /// /// In en, this message translates to: - /// **'Editor toolbar'** + /// **'Toolbar'** String get settings_show_toolbar; /// No description provided for @settings_show_toolbar_description. /// /// In en, this message translates to: - /// **'Show the editor toolbar to enable advanced text formatting'** + /// **'Show the editor\'\'s toolbar to enable advanced text formatting'** String get settings_show_toolbar_description; - /// No description provided for @settings_show_separators. + /// No description provided for @settings_use_paragraph_spacing. /// /// In en, this message translates to: - /// **'Show the separators'** - String get settings_show_separators; + /// **'Paragraph spacing'** + String get settings_use_paragraph_spacing; - /// No description provided for @settings_show_separators_description. + /// No description provided for @settings_use_paragraph_spacing_description. /// /// In en, this message translates to: - /// **'Show a separator between notes to differentiate them easily'** - String get settings_show_separators_description; + /// **'Use spacing between paragraphs'** + String get settings_use_paragraph_spacing_description; - /// No description provided for @settings_show_tiles_background. + /// No description provided for @settings_backup. /// /// In en, this message translates to: - /// **'Show the tiles background'** - String get settings_show_tiles_background; + /// **'Backup'** + String get settings_backup; - /// No description provided for @settings_show_tiles_background_description. + /// No description provided for @settings_backup_description. /// /// In en, this message translates to: - /// **'Show the background of the notes tiles to differentiate them easily'** - String get settings_show_tiles_background_description; + /// **'Export, import'** + String get settings_backup_description; - /// No description provided for @settings_behavior. + /// No description provided for @settings_backup_export. /// /// In en, this message translates to: - /// **'Behavior'** - String get settings_behavior; + /// **'Export'** + String get settings_backup_export; - /// No description provided for @settings_confirmations. + /// No description provided for @settings_backup_import. /// /// In en, this message translates to: - /// **'Show confirmation dialogs'** - String get settings_confirmations; + /// **'Import'** + String get settings_backup_import; - /// No description provided for @settings_backup. + /// No description provided for @settings_auto_export. /// /// In en, this message translates to: - /// **'Backup'** - String get settings_backup; + /// **'Auto export as JSON'** + String get settings_auto_export; - /// No description provided for @settings_export_json. + /// No description provided for @settings_auto_export_description. /// /// In en, this message translates to: - /// **'Export as JSON'** - String get settings_export_json; + /// **'Automatically export the notes to a JSON file (bin included) that can be imported back'** + String get settings_auto_export_description; - /// No description provided for @settings_export_markdown. + /// No description provided for @settings_auto_export_value. /// /// In en, this message translates to: - /// **'Export as Markdown'** - String get settings_export_markdown; + /// **'Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}, {encrypt, select, true{encrypted} false{not encrypted} other{}}'** + String settings_auto_export_value(String encrypt, String frequency); - /// No description provided for @settings_export_json_description. + /// No description provided for @settings_auto_export_disabled. /// /// In en, this message translates to: - /// **'Export notes to a JSON file (bin included) that can be imported back'** - String get settings_export_json_description; + /// **'Disabled'** + String get settings_auto_export_disabled; - /// No description provided for @settings_export_markdown_description. + /// No description provided for @settings_auto_export_directory. /// /// In en, this message translates to: - /// **'Export notes to a Markdown file (bin included)'** - String get settings_export_markdown_description; + /// **'Exports can be found in {directory}'** + String settings_auto_export_directory(Object directory); + + /// No description provided for @settings_auto_export_dialog_description_disabled. + /// + /// In en, this message translates to: + /// **'Auto export will be disabled.'** + String get settings_auto_export_dialog_description_disabled; + + /// No description provided for @settings_auto_export_dialog_description_enabled. + /// + /// In en, this message translates to: + /// **'Auto export will be performed every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}. Set the frequency to 0 to disable it.'** + String settings_auto_export_dialog_description_enabled(String frequency); + + /// No description provided for @settings_auto_export_dialog_slider_label. + /// + /// In en, this message translates to: + /// **'Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}'** + String settings_auto_export_dialog_slider_label(String frequency); /// No description provided for @settings_export_success. /// @@ -320,6 +531,30 @@ abstract class AppLocalizations { /// **'The notes were successfully exported.'** String get settings_export_success; + /// No description provided for @settings_export_json. + /// + /// In en, this message translates to: + /// **'Export as JSON'** + String get settings_export_json; + + /// No description provided for @settings_export_json_description. + /// + /// In en, this message translates to: + /// **'Immediately export the notes to a JSON file (bin included) that can be imported back'** + String get settings_export_json_description; + + /// No description provided for @settings_export_markdown. + /// + /// In en, this message translates to: + /// **'Export as Markdown'** + String get settings_export_markdown; + + /// No description provided for @settings_export_markdown_description. + /// + /// In en, this message translates to: + /// **'Immediately export the notes to a Markdown file (bin included)'** + String get settings_export_markdown_description; + /// No description provided for @settings_import. /// /// In en, this message translates to: @@ -344,6 +579,48 @@ abstract class AppLocalizations { /// **'About'** String get settings_about; + /// No description provided for @settings_about_application. + /// + /// In en, this message translates to: + /// **'Application'** + String get settings_about_application; + + /// No description provided for @settings_about_links. + /// + /// In en, this message translates to: + /// **'Links'** + String get settings_about_links; + + /// No description provided for @settings_about_help. + /// + /// In en, this message translates to: + /// **'Help'** + String get settings_about_help; + + /// No description provided for @settings_about_description. + /// + /// In en, this message translates to: + /// **'Information, help, GitHub, license'** + String get settings_about_description; + + /// No description provided for @settings_build_mode. + /// + /// In en, this message translates to: + /// **'Build mode'** + String get settings_build_mode; + + /// No description provided for @settings_build_mode_release. + /// + /// In en, this message translates to: + /// **'Release'** + String get settings_build_mode_release; + + /// No description provided for @settings_build_mode_debug. + /// + /// In en, this message translates to: + /// **'Debug'** + String get settings_build_mode_debug; + /// No description provided for @settings_github. /// /// In en, this message translates to: @@ -368,17 +645,41 @@ abstract class AppLocalizations { /// **'AGPL-3.0'** String get settings_licence_description; - /// No description provided for @settings_issue. + /// No description provided for @settings_github_issues. /// /// In en, this message translates to: /// **'Report a bug'** - String get settings_issue; + String get settings_github_issues; + + /// No description provided for @settings_github_issues_description. + /// + /// In en, this message translates to: + /// **'Report a bug by creating a GitHub issue'** + String get settings_github_issues_description; - /// No description provided for @settings_issue_description. + /// No description provided for @settings_github_discussions. /// /// In en, this message translates to: - /// **'Report a bug by creating an issue on GitHub'** - String get settings_issue_description; + /// **'Ask a question'** + String get settings_github_discussions; + + /// No description provided for @settings_github_discussions_description. + /// + /// In en, this message translates to: + /// **'Ask a question on GitHub discussions'** + String get settings_github_discussions_description; + + /// No description provided for @settings_get_in_touch. + /// + /// In en, this message translates to: + /// **'Contact the developer'** + String get settings_get_in_touch; + + /// No description provided for @settings_get_in_touch_description. + /// + /// In en, this message translates to: + /// **'Contact the developer via mail at contact@maelchiotti.dev'** + String get settings_get_in_touch_description; /// No description provided for @hint_title. /// @@ -470,30 +771,6 @@ abstract class AppLocalizations { /// **'Toggle pins'** String get tooltip_toggle_pins; - /// No description provided for @button_ok. - /// - /// In en, this message translates to: - /// **'Ok'** - String get button_ok; - - /// No description provided for @button_close. - /// - /// In en, this message translates to: - /// **'Close'** - String get button_close; - - /// No description provided for @button_cancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get button_cancel; - - /// No description provided for @button_add. - /// - /// In en, this message translates to: - /// **'Add'** - String get button_add; - /// No description provided for @dialog_delete. /// /// In en, this message translates to: @@ -560,6 +837,54 @@ abstract class AppLocalizations { /// **'Do you really want to permanently empty the bin? You will not be able to restore the notes it contains.'** String get dialog_empty_bin_body; + /// No description provided for @dialog_export_encryption_switch. + /// + /// In en, this message translates to: + /// **'Encrypt the JSON export'** + String get dialog_export_encryption_switch; + + /// No description provided for @dialog_export_encryption_description. + /// + /// In en, this message translates to: + /// **'The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.'** + String get dialog_export_encryption_description; + + /// No description provided for @dialog_export_encryption_secondary_description_auto. + /// + /// In en, this message translates to: + /// **'This password will be used for all future auto exports.'** + String get dialog_export_encryption_secondary_description_auto; + + /// No description provided for @dialog_export_encryption_secondary_description_manual. + /// + /// In en, this message translates to: + /// **'This password will only be used for this export.'** + String get dialog_export_encryption_secondary_description_manual; + + /// No description provided for @dialog_export_encryption_password_hint. + /// + /// In en, this message translates to: + /// **'Password'** + String get dialog_export_encryption_password_hint; + + /// No description provided for @dialog_export_encryption_password_invalid. + /// + /// In en, this message translates to: + /// **'Invalid'** + String get dialog_export_encryption_password_invalid; + + /// No description provided for @dialog_import_encryption_password_description. + /// + /// In en, this message translates to: + /// **'This export is encrypted. To import it, you need to provide the password used to encrypt it.'** + String get dialog_import_encryption_password_description; + + /// No description provided for @dialog_import_encryption_password_error. + /// + /// In en, this message translates to: + /// **'the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.'** + String get dialog_import_encryption_password_error; + /// No description provided for @sort_date. /// /// In en, this message translates to: @@ -587,7 +912,7 @@ abstract class AppLocalizations { /// No description provided for @placeholder_bin. /// /// In en, this message translates to: - /// **'The bin is empty'** + /// **'No deleted notes'** String get placeholder_bin; /// No description provided for @menu_pin. @@ -650,6 +975,24 @@ abstract class AppLocalizations { /// **'Always'** String get confirmations_title_all; + /// No description provided for @swipe_action_disabled. + /// + /// In en, this message translates to: + /// **'Disabled'** + String get swipe_action_disabled; + + /// No description provided for @swipe_action_delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get swipe_action_delete; + + /// No description provided for @swipe_action_pin. + /// + /// In en, this message translates to: + /// **'Pin'** + String get swipe_action_pin; + /// No description provided for @dismiss_pin. /// /// In en, this message translates to: @@ -668,18 +1011,6 @@ abstract class AppLocalizations { /// **'Delete'** String get dismiss_delete; - /// No description provided for @dismiss_permanently_delete. - /// - /// In en, this message translates to: - /// **'Permanently delete'** - String get dismiss_permanently_delete; - - /// No description provided for @dismiss_restore. - /// - /// In en, this message translates to: - /// **'Restore'** - String get dismiss_restore; - /// No description provided for @about_last_edited. /// /// In en, this message translates to: @@ -738,7 +1069,7 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['en', 'es', 'fr', 'tr'].contains(locale.languageCode); + bool isSupported(Locale locale) => ['en', 'es', 'fr', 'ru', 'tr'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -753,6 +1084,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { return AppLocalizationsEs(); case 'fr': return AppLocalizationsFr(); + case 'ru': + return AppLocalizationsRu(); case 'tr': return AppLocalizationsTr(); } diff --git a/lib/l10n/app_localizations/app_localizations_en.g.dart b/lib/l10n/app_localizations/app_localizations_en.g.dart index 82871513..60cc46d6 100644 --- a/lib/l10n/app_localizations/app_localizations_en.g.dart +++ b/lib/l10n/app_localizations/app_localizations_en.g.dart @@ -17,6 +17,15 @@ class AppLocalizationsEn extends AppLocalizations { return '$appName is a text-based note-taking application, aimed at simplicity. It embraces Material Design. It stores the notes locally and doesn\'t have any internet permissions, so you are the only one that can access the notes.'; } + @override + String get error_error => 'Error'; + + @override + String get error_permission => 'Failed to get permission to write the file.'; + + @override + String get error_read_file => 'Failed to read the file.'; + @override String get navigation_notes => 'Notes'; @@ -27,17 +36,44 @@ class AppLocalizationsEn extends AppLocalizations { String get navigation_settings => 'Settings'; @override - String get error_error => 'Error'; + String get navigation_settings_appearance => 'Appearance'; @override - String get error_permission => 'Failed to get permission to write the file.'; + String get navigation_settings_behavior => 'Behavior'; @override - String get error_read_file => 'Failed to read the file.'; + String get navigation_settings_editor => 'Editor'; + + @override + String get navigation_settings_backup => 'Backup'; + + @override + String get navigation_settings_about => 'About'; + + @override + String get button_ok => 'Ok'; + + @override + String get button_close => 'Close'; + + @override + String get button_cancel => 'Cancel'; + + @override + String get button_add => 'Add'; @override String get settings_appearance => 'Appearance'; + @override + String get settings_appearance_description => 'Language, theme, notes tiles'; + + @override + String get settings_appearance_application => 'Application'; + + @override + String get settings_appearance_notes_tiles => 'Notes tiles'; + @override String get settings_language => 'Language'; @@ -57,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_dynamic_theming => 'Dynamic theming'; @override - String get settings_dynamic_theming_description => 'Generate colors from your system'; + String get settings_dynamic_theming_description => 'Generate colors from the system'; @override String get settings_black_theming => 'Black theming'; @@ -65,64 +101,197 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_black_theming_description => 'Use a black background in dark mode'; + @override + String get settings_show_separators => 'Separators'; + + @override + String get settings_show_separators_description => + 'Show a separator between the notes tiles to differentiate them easily'; + + @override + String get settings_show_tiles_background => 'Background'; + + @override + String get settings_show_tiles_background_description => + 'Show the background of the notes tiles to differentiate them easily'; + + @override + String get settings_behavior => 'Behavior'; + + @override + String get settings_behavior_application => 'Application'; + + @override + String get settings_behavior_description => 'Confirmations, swipe actions'; + + @override + String get settings_behavior_swipe_actions => 'Swipe actions'; + + @override + String get settings_confirmations => 'Confirmation dialogs'; + + @override + String get settings_confirmations_description => + 'Show the confirmation dialogs for actions such as pining and deleting notes'; + + @override + String get settings_swipe_action_right => 'Right swipe action'; + + @override + String get settings_swipe_action_right_description => + 'Action to trigger when a right swipe is performed on the notes tiles'; + + @override + String get settings_swipe_action_left => 'Left swipe action'; + + @override + String get settings_swipe_action_left_description => + 'Action to trigger when a left swipe is performed on the notes tiles'; + + @override + String get settings_flag_secure => 'Flag the app as secure'; + + @override + String get settings_flag_secure_description => + 'Hide the app from the recent apps and prevent screenshots from being made'; + @override String get settings_editor => 'Editor'; + @override + String get settings_editor_formatting => 'Formatting'; + + @override + String get settings_editor_appearance => 'Appearance'; + + @override + String get settings_editor_description => 'Buttons, toolbar, spacing'; + @override String get settings_show_undo_redo_buttons => 'Undo/redo buttons'; @override - String get settings_show_undo_redo_buttons_description => 'Show the buttons to undo and redo changes in the editor'; + String get settings_show_undo_redo_buttons_description => + 'Show the buttons to undo and redo changes in the editor\'s app bar'; @override String get settings_show_checklist_button => 'Checklist button'; @override - String get settings_show_checklist_button_description => 'Show the button to toggle checklists in the editor'; + String get settings_show_checklist_button_description => + 'Show the button to toggle checklists in the editor\'s app bar, hiding it from the editor\'s toolbar if enabled'; @override - String get settings_show_toolbar => 'Editor toolbar'; + String get settings_show_toolbar => 'Toolbar'; @override - String get settings_show_toolbar_description => 'Show the editor toolbar to enable advanced text formatting'; + String get settings_show_toolbar_description => 'Show the editor\'s toolbar to enable advanced text formatting'; @override - String get settings_show_separators => 'Show the separators'; + String get settings_use_paragraph_spacing => 'Paragraph spacing'; @override - String get settings_show_separators_description => 'Show a separator between notes to differentiate them easily'; + String get settings_use_paragraph_spacing_description => 'Use spacing between paragraphs'; @override - String get settings_show_tiles_background => 'Show the tiles background'; + String get settings_backup => 'Backup'; @override - String get settings_show_tiles_background_description => - 'Show the background of the notes tiles to differentiate them easily'; + String get settings_backup_description => 'Export, import'; @override - String get settings_behavior => 'Behavior'; + String get settings_backup_export => 'Export'; @override - String get settings_confirmations => 'Show confirmation dialogs'; + String get settings_backup_import => 'Import'; @override - String get settings_backup => 'Backup'; + String get settings_auto_export => 'Auto export as JSON'; @override - String get settings_export_json => 'Export as JSON'; + String get settings_auto_export_description => + 'Automatically export the notes to a JSON file (bin included) that can be imported back'; @override - String get settings_export_markdown => 'Export as Markdown'; + String settings_auto_export_value(String encrypt, String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + String _temp1 = intl.Intl.selectLogic( + encrypt, + { + 'true': 'encrypted', + 'false': 'not encrypted', + 'other': '', + }, + ); + return 'Every $_temp0, $_temp1'; + } @override - String get settings_export_json_description => 'Export notes to a JSON file (bin included) that can be imported back'; + String get settings_auto_export_disabled => 'Disabled'; + + @override + String settings_auto_export_directory(Object directory) { + return 'Exports can be found in $directory'; + } @override - String get settings_export_markdown_description => 'Export notes to a Markdown file (bin included)'; + String get settings_auto_export_dialog_description_disabled => 'Auto export will be disabled.'; + + @override + String settings_auto_export_dialog_description_enabled(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Auto export will be performed every $_temp0. Set the frequency to 0 to disable it.'; + } + + @override + String settings_auto_export_dialog_slider_label(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Every $_temp0'; + } @override String get settings_export_success => 'The notes were successfully exported.'; + @override + String get settings_export_json => 'Export as JSON'; + + @override + String get settings_export_json_description => + 'Immediately export the notes to a JSON file (bin included) that can be imported back'; + + @override + String get settings_export_markdown => 'Export as Markdown'; + + @override + String get settings_export_markdown_description => 'Immediately export the notes to a Markdown file (bin included)'; + @override String get settings_import => 'Import'; @@ -135,6 +304,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_about => 'About'; + @override + String get settings_about_application => 'Application'; + + @override + String get settings_about_links => 'Links'; + + @override + String get settings_about_help => 'Help'; + + @override + String get settings_about_description => 'Information, help, GitHub, license'; + + @override + String get settings_build_mode => 'Build mode'; + + @override + String get settings_build_mode_release => 'Release'; + + @override + String get settings_build_mode_debug => 'Debug'; + @override String get settings_github => 'GitHub'; @@ -148,10 +338,22 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_licence_description => 'AGPL-3.0'; @override - String get settings_issue => 'Report a bug'; + String get settings_github_issues => 'Report a bug'; + + @override + String get settings_github_issues_description => 'Report a bug by creating a GitHub issue'; + + @override + String get settings_github_discussions => 'Ask a question'; + + @override + String get settings_github_discussions_description => 'Ask a question on GitHub discussions'; @override - String get settings_issue_description => 'Report a bug by creating an issue on GitHub'; + String get settings_get_in_touch => 'Contact the developer'; + + @override + String get settings_get_in_touch_description => 'Contact the developer via mail at contact@maelchiotti.dev'; @override String get hint_title => 'Title'; @@ -198,18 +400,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tooltip_toggle_pins => 'Toggle pins'; - @override - String get button_ok => 'Ok'; - - @override - String get button_close => 'Close'; - - @override - String get button_cancel => 'Cancel'; - - @override - String get button_add => 'Add'; - @override String get dialog_delete => 'Delete'; @@ -286,6 +476,35 @@ class AppLocalizationsEn extends AppLocalizations { String get dialog_empty_bin_body => 'Do you really want to permanently empty the bin? You will not be able to restore the notes it contains.'; + @override + String get dialog_export_encryption_switch => 'Encrypt the JSON export'; + + @override + String get dialog_export_encryption_description => + 'The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.'; + + @override + String get dialog_export_encryption_secondary_description_auto => + 'This password will be used for all future auto exports.'; + + @override + String get dialog_export_encryption_secondary_description_manual => + 'This password will only be used for this export.'; + + @override + String get dialog_export_encryption_password_hint => 'Password'; + + @override + String get dialog_export_encryption_password_invalid => 'Invalid'; + + @override + String get dialog_import_encryption_password_description => + 'This export is encrypted. To import it, you need to provide the password used to encrypt it.'; + + @override + String get dialog_import_encryption_password_error => + 'the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.'; + @override String get sort_date => 'Date'; @@ -299,7 +518,7 @@ class AppLocalizationsEn extends AppLocalizations { String get placeholder_notes => 'No notes'; @override - String get placeholder_bin => 'The bin is empty'; + String get placeholder_bin => 'No deleted notes'; @override String get menu_pin => 'Pin'; @@ -332,19 +551,22 @@ class AppLocalizationsEn extends AppLocalizations { String get confirmations_title_all => 'Always'; @override - String get dismiss_pin => 'Pin'; + String get swipe_action_disabled => 'Disabled'; @override - String get dismiss_unpin => 'Unpin'; + String get swipe_action_delete => 'Delete'; @override - String get dismiss_delete => 'Delete'; + String get swipe_action_pin => 'Pin'; @override - String get dismiss_permanently_delete => 'Permanently delete'; + String get dismiss_pin => 'Pin'; @override - String get dismiss_restore => 'Restore'; + String get dismiss_unpin => 'Unpin'; + + @override + String get dismiss_delete => 'Delete'; @override String get about_last_edited => 'Last edited'; diff --git a/lib/l10n/app_localizations/app_localizations_es.g.dart b/lib/l10n/app_localizations/app_localizations_es.g.dart index 80897237..7259dadb 100644 --- a/lib/l10n/app_localizations/app_localizations_es.g.dart +++ b/lib/l10n/app_localizations/app_localizations_es.g.dart @@ -17,6 +17,15 @@ class AppLocalizationsEs extends AppLocalizations { return '$appName es una aplicación de toma de notas basadas en texto, orientada a la simplicidad, diseñada adoptando Material Design. $appName almacena las notas localmente y no requiere ningún permiso de internet, siendo tú el único que puede acceder a las notas.'; } + @override + String get error_error => 'Error'; + + @override + String get error_permission => 'Fallo al obtener permisos para guardar el archivo.'; + + @override + String get error_read_file => 'Fallo al leer el archivo.'; + @override String get navigation_notes => 'Notas'; @@ -27,17 +36,44 @@ class AppLocalizationsEs extends AppLocalizations { String get navigation_settings => 'Ajustes'; @override - String get error_error => 'Error'; + String get navigation_settings_appearance => 'Appearance'; @override - String get error_permission => 'Fallo al obtener permisos para guardar el archivo.'; + String get navigation_settings_behavior => 'Behavior'; @override - String get error_read_file => 'Fallo al leer el archivo.'; + String get navigation_settings_editor => 'Editor'; + + @override + String get navigation_settings_backup => 'Backup'; + + @override + String get navigation_settings_about => 'About'; + + @override + String get button_ok => 'Aceptar'; + + @override + String get button_close => 'Cerrar'; + + @override + String get button_cancel => 'Cancelar'; + + @override + String get button_add => 'Agregar'; @override String get settings_appearance => 'Apariencia'; + @override + String get settings_appearance_description => 'Language, theme, notes tiles'; + + @override + String get settings_appearance_application => 'Application'; + + @override + String get settings_appearance_notes_tiles => 'Notes tiles'; + @override String get settings_language => 'Idioma'; @@ -57,7 +93,7 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_dynamic_theming => 'Tema dinámico'; @override - String get settings_dynamic_theming_description => 'Generar colores a partir del sistema'; + String get settings_dynamic_theming_description => 'Generate colors from the system'; @override String get settings_black_theming => 'Negro puro'; @@ -65,68 +101,197 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_black_theming_description => 'Fondo negro puro para el tema oscuro'; + @override + String get settings_show_separators => 'Separators'; + + @override + String get settings_show_separators_description => + 'Show a separator between the notes tiles to differentiate them easily'; + + @override + String get settings_show_tiles_background => 'Background'; + + @override + String get settings_show_tiles_background_description => + 'Show the background of the notes tiles to differentiate them easily'; + + @override + String get settings_behavior => 'Comportamiento'; + + @override + String get settings_behavior_application => 'Application'; + + @override + String get settings_behavior_description => 'Confirmations, swipe actions'; + + @override + String get settings_behavior_swipe_actions => 'Swipe actions'; + + @override + String get settings_confirmations => 'Confirmation dialogs'; + + @override + String get settings_confirmations_description => + 'Show the confirmation dialogs for actions such as pining and deleting notes'; + + @override + String get settings_swipe_action_right => 'Right swipe action'; + + @override + String get settings_swipe_action_right_description => + 'Action to trigger when a right swipe is performed on the notes tiles'; + + @override + String get settings_swipe_action_left => 'Left swipe action'; + + @override + String get settings_swipe_action_left_description => + 'Action to trigger when a left swipe is performed on the notes tiles'; + + @override + String get settings_flag_secure => 'Flag the app as secure'; + + @override + String get settings_flag_secure_description => + 'Hide the app from the recent apps and prevent screenshots from being made'; + @override String get settings_editor => 'Editor'; + @override + String get settings_editor_formatting => 'Formatting'; + + @override + String get settings_editor_appearance => 'Appearance'; + + @override + String get settings_editor_description => 'Buttons, toolbar, spacing'; + @override String get settings_show_undo_redo_buttons => 'Botones deshacer/rehacer'; @override String get settings_show_undo_redo_buttons_description => - 'Mostrar los botones para deshacer y rehacer cambios en el editor'; + 'Show the buttons to undo and redo changes in the editor\'s app bar'; @override String get settings_show_checklist_button => 'Botón de casilla de verificación'; @override String get settings_show_checklist_button_description => - 'Mostrar el botón para alternar casillas de verificación en el editor'; + 'Show the button to toggle checklists in the editor\'s app bar, hiding it from the editor\'s toolbar if enabled'; @override - String get settings_show_toolbar => 'Barra de herramientas del editor'; + String get settings_show_toolbar => 'Toolbar'; @override - String get settings_show_toolbar_description => - 'Mostrar la barra de herramientas del editor para habilitar el formateado avanzado del texto.'; + String get settings_show_toolbar_description => 'Show the editor\'s toolbar to enable advanced text formatting'; @override - String get settings_show_separators => 'Mostrar separadores'; + String get settings_use_paragraph_spacing => 'Paragraph spacing'; @override - String get settings_show_separators_description => 'Mostrar un separador entre notas para diferenciarlas fácilmente'; + String get settings_use_paragraph_spacing_description => 'Use spacing between paragraphs'; @override - String get settings_show_tiles_background => 'Show the tiles background'; + String get settings_backup => 'Respaldo'; @override - String get settings_show_tiles_background_description => - 'Show the background of the notes tiles to differentiate them easily'; + String get settings_backup_description => 'Export, import'; @override - String get settings_behavior => 'Comportamiento'; + String get settings_backup_export => 'Export'; @override - String get settings_confirmations => 'Mostrar diálogos de confirmación'; + String get settings_backup_import => 'Import'; @override - String get settings_backup => 'Respaldo'; + String get settings_auto_export => 'Auto export as JSON'; @override - String get settings_export_json => 'Exportar a JSON'; + String get settings_auto_export_description => + 'Automatically export the notes to a JSON file (bin included) that can be imported back'; @override - String get settings_export_markdown => 'Exportar a Markdown'; + String settings_auto_export_value(String encrypt, String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + String _temp1 = intl.Intl.selectLogic( + encrypt, + { + 'true': 'encrypted', + 'false': 'not encrypted', + 'other': '', + }, + ); + return 'Every $_temp0, $_temp1'; + } @override - String get settings_export_json_description => - 'Exportar notas a un archivo JSON (incluyendo la papelera) que pueda ser importado de vuelta'; + String get settings_auto_export_disabled => 'Disabled'; + + @override + String settings_auto_export_directory(Object directory) { + return 'Exports can be found in $directory'; + } @override - String get settings_export_markdown_description => 'Exportar notas a un archivo Markdown (incluyendo la papelera)'; + String get settings_auto_export_dialog_description_disabled => 'Auto export will be disabled.'; + + @override + String settings_auto_export_dialog_description_enabled(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Auto export will be performed every $_temp0. Set the frequency to 0 to disable it.'; + } + + @override + String settings_auto_export_dialog_slider_label(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Every $_temp0'; + } @override String get settings_export_success => 'Las notas fueron exportadas exitosamente.'; + @override + String get settings_export_json => 'Exportar a JSON'; + + @override + String get settings_export_json_description => + 'Immediately export the notes to a JSON file (bin included) that can be imported back'; + + @override + String get settings_export_markdown => 'Exportar a Markdown'; + + @override + String get settings_export_markdown_description => 'Immediately export the notes to a Markdown file (bin included)'; + @override String get settings_import => 'Importar'; @@ -139,6 +304,27 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_about => 'Acerca de'; + @override + String get settings_about_application => 'Application'; + + @override + String get settings_about_links => 'Links'; + + @override + String get settings_about_help => 'Help'; + + @override + String get settings_about_description => 'Information, help, GitHub, license'; + + @override + String get settings_build_mode => 'Build mode'; + + @override + String get settings_build_mode_release => 'Release'; + + @override + String get settings_build_mode_debug => 'Debug'; + @override String get settings_github => 'GitHub'; @@ -152,10 +338,22 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_licence_description => 'AGPL-3.0'; @override - String get settings_issue => 'Reportar un bug'; + String get settings_github_issues => 'Report a bug'; @override - String get settings_issue_description => 'Reportar un bug creando un issue en GitHub'; + String get settings_github_issues_description => 'Report a bug by creating a GitHub issue'; + + @override + String get settings_github_discussions => 'Ask a question'; + + @override + String get settings_github_discussions_description => 'Ask a question on GitHub discussions'; + + @override + String get settings_get_in_touch => 'Contact the developer'; + + @override + String get settings_get_in_touch_description => 'Contact the developer via mail at contact@maelchiotti.dev'; @override String get hint_title => 'Título'; @@ -202,18 +400,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get tooltip_toggle_pins => 'Alternar fijado'; - @override - String get button_ok => 'Aceptar'; - - @override - String get button_close => 'Cerrar'; - - @override - String get button_cancel => 'Cancelar'; - - @override - String get button_add => 'Agregar'; - @override String get dialog_delete => 'Eliminar'; @@ -291,6 +477,35 @@ class AppLocalizationsEs extends AppLocalizations { String get dialog_empty_bin_body => '¿Realmente quieres restaurar la papelera? No podrás restaurar las notas en ella.'; + @override + String get dialog_export_encryption_switch => 'Encrypt the JSON export'; + + @override + String get dialog_export_encryption_description => + 'The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.'; + + @override + String get dialog_export_encryption_secondary_description_auto => + 'This password will be used for all future auto exports.'; + + @override + String get dialog_export_encryption_secondary_description_manual => + 'This password will only be used for this export.'; + + @override + String get dialog_export_encryption_password_hint => 'Password'; + + @override + String get dialog_export_encryption_password_invalid => 'Invalid'; + + @override + String get dialog_import_encryption_password_description => + 'This export is encrypted. To import it, you need to provide the password used to encrypt it.'; + + @override + String get dialog_import_encryption_password_error => + 'the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.'; + @override String get sort_date => 'Fecha'; @@ -304,7 +519,7 @@ class AppLocalizationsEs extends AppLocalizations { String get placeholder_notes => 'No hay notas'; @override - String get placeholder_bin => 'La papelera está vacía'; + String get placeholder_bin => 'No deleted notes'; @override String get menu_pin => 'Fijar'; @@ -337,19 +552,22 @@ class AppLocalizationsEs extends AppLocalizations { String get confirmations_title_all => 'Siempre'; @override - String get dismiss_pin => 'Fijar'; + String get swipe_action_disabled => 'Disabled'; @override - String get dismiss_unpin => 'Desfijar'; + String get swipe_action_delete => 'Delete'; @override - String get dismiss_delete => 'Eliminar'; + String get swipe_action_pin => 'Pin'; @override - String get dismiss_permanently_delete => 'Eliminar permanentemente'; + String get dismiss_pin => 'Fijar'; @override - String get dismiss_restore => 'Restaurar'; + String get dismiss_unpin => 'Desfijar'; + + @override + String get dismiss_delete => 'Eliminar'; @override String get about_last_edited => 'Última edición'; diff --git a/lib/l10n/app_localizations/app_localizations_fr.g.dart b/lib/l10n/app_localizations/app_localizations_fr.g.dart index 7188c58e..8a94816b 100644 --- a/lib/l10n/app_localizations/app_localizations_fr.g.dart +++ b/lib/l10n/app_localizations/app_localizations_fr.g.dart @@ -17,6 +17,15 @@ class AppLocalizationsFr extends AppLocalizations { return '$appName est une application de prise de notes textuelles, qui vise la simplicité. Elle adopte le style Material Design. Elle stocke les notes localement et n\'a aucune permission internet, vous êtes donc le seul à pouvoir accéder aux notes.'; } + @override + String get error_error => 'Erreur'; + + @override + String get error_permission => 'Échec lors de la demande de permission pour écrire le fichier.'; + + @override + String get error_read_file => 'Échec lors de la lecture du fichier.'; + @override String get navigation_notes => 'Notes'; @@ -27,17 +36,44 @@ class AppLocalizationsFr extends AppLocalizations { String get navigation_settings => 'Paramètres'; @override - String get error_error => 'Erreur'; + String get navigation_settings_appearance => 'Apparence'; @override - String get error_permission => 'Échec lors de la demande de permission pour écrire le fichier.'; + String get navigation_settings_behavior => 'Comportement'; @override - String get error_read_file => 'Échec lors de la lecture du fichier.'; + String get navigation_settings_editor => 'Éditeur'; + + @override + String get navigation_settings_backup => 'Sauvegarde'; + + @override + String get navigation_settings_about => 'À propos'; + + @override + String get button_ok => 'Ok'; + + @override + String get button_close => 'Fermer'; + + @override + String get button_cancel => 'Annuler'; + + @override + String get button_add => 'Ajouter'; @override String get settings_appearance => 'Apparence'; + @override + String get settings_appearance_description => 'Langue, thème, tuiles de notes'; + + @override + String get settings_appearance_application => 'Application'; + + @override + String get settings_appearance_notes_tiles => 'Tuiles de notes'; + @override String get settings_language => 'Langue'; @@ -57,7 +93,7 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_dynamic_theming => 'Thème dynamique'; @override - String get settings_dynamic_theming_description => 'Génère des couleurs depuis votre système'; + String get settings_dynamic_theming_description => 'Générer des couleurs depuis le système'; @override String get settings_black_theming => 'Thème noir'; @@ -65,69 +101,199 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_black_theming_description => 'Utilise un fond noir pour le thème sombre'; + @override + String get settings_show_separators => 'Séparateurs'; + + @override + String get settings_show_separators_description => + 'Afficher un séparateur entre les tuiles de notes pour les différencier facilement'; + + @override + String get settings_show_tiles_background => 'Fond'; + + @override + String get settings_show_tiles_background_description => + 'Afficher l\'arrière plan des tuiles des notes pour les différencier plus facilement'; + + @override + String get settings_behavior => 'Comportement'; + + @override + String get settings_behavior_application => 'Application'; + + @override + String get settings_behavior_description => 'Confirmations, actions de balayage'; + + @override + String get settings_behavior_swipe_actions => 'Actions de balayage'; + + @override + String get settings_confirmations => 'Dialogues de confirmation'; + + @override + String get settings_confirmations_description => + 'Afficher les dialogues de confirmation pour les actions telles qu\'épingler ou supprimer les notes'; + + @override + String get settings_swipe_action_right => 'Action de balayage à droite'; + + @override + String get settings_swipe_action_right_description => + 'Action à déclencher lorsqu\'un balayage vers la droite est effectué sur les tuiles de notes'; + + @override + String get settings_swipe_action_left => 'Action de balayage à gauche'; + + @override + String get settings_swipe_action_left_description => + 'Action à déclencher lorsqu\'un balayage vers la gauche est effectué sur les tuiles de notes'; + + @override + String get settings_flag_secure => 'Marquer l\'application comme sécurisée'; + + @override + String get settings_flag_secure_description => + 'Masquer lapplication des applications récentes et empêcher la réalisation de captures décran'; + @override String get settings_editor => 'Éditeur'; + @override + String get settings_editor_formatting => 'Mise en forme'; + + @override + String get settings_editor_appearance => 'Apparence'; + + @override + String get settings_editor_description => 'Boutons, barre d\'outils, espacement'; + @override String get settings_show_undo_redo_buttons => 'Boutons annuler/rétablir'; @override String get settings_show_undo_redo_buttons_description => - 'Afficher les boutons pour annuler et rétablir les modifications dans l\'éditeur'; + 'Afficher les boutons pour annuler et refaire les modifications dans la barre d\'application de l\'éditeur'; @override String get settings_show_checklist_button => 'Bouton case à cocher'; @override String get settings_show_checklist_button_description => - 'Afficher le bouton pour basculer les cases à cocher dans l\'éditeur'; + 'Afficher le bouton pour activer/désactiver les cases à cocher dans la barre d\'application de l\'éditeur, en le cachant de la barre d\'outils de l\'éditeur si activée'; @override - String get settings_show_toolbar => 'Barre d\'outils de l\'éditeur'; + String get settings_show_toolbar => 'Barre d\'outils'; @override String get settings_show_toolbar_description => - 'Afficher la barre d\'outils de l\'éditeur pour permettre le formatage textuel avancé'; + 'Afficher la barre d\'outils de l\'éditeur pour permettre la mise en forme textuelle avancée'; @override - String get settings_show_separators => 'Afficher les séparateurs'; + String get settings_use_paragraph_spacing => 'Espacement entre les paragraphes'; @override - String get settings_show_separators_description => - 'Afficher un séparateur entre les notes pour les différencier plus facilement'; + String get settings_use_paragraph_spacing_description => 'Utiliser l\'espacement entre les paragraphes'; @override - String get settings_show_tiles_background => 'Afficher l\'arrière plan des tuiles'; + String get settings_backup => 'Sauvegarde'; @override - String get settings_show_tiles_background_description => - 'Afficher l\'arrière plan des tuiles des notes pour les différencier plus facilement'; + String get settings_backup_description => 'Exportation, importation'; @override - String get settings_behavior => 'Comportement'; + String get settings_backup_export => 'Exporter'; @override - String get settings_confirmations => 'Afficher les dialogues de confirmation'; + String get settings_backup_import => 'Importer'; @override - String get settings_backup => 'Sauvegarde'; + String get settings_auto_export => 'Export automatique en JSON'; @override - String get settings_export_json => 'Exporter en JSON'; + String get settings_auto_export_description => + 'Exporter les notes automatiquement dans un fichier JSON (corbeille incluse) qui peut être réimporté'; @override - String get settings_export_markdown => 'Exporter en Markdown'; + String settings_auto_export_value(String encrypt, String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'jour', + '7': 'semaine', + '14': '2 semaines', + '30': 'mois', + 'other': '$frequency jours', + }, + ); + String _temp1 = intl.Intl.selectLogic( + encrypt, + { + 'true': 'chiffré', + 'false': 'non chiffré', + 'other': '', + }, + ); + return 'Tous les $_temp0, $_temp1'; + } @override - String get settings_export_json_description => - 'Exporter les notes dans un fichier JSON (corbeille incluse) qui peut être réimporté'; + String get settings_auto_export_disabled => 'Désactivé'; @override - String get settings_export_markdown_description => 'Exporter les notes dans un fichier Markdown (corbeille incluse)'; + String settings_auto_export_directory(Object directory) { + return 'Les exports peuvent être trouvés dans $directory'; + } + + @override + String get settings_auto_export_dialog_description_disabled => 'L\'export automatique sera désactivé.'; + + @override + String settings_auto_export_dialog_description_enabled(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'jour', + '7': 'semaine', + '14': '2 semaines', + '30': 'mois', + 'other': '$frequency jours', + }, + ); + return 'L\'exportation automatique sera effectuée tous les $_temp0. Mettez la fréquence à 0 pour la désactiver.'; + } + + @override + String settings_auto_export_dialog_slider_label(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'jour', + '7': 'semaine', + '14': '2 semaines', + '30': 'mois', + 'other': '$frequency jours', + }, + ); + return 'Tous les $_temp0'; + } @override String get settings_export_success => 'Les notes ont bien été exportées.'; + @override + String get settings_export_json => 'Exporter en JSON'; + + @override + String get settings_export_json_description => + 'Exporter immédiatement les notes vers un fichier JSON (corbeille incluse) qui peut être réimporté'; + + @override + String get settings_export_markdown => 'Exporter en Markdown'; + + @override + String get settings_export_markdown_description => + 'Exporter immédiatement les notes vers un fichier Markdown (corbeille incluse)'; + @override String get settings_import => 'Importer'; @@ -140,6 +306,27 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_about => 'À propos'; + @override + String get settings_about_application => 'Application'; + + @override + String get settings_about_links => 'Liens'; + + @override + String get settings_about_help => 'Aide'; + + @override + String get settings_about_description => 'Informations, aide, GitHub, licence'; + + @override + String get settings_build_mode => 'Build mode'; + + @override + String get settings_build_mode_release => 'Production'; + + @override + String get settings_build_mode_debug => 'Debug'; + @override String get settings_github => 'GitHub'; @@ -153,10 +340,22 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_licence_description => 'AGPL-3.0'; @override - String get settings_issue => 'Signaler un bug'; + String get settings_github_issues => 'Signaler un bug'; + + @override + String get settings_github_issues_description => 'Signaler un bug en créant une issue GitHub'; @override - String get settings_issue_description => 'Signaler un bug en créant une issue sur GitHub'; + String get settings_github_discussions => 'Poser une question'; + + @override + String get settings_github_discussions_description => 'Poser une question dans les discussions GitHub'; + + @override + String get settings_get_in_touch => 'Contacter le développeur'; + + @override + String get settings_get_in_touch_description => 'Contacter le développeur par mail à contact@maelchiotti.dev'; @override String get hint_title => 'Titre'; @@ -203,18 +402,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get tooltip_toggle_pins => 'Basculer les épingles'; - @override - String get button_ok => 'Ok'; - - @override - String get button_close => 'Fermer'; - - @override - String get button_cancel => 'Annuler'; - - @override - String get button_add => 'Ajouter'; - @override String get dialog_delete => 'Supprimer'; @@ -292,6 +479,35 @@ class AppLocalizationsFr extends AppLocalizations { String get dialog_empty_bin_body => 'Voulez-vous vraiment vider définitivement la corbeille ? Vous ne pourrez pas restaurer les notes qu\'elle contient.'; + @override + String get dialog_export_encryption_switch => 'Chiffrer l\'export JSON'; + + @override + String get dialog_export_encryption_description => + 'Le titre et le contenu des notes seront chiffrés à laide de votre mot de passe. Il devrait être généré de façon aléatoire, dexactement 32 caractères de long, fort (au moins 1 minuscule, 1 majuscule, 1 chiffre et 1 caractère spécial) et stocké de manière sécurisée.'; + + @override + String get dialog_export_encryption_secondary_description_auto => + 'Ce mot de passe sera utilisé pour tous les futurs exports automatiques.'; + + @override + String get dialog_export_encryption_secondary_description_manual => + 'Ce mot de passe ne sera utilisé que pour cet export.'; + + @override + String get dialog_export_encryption_password_hint => 'Mot de passe'; + + @override + String get dialog_export_encryption_password_invalid => 'Invalide'; + + @override + String get dialog_import_encryption_password_description => + 'Cet export est chiffré. Pour l\'importer, vous devez fournir le mot de passe utilisé pour le chiffrer.'; + + @override + String get dialog_import_encryption_password_error => + 'le déchiffrement de lexport a échoué. Veuillez vérifier que vous avez fourni le même mot de passe que celui que vous avez utilisé pour chiffrer lexport.'; + @override String get sort_date => 'Date'; @@ -305,7 +521,7 @@ class AppLocalizationsFr extends AppLocalizations { String get placeholder_notes => 'Pas de notes'; @override - String get placeholder_bin => 'La corbeille est vide'; + String get placeholder_bin => 'Aucune note supprimée'; @override String get menu_pin => 'Épingler'; @@ -338,19 +554,22 @@ class AppLocalizationsFr extends AppLocalizations { String get confirmations_title_all => 'Toujours'; @override - String get dismiss_pin => 'Épingler'; + String get swipe_action_disabled => 'Désactivé'; @override - String get dismiss_unpin => 'Désépingler'; + String get swipe_action_delete => 'Supprimer'; @override - String get dismiss_delete => 'Supprimer'; + String get swipe_action_pin => 'Épingler'; @override - String get dismiss_permanently_delete => 'Supprimer définitivement'; + String get dismiss_pin => 'Épingler'; @override - String get dismiss_restore => 'Restaurer'; + String get dismiss_unpin => 'Désépingler'; + + @override + String get dismiss_delete => 'Supprimer'; @override String get about_last_edited => 'Dernière modification'; diff --git a/lib/l10n/app_localizations/app_localizations_ru.g.dart b/lib/l10n/app_localizations/app_localizations_ru.g.dart new file mode 100644 index 00000000..05f7ac0d --- /dev/null +++ b/lib/l10n/app_localizations/app_localizations_ru.g.dart @@ -0,0 +1,594 @@ +import 'package:intl/intl.dart' as intl; + +import 'app_localizations.g.dart'; + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get app_name => 'Material Notes'; + + @override + String get app_tagline => 'Простые, локальные заметки в стиле material design'; + + @override + String app_about(Object appName) { + return '$appName – это приложение для создания простых текстовых заметок. Приложение выполнено в стиле Material Design. Оно хранит заметки локально на устройстве и работает без подключения к Интернету, поэтому только вы имеете доступ к заметкам.'; + } + + @override + String get error_error => 'Ошибка'; + + @override + String get error_permission => 'Не удалось получить разрешение на запись файла.'; + + @override + String get error_read_file => 'Не удалось прочитать файл.'; + + @override + String get navigation_notes => 'Заметки'; + + @override + String get navigation_bin => 'Корзина'; + + @override + String get navigation_settings => 'Настройки'; + + @override + String get navigation_settings_appearance => 'Персонализация'; + + @override + String get navigation_settings_behavior => 'Поведение'; + + @override + String get navigation_settings_editor => 'Редактор'; + + @override + String get navigation_settings_backup => 'Резервное копирование'; + + @override + String get navigation_settings_about => 'О приложении'; + + @override + String get button_ok => 'Оk'; + + @override + String get button_close => 'Закрыть'; + + @override + String get button_cancel => 'Отмена'; + + @override + String get button_add => 'Добавить'; + + @override + String get settings_appearance => 'Персонализация'; + + @override + String get settings_appearance_description => 'Язык, тема, стили заметок'; + + @override + String get settings_appearance_application => 'Приложение'; + + @override + String get settings_appearance_notes_tiles => 'Стиль заметок'; + + @override + String get settings_language => 'Язык'; + + @override + String get settings_theme => 'Тема оформления'; + + @override + String get settings_theme_system => 'По умолчанию'; + + @override + String get settings_theme_light => 'Светлая'; + + @override + String get settings_theme_dark => 'Тёмная'; + + @override + String get settings_dynamic_theming => 'Динамические цвета'; + + @override + String get settings_dynamic_theming_description => 'Использовать цвет, установленный в системе'; + + @override + String get settings_black_theming => 'Натуральный чёрный'; + + @override + String get settings_black_theming_description => 'Использовать чёрный фон при тёмном режиме'; + + @override + String get settings_show_separators => 'Разделители'; + + @override + String get settings_show_separators_description => 'Показывать разделитель между заметками'; + + @override + String get settings_show_tiles_background => 'Фон'; + + @override + String get settings_show_tiles_background_description => 'Показывать фон у карточек с заметками'; + + @override + String get settings_behavior => 'Поведение'; + + @override + String get settings_behavior_application => 'Приложение'; + + @override + String get settings_behavior_description => 'Подтверждения, действия свайпа'; + + @override + String get settings_behavior_swipe_actions => 'Действия при свайпе'; + + @override + String get settings_confirmations => 'Диалоги подтверждения'; + + @override + String get settings_confirmations_description => + 'Показать диалоги подтверждения при закреплении или удалении заметок'; + + @override + String get settings_swipe_action_right => 'Действие при свайпе вправо'; + + @override + String get settings_swipe_action_right_description => + 'Действие, которое будет выполняться при свайпе вправо по заметке'; + + @override + String get settings_swipe_action_left => 'Действие при свайпе влево'; + + @override + String get settings_swipe_action_left_description => + 'Действие, которое будет выполняться при свайпе влево по заметке'; + + @override + String get settings_flag_secure => 'Помечать приложение как защищённое'; + + @override + String get settings_flag_secure_description => + 'Скрывать приложение из недавних приложений и предотвращать создание скриншотов'; + + @override + String get settings_editor => 'Редактор'; + + @override + String get settings_editor_formatting => 'Форматирование'; + + @override + String get settings_editor_appearance => 'Персонализация'; + + @override + String get settings_editor_description => 'Кнопки, панель инструментов, интервал'; + + @override + String get settings_show_undo_redo_buttons => 'Кнопки отмены/повтора'; + + @override + String get settings_show_undo_redo_buttons_description => 'Показывать кнопки отмены/повтора изменений в редакторе'; + + @override + String get settings_show_checklist_button => 'Кнопка для переключения списков'; + + @override + String get settings_show_checklist_button_description => + 'Показать кнопку для переключения чек-листов в панели приложений редактора, скрывая его из панели инструментов редактора, если включено'; + + @override + String get settings_show_toolbar => 'Панель инструментов'; + + @override + String get settings_show_toolbar_description => 'Показать панель инструментов для расширенного форматирования текста'; + + @override + String get settings_use_paragraph_spacing => 'Интервал между параграфами'; + + @override + String get settings_use_paragraph_spacing_description => 'Использовать интервал между параграфами'; + + @override + String get settings_backup => 'Резервное копирование'; + + @override + String get settings_backup_description => 'Экспорт, импорт'; + + @override + String get settings_backup_export => 'Экспорт'; + + @override + String get settings_backup_import => 'Импорт'; + + @override + String get settings_auto_export => 'Автоматический экспорт в формате JSON'; + + @override + String get settings_auto_export_description => + 'Автоматически экспортировать заметки в формате JSON (включая заметки из корзины)'; + + @override + String settings_auto_export_value(String encrypt, String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'Каждый день', + '7': 'Каждую неделю', + '14': 'Каждые 2 недели', + '30': 'Каждый месяц', + 'other': 'Каждые $frequency дней', + }, + ); + String _temp1 = intl.Intl.selectLogic( + encrypt, + { + 'true': 'зашифровано', + 'false': 'не зашифровано', + 'other': '', + }, + ); + return '$_temp0, $_temp1'; + } + + @override + String get settings_auto_export_disabled => 'Отключено'; + + @override + String settings_auto_export_directory(Object directory) { + return 'Экспортированные заметки находятся в $directory'; + } + + @override + String get settings_auto_export_dialog_description_disabled => 'Автоматический экспорт будет отключен.'; + + @override + String settings_auto_export_dialog_description_enabled(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'каждый день', + '7': 'каждую неделю', + '14': 'каждые 2 недели', + '30': 'каждый месяц', + 'other': 'каждые $frequency дней', + }, + ); + return 'Автоматический экспорт будет производиться $_temp0. Установите значение на 0, чтобы отключить автоматический экспорт.'; + } + + @override + String settings_auto_export_dialog_slider_label(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'Каждый день', + '7': 'Каждую неделю', + '14': 'Каждые 2 недели', + '30': 'Каждый месяц', + 'other': 'Каждые $frequency дней', + }, + ); + return '$_temp0'; + } + + @override + String get settings_export_success => 'Заметки были успешно экспортированы.'; + + @override + String get settings_export_json => 'Экспортировать в формате JSON'; + + @override + String get settings_export_json_description => + 'Моментально экспортировать заметки в формате JSON (включая заметки из корзины)'; + + @override + String get settings_export_markdown => 'Экспортировать в формате Markdown'; + + @override + String get settings_export_markdown_description => + 'Моментально экспортировать заметки в формате Markdown (включая заметки из корзины)'; + + @override + String get settings_import => 'Импорт заметок'; + + @override + String get settings_import_description => 'Импортировать заметки из JSON–файла'; + + @override + String get settings_import_success => 'Заметки были успешно импортированы.'; + + @override + String get settings_about => 'О приложении'; + + @override + String get settings_about_application => 'Приложение'; + + @override + String get settings_about_links => 'Ссылки'; + + @override + String get settings_about_help => 'Справка'; + + @override + String get settings_about_description => 'Информация, справка, GitHub, лицензия'; + + @override + String get settings_build_mode => 'Режим сборки'; + + @override + String get settings_build_mode_release => 'Release'; + + @override + String get settings_build_mode_debug => 'Debug'; + + @override + String get settings_github => 'GitHub'; + + @override + String get settings_github_description => 'Ознакомьтесь с исходным кодом приложения'; + + @override + String get settings_licence => 'Лицензия'; + + @override + String get settings_licence_description => 'AGPL-3.0'; + + @override + String get settings_github_issues => 'Сообщить об ошибке'; + + @override + String get settings_github_issues_description => 'Сообщить об ошибке через GitHub'; + + @override + String get settings_github_discussions => 'Задать вопрос'; + + @override + String get settings_github_discussions_description => 'Задайте вопрос на обсуждениях в GitHub'; + + @override + String get settings_get_in_touch => 'Связаться с разработчиком'; + + @override + String get settings_get_in_touch_description => 'Свяжитесь с разработчиком по почте contact@maelchiotti.dev'; + + @override + String get hint_title => 'Заголовок'; + + @override + String get hint_note => 'Заметка'; + + @override + String get tooltip_fab_add_note => 'Добавить заметку'; + + @override + String get tooltip_fab_empty_bin => 'Очистить корзину'; + + @override + String get tooltip_layout_list => 'Список'; + + @override + String get tooltip_layout_grid => 'Сетка'; + + @override + String get tooltip_sort => 'Сортировать заметки'; + + @override + String get tooltip_search => 'Поиск заметок'; + + @override + String get tooltip_toggle_checkbox => 'Переключить флажок'; + + @override + String get tooltip_select_all => 'Выбрать все'; + + @override + String get tooltip_unselect_all => 'Отменить выбор'; + + @override + String get tooltip_delete => 'Удалить'; + + @override + String get tooltip_permanently_delete => 'Удалить навсегда'; + + @override + String get tooltip_restore => 'Восстановить'; + + @override + String get tooltip_toggle_pins => 'Закрепить/Открепить'; + + @override + String get dialog_delete => 'Удалить'; + + @override + String dialog_delete_body(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'заметки', + many: 'заметок', + few: 'заметки', + one: 'заметку', + ); + String _temp1 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'их', + one: 'её', + ); + return 'Вы действительно хотите удалить $count $_temp0? Вы можете восстановить $_temp1 из корзины.'; + } + + @override + String get dialog_delete_body_single => + 'Вы действительно хотите поместить эту заметку в корзину? Вы можете восстановить её из корзины.'; + + @override + String get dialog_permanently_delete => 'Удалить'; + + @override + String dialog_permanently_delete_body(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'заметки', + many: 'заметок', + few: 'заметки', + one: 'заметку', + ); + String _temp1 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'их', + one: 'её', + ); + return 'Вы действительно хотите навсегда удалить $count $_temp0? Вы не сможете $_temp1 восстановить.'; + } + + @override + String get dialog_permanently_delete_body_single => + 'Вы действительно хотите навсегда удалить эту заметку? Заметка будет безвозвратно удалена.'; + + @override + String get dialog_restore => 'Восстановить'; + + @override + String dialog_restore_body(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'заметки', + many: 'заметок', + few: 'заметки', + one: 'заметку', + ); + return 'Вы действительно хотите восстановить $count $_temp0?'; + } + + @override + String get dialog_restore_body_single => 'Вы действительно хотите восстановить эту заметку?'; + + @override + String get dialog_empty_bin => 'Очистить корзину'; + + @override + String get dialog_empty_bin_body => + 'Вы действительно хотите отчистить корзину? Все заметки будут безвозвратно удалены.'; + + @override + String get dialog_export_encryption_switch => 'Зашифровать экспорт в формате JSON'; + + @override + String get dialog_export_encryption_description => + 'Заметки будут зашифрованы паролем. Пароль должен быть сгенерирован случайным образом, длиной в 32 символа (должен содержать минимум 1 строчную букву, 1 заглавную букву, 1 цифру и 1 специальный символ) и храниться в надежном месте.'; + + @override + String get dialog_export_encryption_secondary_description_auto => + 'Этот пароль будет использован для всех автоматических экспортов заметок.'; + + @override + String get dialog_export_encryption_secondary_description_manual => + 'Этот пароль будет использоваться только для экспорта текущих заметок.'; + + @override + String get dialog_export_encryption_password_hint => 'Пароль'; + + @override + String get dialog_export_encryption_password_invalid => 'Неверно'; + + @override + String get dialog_import_encryption_password_description => + 'Данные зашифрованы. Чтобы импортировать данные, нужно ввести пароль.'; + + @override + String get dialog_import_encryption_password_error => + 'не удалось расшифровать данные. Убедитесь, что вы ввели правильный пароль.'; + + @override + String get sort_date => 'По дате'; + + @override + String get sort_title => 'По заголовку'; + + @override + String get sort_ascending => 'В порядке возрастания'; + + @override + String get placeholder_notes => 'Нет заметок'; + + @override + String get placeholder_bin => 'Нет удаленных заметок'; + + @override + String get menu_pin => 'Закрепить'; + + @override + String get menu_share => 'Поделиться'; + + @override + String get menu_unpin => 'Открепить'; + + @override + String get menu_delete => 'Удалить'; + + @override + String get menu_restore => 'Восстановить'; + + @override + String get menu_delete_permanently => 'Удалить навсегда'; + + @override + String get menu_about => 'О приложении'; + + @override + String get confirmations_title_none => 'Никогда'; + + @override + String get confirmations_title_irreversible => 'Только необратимые действия'; + + @override + String get confirmations_title_all => 'Всегда'; + + @override + String get swipe_action_disabled => 'Отключено'; + + @override + String get swipe_action_delete => 'Удалить'; + + @override + String get swipe_action_pin => 'Закрепить'; + + @override + String get dismiss_pin => 'Закрепить'; + + @override + String get dismiss_unpin => 'Открепить'; + + @override + String get dismiss_delete => 'Удалить'; + + @override + String get about_last_edited => 'Изменено'; + + @override + String get about_created => 'Создано'; + + @override + String get about_words => 'Количество слов'; + + @override + String get about_characters => 'Количество символов'; + + @override + String get time_at => 'в'; + + @override + String get action_add_note_title => 'Добавить заметку'; + + @override + String get welcome_note_title => 'Добро пожаловать в Material Notes!'; + + @override + String get welcome_note_content => 'Простые, локальные заметки в стиле Material Design'; +} diff --git a/lib/l10n/app_localizations/app_localizations_tr.g.dart b/lib/l10n/app_localizations/app_localizations_tr.g.dart index 25788a89..0a9ec04f 100644 --- a/lib/l10n/app_localizations/app_localizations_tr.g.dart +++ b/lib/l10n/app_localizations/app_localizations_tr.g.dart @@ -17,6 +17,15 @@ class AppLocalizationsTr extends AppLocalizations { return '$appName basitliği hedefleyen metin tabanlı bir not alma uygulamasıdır. Materyal Tasarımı benimser. Notları yerel olarak saklar ve internet izni yoktur, böylece notlara erişebilen tek kişi sizsiniz.'; } + @override + String get error_error => 'Hata'; + + @override + String get error_permission => 'Dosyayı yazma izni alınamadı.'; + + @override + String get error_read_file => 'Dosyayı okuma izni alınamadı.'; + @override String get navigation_notes => 'Notlar'; @@ -27,17 +36,44 @@ class AppLocalizationsTr extends AppLocalizations { String get navigation_settings => 'Ayarlar'; @override - String get error_error => 'Hata'; + String get navigation_settings_appearance => 'Appearance'; @override - String get error_permission => 'Dosyayı yazma izni alınamadı.'; + String get navigation_settings_behavior => 'Behavior'; @override - String get error_read_file => 'Dosyayı okuma izni alınamadı.'; + String get navigation_settings_editor => 'Editor'; + + @override + String get navigation_settings_backup => 'Backup'; + + @override + String get navigation_settings_about => 'About'; + + @override + String get button_ok => 'Tamam'; + + @override + String get button_close => 'Kapat'; + + @override + String get button_cancel => 'İptal et'; + + @override + String get button_add => 'Ekle'; @override String get settings_appearance => 'Görünüş'; + @override + String get settings_appearance_description => 'Language, theme, notes tiles'; + + @override + String get settings_appearance_application => 'Application'; + + @override + String get settings_appearance_notes_tiles => 'Notes tiles'; + @override String get settings_language => 'Dil'; @@ -57,7 +93,7 @@ class AppLocalizationsTr extends AppLocalizations { String get settings_dynamic_theming => 'Dinamik tema'; @override - String get settings_dynamic_theming_description => 'Sisteminizdeki rengi baz alın'; + String get settings_dynamic_theming_description => 'Generate colors from the system'; @override String get settings_black_theming => 'Siyah tema'; @@ -65,66 +101,197 @@ class AppLocalizationsTr extends AppLocalizations { @override String get settings_black_theming_description => 'Koyu modda siyah arkaplan kullanın'; + @override + String get settings_show_separators => 'Separators'; + + @override + String get settings_show_separators_description => + 'Show a separator between the notes tiles to differentiate them easily'; + + @override + String get settings_show_tiles_background => 'Background'; + + @override + String get settings_show_tiles_background_description => + 'Show the background of the notes tiles to differentiate them easily'; + + @override + String get settings_behavior => 'Davranış'; + + @override + String get settings_behavior_application => 'Application'; + + @override + String get settings_behavior_description => 'Confirmations, swipe actions'; + + @override + String get settings_behavior_swipe_actions => 'Swipe actions'; + + @override + String get settings_confirmations => 'Confirmation dialogs'; + + @override + String get settings_confirmations_description => + 'Show the confirmation dialogs for actions such as pining and deleting notes'; + + @override + String get settings_swipe_action_right => 'Right swipe action'; + + @override + String get settings_swipe_action_right_description => + 'Action to trigger when a right swipe is performed on the notes tiles'; + + @override + String get settings_swipe_action_left => 'Left swipe action'; + + @override + String get settings_swipe_action_left_description => + 'Action to trigger when a left swipe is performed on the notes tiles'; + + @override + String get settings_flag_secure => 'Flag the app as secure'; + + @override + String get settings_flag_secure_description => + 'Hide the app from the recent apps and prevent screenshots from being made'; + @override String get settings_editor => 'Editor'; + @override + String get settings_editor_formatting => 'Formatting'; + + @override + String get settings_editor_appearance => 'Appearance'; + + @override + String get settings_editor_description => 'Buttons, toolbar, spacing'; + @override String get settings_show_undo_redo_buttons => 'Undo/redo buttons'; @override - String get settings_show_undo_redo_buttons_description => 'Show the buttons to undo and redo changes in the editor'; + String get settings_show_undo_redo_buttons_description => + 'Show the buttons to undo and redo changes in the editor\'s app bar'; @override String get settings_show_checklist_button => 'Checklist button'; @override - String get settings_show_checklist_button_description => 'Show the button to toggle checklists in the editor'; + String get settings_show_checklist_button_description => + 'Show the button to toggle checklists in the editor\'s app bar, hiding it from the editor\'s toolbar if enabled'; @override - String get settings_show_toolbar => 'Editor toolbar'; + String get settings_show_toolbar => 'Toolbar'; @override - String get settings_show_toolbar_description => 'Show the editor toolbar to enable advanced text formatting'; + String get settings_show_toolbar_description => 'Show the editor\'s toolbar to enable advanced text formatting'; @override - String get settings_show_separators => 'Ayırıcıları göster'; + String get settings_use_paragraph_spacing => 'Paragraph spacing'; @override - String get settings_show_separators_description => 'Notları kolayca ayırt etmek için aralarında bir ayırıcı gösterin'; + String get settings_use_paragraph_spacing_description => 'Use spacing between paragraphs'; @override - String get settings_show_tiles_background => 'Show the tiles background'; + String get settings_backup => 'Yedekleme'; @override - String get settings_show_tiles_background_description => - 'Show the background of the notes tiles to differentiate them easily'; + String get settings_backup_description => 'Export, import'; @override - String get settings_behavior => 'Davranış'; + String get settings_backup_export => 'Export'; @override - String get settings_confirmations => 'Onay diyaloglarını göster'; + String get settings_backup_import => 'Import'; @override - String get settings_backup => 'Yedekleme'; + String get settings_auto_export => 'Auto export as JSON'; @override - String get settings_export_json => 'JSON olarak dışa aktar'; + String get settings_auto_export_description => + 'Automatically export the notes to a JSON file (bin included) that can be imported back'; @override - String get settings_export_markdown => 'Markdown olarak dışa aktar'; + String settings_auto_export_value(String encrypt, String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + String _temp1 = intl.Intl.selectLogic( + encrypt, + { + 'true': 'encrypted', + 'false': 'not encrypted', + 'other': '', + }, + ); + return 'Every $_temp0, $_temp1'; + } @override - String get settings_export_json_description => - 'Notları daha sonra kurtarabilmek için bir JSON dosyasına (çöp kutusu dahil) aktarın'; + String get settings_auto_export_disabled => 'Disabled'; + + @override + String settings_auto_export_directory(Object directory) { + return 'Exports can be found in $directory'; + } @override - String get settings_export_markdown_description => - 'Notları daha sonra kurtarabilmek için bir markdown dosyasına (çöp kutusu dahil) aktarın'; + String get settings_auto_export_dialog_description_disabled => 'Auto export will be disabled.'; + + @override + String settings_auto_export_dialog_description_enabled(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Auto export will be performed every $_temp0. Set the frequency to 0 to disable it.'; + } + + @override + String settings_auto_export_dialog_slider_label(String frequency) { + String _temp0 = intl.Intl.selectLogic( + frequency, + { + '1': 'day', + '7': 'week', + '14': '2 weeks', + '30': 'month', + 'other': '$frequency days', + }, + ); + return 'Every $_temp0'; + } @override String get settings_export_success => 'Dışa aktarma başarılı'; + @override + String get settings_export_json => 'JSON olarak dışa aktar'; + + @override + String get settings_export_json_description => + 'Immediately export the notes to a JSON file (bin included) that can be imported back'; + + @override + String get settings_export_markdown => 'Markdown olarak dışa aktar'; + + @override + String get settings_export_markdown_description => 'Immediately export the notes to a Markdown file (bin included)'; + @override String get settings_import => 'İçe aktar'; @@ -137,6 +304,27 @@ class AppLocalizationsTr extends AppLocalizations { @override String get settings_about => 'Hakkında'; + @override + String get settings_about_application => 'Application'; + + @override + String get settings_about_links => 'Links'; + + @override + String get settings_about_help => 'Help'; + + @override + String get settings_about_description => 'Information, help, GitHub, license'; + + @override + String get settings_build_mode => 'Build mode'; + + @override + String get settings_build_mode_release => 'Release'; + + @override + String get settings_build_mode_debug => 'Debug'; + @override String get settings_github => 'GitHub'; @@ -150,10 +338,22 @@ class AppLocalizationsTr extends AppLocalizations { String get settings_licence_description => 'AGPL-3.0'; @override - String get settings_issue => 'Hata bildir'; + String get settings_github_issues => 'Report a bug'; @override - String get settings_issue_description => 'GitHub\'da bir issue oluşturarak bir hata bildirin'; + String get settings_github_issues_description => 'Report a bug by creating a GitHub issue'; + + @override + String get settings_github_discussions => 'Ask a question'; + + @override + String get settings_github_discussions_description => 'Ask a question on GitHub discussions'; + + @override + String get settings_get_in_touch => 'Contact the developer'; + + @override + String get settings_get_in_touch_description => 'Contact the developer via mail at contact@maelchiotti.dev'; @override String get hint_title => 'Başlık'; @@ -200,18 +400,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get tooltip_toggle_pins => 'Sabitlemeyi aç/kapat'; - @override - String get button_ok => 'Tamam'; - - @override - String get button_close => 'Kapat'; - - @override - String get button_cancel => 'İptal et'; - - @override - String get button_add => 'Ekle'; - @override String get dialog_delete => 'Sil'; @@ -288,6 +476,35 @@ class AppLocalizationsTr extends AppLocalizations { String get dialog_empty_bin_body => 'Çöp kutusunu gerçekten kalıcı olarak boşaltmak istiyor musunuz? İçerdiği notları kurtaramazsınız'; + @override + String get dialog_export_encryption_switch => 'Encrypt the JSON export'; + + @override + String get dialog_export_encryption_description => + 'The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.'; + + @override + String get dialog_export_encryption_secondary_description_auto => + 'This password will be used for all future auto exports.'; + + @override + String get dialog_export_encryption_secondary_description_manual => + 'This password will only be used for this export.'; + + @override + String get dialog_export_encryption_password_hint => 'Password'; + + @override + String get dialog_export_encryption_password_invalid => 'Invalid'; + + @override + String get dialog_import_encryption_password_description => + 'This export is encrypted. To import it, you need to provide the password used to encrypt it.'; + + @override + String get dialog_import_encryption_password_error => + 'the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.'; + @override String get sort_date => 'Tarih'; @@ -301,7 +518,7 @@ class AppLocalizationsTr extends AppLocalizations { String get placeholder_notes => 'Not yok'; @override - String get placeholder_bin => 'Çöp kutusu boş'; + String get placeholder_bin => 'No deleted notes'; @override String get menu_pin => 'Sabitle'; @@ -334,19 +551,22 @@ class AppLocalizationsTr extends AppLocalizations { String get confirmations_title_all => 'Her zaman'; @override - String get dismiss_pin => 'Sabitle'; + String get swipe_action_disabled => 'Disabled'; @override - String get dismiss_unpin => 'Sabitleme'; + String get swipe_action_delete => 'Delete'; @override - String get dismiss_delete => 'Sil'; + String get swipe_action_pin => 'Pin'; @override - String get dismiss_permanently_delete => 'Kalıcı sil'; + String get dismiss_pin => 'Sabitle'; @override - String get dismiss_restore => 'Kurtar'; + String get dismiss_unpin => 'Sabitleme'; + + @override + String get dismiss_delete => 'Sil'; @override String get about_last_edited => 'Son düzenleme'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 00000000..b13b8f2d --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,162 @@ +{ + "app_name": "Material Notes", + "app_tagline": "Простые, локальные заметки в стиле material design", + "app_about": "{appName} – это приложение для создания простых текстовых заметок. Приложение выполнено в стиле Material Design. Оно хранит заметки локально на устройстве и работает без подключения к Интернету, поэтому только вы имеете доступ к заметкам.", + "error_error": "Ошибка", + "error_permission": "Не удалось получить разрешение на запись файла.", + "error_read_file": "Не удалось прочитать файл.", + "navigation_notes": "Заметки", + "navigation_bin": "Корзина", + "navigation_settings": "Настройки", + "navigation_settings_appearance": "Персонализация", + "navigation_settings_behavior": "Поведение", + "navigation_settings_editor": "Редактор", + "navigation_settings_backup": "Резервное копирование", + "navigation_settings_about": "О приложении", + "button_ok": "Оk", + "button_close": "Закрыть", + "button_cancel": "Отмена", + "button_add": "Добавить", + "settings_appearance": "Персонализация", + "settings_appearance_description": "Язык, тема, стили заметок", + "settings_appearance_application": "Приложение", + "settings_appearance_notes_tiles": "Стиль заметок", + "settings_language": "Язык", + "settings_theme": "Тема оформления", + "settings_theme_system": "По умолчанию", + "settings_theme_light": "Светлая", + "settings_theme_dark": "Тёмная", + "settings_dynamic_theming": "Динамические цвета", + "settings_dynamic_theming_description": "Использовать цвет, установленный в системе", + "settings_black_theming": "Натуральный чёрный", + "settings_black_theming_description": "Использовать чёрный фон при тёмном режиме", + "settings_show_separators": "Разделители", + "settings_show_separators_description": "Показывать разделитель между заметками", + "settings_show_tiles_background": "Фон", + "settings_show_tiles_background_description": "Показывать фон у карточек с заметками", + "settings_behavior": "Поведение", + "settings_behavior_application": "Приложение", + "settings_behavior_description": "Подтверждения, действия свайпа", + "settings_behavior_swipe_actions": "Действия при свайпе", + "settings_confirmations": "Диалоги подтверждения", + "settings_confirmations_description": "Показать диалоги подтверждения при закреплении или удалении заметок", + "settings_swipe_action_right": "Действие при свайпе вправо", + "settings_swipe_action_right_description": "Действие, которое будет выполняться при свайпе вправо по заметке", + "settings_swipe_action_left": "Действие при свайпе влево", + "settings_swipe_action_left_description": "Действие, которое будет выполняться при свайпе влево по заметке", + "settings_flag_secure": "Помечать приложение как защищённое", + "settings_flag_secure_description": "Скрывать приложение из недавних приложений и предотвращать создание скриншотов", + "settings_editor": "Редактор", + "settings_editor_formatting": "Форматирование", + "settings_editor_appearance": "Персонализация", + "settings_editor_description": "Кнопки, панель инструментов, интервал", + "settings_show_undo_redo_buttons": "Кнопки отмены/повтора", + "settings_show_undo_redo_buttons_description": "Показывать кнопки отмены/повтора изменений в редакторе", + "settings_show_checklist_button": "Кнопка для переключения списков", + "settings_show_checklist_button_description": "Показать кнопку для переключения чек-листов в панели приложений редактора, скрывая его из панели инструментов редактора, если включено", + "settings_show_toolbar": "Панель инструментов", + "settings_show_toolbar_description": "Показать панель инструментов для расширенного форматирования текста", + "settings_use_paragraph_spacing": "Интервал между параграфами", + "settings_use_paragraph_spacing_description": "Использовать интервал между параграфами", + "settings_backup": "Резервное копирование", + "settings_backup_description": "Экспорт, импорт", + "settings_backup_export": "Экспорт", + "settings_backup_import": "Импорт", + "settings_auto_export": "Автоматический экспорт в формате JSON", + "settings_auto_export_description": "Автоматически экспортировать заметки в формате JSON (включая заметки из корзины)", + "settings_auto_export_value": "{frequency, select, 1{Каждый день} 7{Каждую неделю} 14{Каждые 2 недели} 30{Каждый месяц} other{Каждые {frequency} дней}}, {encrypt, select, true{зашифровано} false{не зашифровано} other{}}", + "settings_auto_export_disabled": "Отключено", + "settings_auto_export_directory": "Экспортированные заметки находятся в {directory}", + "settings_auto_export_dialog_description_disabled": "Автоматический экспорт будет отключен.", + "settings_auto_export_dialog_description_enabled": "Автоматический экспорт будет производиться {frequency, select, 1{каждый день} 7{каждую неделю} 14{каждые 2 недели} 30{каждый месяц} other{каждые {frequency} дней}}. Установите значение на 0, чтобы отключить автоматический экспорт.", + "settings_auto_export_dialog_slider_label": "{frequency, select, 1{Каждый день} 7{Каждую неделю} 14{Каждые 2 недели} 30{Каждый месяц} other{Каждые {frequency} дней}}", + "settings_export_success": "Заметки были успешно экспортированы.", + "settings_export_json": "Экспортировать в формате JSON", + "settings_export_json_description": "Моментально экспортировать заметки в формате JSON (включая заметки из корзины)", + "settings_export_markdown": "Экспортировать в формате Markdown", + "settings_export_markdown_description": "Моментально экспортировать заметки в формате Markdown (включая заметки из корзины)", + "settings_import": "Импорт заметок", + "settings_import_description": "Импортировать заметки из JSON–файла", + "settings_import_success": "Заметки были успешно импортированы.", + "settings_about": "О приложении", + "settings_about_application": "Приложение", + "settings_about_links": "Ссылки", + "settings_about_help": "Справка", + "settings_about_description": "Информация, справка, GitHub, лицензия", + "settings_build_mode": "Режим сборки", + "settings_build_mode_release": "Release", + "settings_build_mode_debug": "Debug", + "settings_github": "GitHub", + "settings_github_description": "Ознакомьтесь с исходным кодом приложения", + "settings_licence": "Лицензия", + "settings_licence_description": "AGPL-3.0", + "settings_github_issues": "Сообщить об ошибке", + "settings_github_issues_description": "Сообщить об ошибке через GitHub", + "settings_github_discussions": "Задать вопрос", + "settings_github_discussions_description": "Задайте вопрос на обсуждениях в GitHub", + "settings_get_in_touch": "Связаться с разработчиком", + "settings_get_in_touch_description": "Свяжитесь с разработчиком по почте contact@maelchiotti.dev", + "hint_title": "Заголовок", + "hint_note": "Заметка", + "tooltip_fab_add_note": "Добавить заметку", + "tooltip_fab_empty_bin": "Очистить корзину", + "tooltip_layout_list": "Список", + "tooltip_layout_grid": "Сетка", + "tooltip_sort": "Сортировать заметки", + "tooltip_search": "Поиск заметок", + "tooltip_toggle_checkbox": "Переключить флажок", + "tooltip_select_all": "Выбрать все", + "tooltip_unselect_all": "Отменить выбор", + "tooltip_delete": "Удалить", + "tooltip_permanently_delete": "Удалить навсегда", + "tooltip_restore": "Восстановить", + "tooltip_toggle_pins": "Закрепить/Открепить", + "dialog_delete": "Удалить", + "dialog_delete_body": "Вы действительно хотите удалить {count} {count, plural, one{заметку} few {заметки} many {заметок} other{заметки}}? Вы можете восстановить {count, plural, one{её} other{их}} из корзины.", + "dialog_delete_body_single": "Вы действительно хотите поместить эту заметку в корзину? Вы можете восстановить её из корзины.", + "dialog_permanently_delete": "Удалить", + "dialog_permanently_delete_body": "Вы действительно хотите навсегда удалить {count} {count, plural, one{заметку} few {заметки} many {заметок} other{заметки}}? Вы не сможете {count, plural, one{её} other{их}} восстановить.", + "dialog_permanently_delete_body_single": "Вы действительно хотите навсегда удалить эту заметку? Заметка будет безвозвратно удалена.", + "dialog_restore": "Восстановить", + "dialog_restore_body": "Вы действительно хотите восстановить {count} {count, plural, one{заметку} few {заметки} many {заметок} other{заметки}}?", + "dialog_restore_body_single": "Вы действительно хотите восстановить эту заметку?", + "dialog_empty_bin": "Очистить корзину", + "dialog_empty_bin_body": "Вы действительно хотите отчистить корзину? Все заметки будут безвозвратно удалены.", + "dialog_export_encryption_switch": "Зашифровать экспорт в формате JSON", + "dialog_export_encryption_description": "Заметки будут зашифрованы паролем. Пароль должен быть сгенерирован случайным образом, длиной в 32 символа (должен содержать минимум 1 строчную букву, 1 заглавную букву, 1 цифру и 1 специальный символ) и храниться в надежном месте.", + "dialog_export_encryption_secondary_description_auto": "Этот пароль будет использован для всех автоматических экспортов заметок.", + "dialog_export_encryption_secondary_description_manual": "Этот пароль будет использоваться только для экспорта текущих заметок.", + "dialog_export_encryption_password_hint": "Пароль", + "dialog_export_encryption_password_invalid": "Неверно", + "dialog_import_encryption_password_description": "Данные зашифрованы. Чтобы импортировать данные, нужно ввести пароль.", + "dialog_import_encryption_password_error": "не удалось расшифровать данные. Убедитесь, что вы ввели правильный пароль.", + "sort_date": "По дате", + "sort_title": "По заголовку", + "sort_ascending": "В порядке возрастания", + "placeholder_notes": "Нет заметок", + "placeholder_bin": "Нет удаленных заметок", + "menu_pin": "Закрепить", + "menu_share": "Поделиться", + "menu_unpin": "Открепить", + "menu_delete": "Удалить", + "menu_restore": "Восстановить", + "menu_delete_permanently": "Удалить навсегда", + "menu_about": "О приложении", + "confirmations_title_none": "Никогда", + "confirmations_title_irreversible": "Только необратимые действия", + "confirmations_title_all": "Всегда", + "swipe_action_disabled": "Отключено", + "swipe_action_delete": "Удалить", + "swipe_action_pin": "Закрепить", + "dismiss_pin": "Закрепить", + "dismiss_unpin": "Открепить", + "dismiss_delete": "Удалить", + "about_last_edited": "Изменено", + "about_created": "Создано", + "about_words": "Количество слов", + "about_characters": "Количество символов", + "time_at": "в", + "action_add_note_title": "Добавить заметку", + "welcome_note_title": "Добро пожаловать в Material Notes!", + "welcome_note_content": "Простые, локальные заметки в стиле Material Design" +} \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 3fb0bceb..432e317c 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -2,46 +2,106 @@ "app_name": "Material Notes", "app_tagline": "Basit, çevrimdışı, materyal tasarımlı notlar", "app_about": "{appName} basitliği hedefleyen metin tabanlı bir not alma uygulamasıdır. Materyal Tasarımı benimser. Notları yerel olarak saklar ve internet izni yoktur, böylece notlara erişebilen tek kişi sizsiniz.", - "navigation_notes": "Notlar", - "navigation_bin": "Çöp Kutusu", - "navigation_settings": "Ayarlar", "error_error": "Hata", "error_permission": "Dosyayı yazma izni alınamadı.", "error_read_file": "Dosyayı okuma izni alınamadı.", + "navigation_notes": "Notlar", + "navigation_bin": "Çöp Kutusu", + "navigation_settings": "Ayarlar", + "navigation_settings_appearance": "Appearance", + "navigation_settings_behavior": "Behavior", + "navigation_settings_editor": "Editor", + "navigation_settings_backup": "Backup", + "navigation_settings_about": "About", + "button_ok": "Tamam", + "button_close": "Kapat", + "button_cancel": "İptal et", + "button_add": "Ekle", "settings_appearance": "Görünüş", + "settings_appearance_description": "Language, theme, notes tiles", + "settings_appearance_application": "Application", + "settings_appearance_notes_tiles": "Notes tiles", "settings_language": "Dil", "settings_theme": "Tema", "settings_theme_system": "Sistem", "settings_theme_light": "Açık", "settings_theme_dark": "Koyu", "settings_dynamic_theming": "Dinamik tema", - "settings_dynamic_theming_description": "Sisteminizdeki rengi baz alın", + "settings_dynamic_theming_description": "Generate colors from the system", "settings_black_theming": "Siyah tema", "settings_black_theming_description": "Koyu modda siyah arkaplan kullanın", - "settings_show_separators": "Ayırıcıları göster", - "settings_show_separators_description": "Notları kolayca ayırt etmek için aralarında bir ayırıcı gösterin", + "settings_show_separators": "Separators", + "settings_show_separators_description": "Show a separator between the notes tiles to differentiate them easily", + "settings_show_tiles_background": "Background", + "settings_show_tiles_background_description": "Show the background of the notes tiles to differentiate them easily", "settings_behavior": "Davranış", - "settings_confirmations": "Onay diyaloglarını göster", + "settings_behavior_application": "Application", + "settings_behavior_description": "Confirmations, swipe actions", + "settings_behavior_swipe_actions": "Swipe actions", + "settings_confirmations": "Confirmation dialogs", + "settings_confirmations_description": "Show the confirmation dialogs for actions such as pining and deleting notes", + "settings_swipe_action_right": "Right swipe action", + "settings_swipe_action_right_description": "Action to trigger when a right swipe is performed on the notes tiles", + "settings_swipe_action_left": "Left swipe action", + "settings_swipe_action_left_description": "Action to trigger when a left swipe is performed on the notes tiles", + "settings_flag_secure": "Flag the app as secure", + "settings_flag_secure_description": "Hide the app from the recent apps and prevent screenshots from being made", + "settings_editor": "Editor", + "settings_editor_formatting": "Formatting", + "settings_editor_appearance": "Appearance", + "settings_editor_description": "Buttons, toolbar, spacing", + "settings_show_undo_redo_buttons": "Undo/redo buttons", + "settings_show_undo_redo_buttons_description": "Show the buttons to undo and redo changes in the editor''s app bar", + "settings_show_checklist_button": "Checklist button", + "settings_show_checklist_button_description": "Show the button to toggle checklists in the editor''s app bar, hiding it from the editor''s toolbar if enabled", + "settings_show_toolbar": "Toolbar", + "settings_show_toolbar_description": "Show the editor''s toolbar to enable advanced text formatting", + "settings_use_paragraph_spacing": "Paragraph spacing", + "settings_use_paragraph_spacing_description": "Use spacing between paragraphs", "settings_backup": "Yedekleme", + "settings_backup_description": "Export, import", + "settings_backup_export": "Export", + "settings_backup_import": "Import", + "settings_auto_export": "Auto export as JSON", + "settings_auto_export_description": "Automatically export the notes to a JSON file (bin included) that can be imported back", + "settings_auto_export_value": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}, {encrypt, select, true{encrypted} false{not encrypted} other{}}", + "settings_auto_export_disabled": "Disabled", + "settings_auto_export_directory": "Exports can be found in {directory}", + "settings_auto_export_dialog_description_disabled": "Auto export will be disabled.", + "settings_auto_export_dialog_description_enabled": "Auto export will be performed every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}. Set the frequency to 0 to disable it.", + "settings_auto_export_dialog_slider_label": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{{frequency} days}}", + "settings_export_success": "Dışa aktarma başarılı", "settings_export_json": "JSON olarak dışa aktar", + "settings_export_json_description": "Immediately export the notes to a JSON file (bin included) that can be imported back", "settings_export_markdown": "Markdown olarak dışa aktar", - "settings_export_json_description": "Notları daha sonra kurtarabilmek için bir JSON dosyasına (çöp kutusu dahil) aktarın", - "settings_export_markdown_description": "Notları daha sonra kurtarabilmek için bir markdown dosyasına (çöp kutusu dahil) aktarın", - "settings_export_success": "Dışa aktarma başarılı", + "settings_export_markdown_description": "Immediately export the notes to a Markdown file (bin included)", "settings_import": "İçe aktar", "settings_import_description": "JSON dosyasından içe aktar", "settings_import_success": "İçe aktarma başarılı.", "settings_about": "Hakkında", + "settings_about_application": "Application", + "settings_about_links": "Links", + "settings_about_help": "Help", + "settings_about_description": "Information, help, GitHub, license", + "settings_build_mode": "Build mode", + "settings_build_mode_release": "Release", + "settings_build_mode_debug": "Debug", "settings_github": "GitHub", "settings_github_description": "Kaynak koduna göz at", "settings_licence": "Lisans", "settings_licence_description": "AGPL-3.0", - "settings_issue": "Hata bildir", - "settings_issue_description": "GitHub''da bir issue oluşturarak bir hata bildirin", + "settings_github_issues": "Report a bug", + "settings_github_issues_description": "Report a bug by creating a GitHub issue", + "settings_github_discussions": "Ask a question", + "settings_github_discussions_description": "Ask a question on GitHub discussions", + "settings_get_in_touch": "Contact the developer", + "settings_get_in_touch_description": "Contact the developer via mail at contact@maelchiotti.dev", "hint_title": "Başlık", "hint_note": "Note", "tooltip_fab_add_note": "Bir not ekle", "tooltip_fab_empty_bin": "Çöp kutusunu boşalt", + "tooltip_layout_list": "List view", + "tooltip_layout_grid": "Grid view", "tooltip_sort": "Notları sırala", "tooltip_search": "Notların içinde ara", "tooltip_toggle_checkbox": "Onay kutusunu aç/kapat", @@ -51,10 +111,6 @@ "tooltip_permanently_delete": "Kalıcı sil", "tooltip_restore": "Kurtar", "tooltip_toggle_pins": "Sabitlemeyi aç/kapat", - "button_ok": "Tamam", - "button_close": "Kapat", - "button_cancel": "İptal et", - "button_add": "Ekle", "dialog_delete": "Sil", "dialog_delete_body": "Gerçekten {count} {count, plural, zero{} one{notu} other{notu}} silmek istiyor musunuz? Çöp kutusundan {count, plural, zero{} one{onu} other{onları}} kurtarabilirsiniz.", "dialog_delete_body_single": "Bu notu gerçekten silmek istiyor musunuz?Çöp kutusundan kurtarabilirsiniz.", @@ -66,11 +122,19 @@ "dialog_restore_body_single": "Bu notu gerçekten kurtarmak istiyor musunuz?", "dialog_empty_bin": "Çöp kutusunu boşalt", "dialog_empty_bin_body": "Çöp kutusunu gerçekten kalıcı olarak boşaltmak istiyor musunuz? İçerdiği notları kurtaramazsınız", + "dialog_export_encryption_switch": "Encrypt the JSON export", + "dialog_export_encryption_description": "The title and the content of the notes will be encrypted using your password. It should be randomly generated, exactly 32 characters long, strong (at least 1 lowercase, 1 uppercase, 1 number and 1 special character) and securely stored.", + "dialog_export_encryption_secondary_description_auto": "This password will be used for all future auto exports.", + "dialog_export_encryption_secondary_description_manual": "This password will only be used for this export.", + "dialog_export_encryption_password_hint": "Password", + "dialog_export_encryption_password_invalid": "Invalid", + "dialog_import_encryption_password_description": "This export is encrypted. To import it, you need to provide the password used to encrypt it.", + "dialog_import_encryption_password_error": "the decrypting of the export failed. Please check that you provided the same password that the one you used for encrypting the export.", "sort_date": "Tarih", "sort_title": "Başlık", "sort_ascending": "Artan", "placeholder_notes": "Not yok", - "placeholder_bin": "Çöp kutusu boş", + "placeholder_bin": "No deleted notes", "menu_pin": "Sabitle", "menu_share": "Paylaş", "menu_unpin": "Sabitleme", @@ -81,11 +145,12 @@ "confirmations_title_none": "Asla", "confirmations_title_irreversible": "Sadece geri alınamaz eylemler", "confirmations_title_all": "Her zaman", + "swipe_action_disabled": "Disabled", + "swipe_action_delete": "Delete", + "swipe_action_pin": "Pin", "dismiss_pin": "Sabitle", "dismiss_unpin": "Sabitleme", "dismiss_delete": "Sil", - "dismiss_permanently_delete": "Kalıcı sil", - "dismiss_restore": "Kurtar", "about_last_edited": "Son düzenleme", "about_created": "Oluşturma tarihi", "about_words": "Kelime", diff --git a/lib/l10n/localization_completion.dart b/lib/l10n/localization_completion.dart new file mode 100644 index 00000000..4d181bad --- /dev/null +++ b/lib/l10n/localization_completion.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// Lists the localization completion for every supported language. +enum LocalizationCompletion { + en(Locale('en'), 1), + es(Locale('es'), .42), + fr(Locale('fr'), 1), + ru(Locale('ru'), 1), + tr(Locale('tr'), .41), + ; + + /// The locale of this localization. + final Locale locale; + + /// The percentage of strings that are localized for this [locale]. + /// + /// The value is a double contained between 0 and 1. + final double percentage; + + const LocalizationCompletion(this.locale, this.percentage); + + /// Returns the percentage of strings that are localized for the [locale], formatted as a String for the [locale]. + static String getFormattedPercentage(Locale locale) { + final percentage = values.firstWhere((localizationSupport) { + return localizationSupport.locale == locale; + }).percentage; + + final formatter = NumberFormat.percentPattern(locale.languageCode); + + return formatter.format(percentage); + } +} diff --git a/lib/main.dart b/lib/main.dart index 23d38cc9..09419168 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:localmaterialnotes/app.dart'; +import 'package:localmaterialnotes/utils/auto_export_utils.dart'; import 'package:localmaterialnotes/utils/database_utils.dart'; +import 'package:localmaterialnotes/utils/flag_secure_utils.dart'; import 'package:localmaterialnotes/utils/info_utils.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; import 'package:localmaterialnotes/utils/theme_utils.dart'; @@ -11,6 +14,11 @@ import 'package:localmaterialnotes/utils/theme_utils.dart'; Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + // Display the application behind the system's notifications bar and navigation bar + // See https://github.com/flutter/flutter/issues/40974 + // See https://github.com/flutter/flutter/issues/34678 SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( @@ -18,12 +26,21 @@ Future main() async { ), ); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + // Set the application refresh rate + // See https://github.com/flutter/flutter/issues/35162 + await FlutterDisplayMode.setHighRefreshRate(); + + // Initialize all the utilities + await PreferencesUtils().ensureInitialized(); + await InfoUtils().ensureInitialized(); + await ThemeUtils().ensureInitialized(); + await DatabaseUtils().ensureInitialized(); + + // No need to await this, it can be performed in the background + AutoExportUtils().ensureInitialized(); - await PreferencesUtils().init(); - await InfoUtils().init(); - await ThemeUtils().init(); - await DatabaseUtils().init(); + // Set FLAG_SECURE if needed + await setFlagSecureIfNeeded(); FlutterNativeSplash.remove(); diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index 52ee07e0..b5079bb9 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -6,34 +6,55 @@ import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:isar/isar.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/encryption_utils.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/sort_method.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; -import 'package:localmaterialnotes/utils/preferences/sort_method.dart'; part 'note.g.dart'; // ignore_for_file: must_be_immutable +/// Rich text note with title, content and metadata. @JsonSerializable() @Collection(inheritance: false) class Note extends Equatable { + /// Empty content in fleather data representation. static const String _emptyContent = '[{"insert":"\\n"}]'; + /// Id of the note. + /// + /// It's excluded from the JSON because it's fully managed by Isar. @JsonKey(includeFromJson: false, includeToJson: false) - String? id; - @Index() - late bool deleted; - @Index() - late bool pinned; - late DateTime createdTime; - late DateTime editedTime; - late String title; - late String content; + Id id = Isar.autoIncrement; + + /// Whether the note is selected. + /// + /// It's excluded from the JSON because it's only needed temporarily during multi-selection. @JsonKey(includeFromJson: false, includeToJson: false) @ignore - late bool selected = false; + bool selected = false; + + /// Whether the note is deleted. + @Index() + bool deleted; + + /// Whether the note is pinned. + @Index() + bool pinned; + + /// Date of creation. + DateTime createdTime; + + /// Last date of edition. + DateTime editedTime; + + /// Title (simple text). + String title; + + /// Content (rich text in the fleather representation). + String content; Note({ - this.id, required this.deleted, required this.pinned, required this.createdTime, @@ -42,8 +63,8 @@ class Note extends Equatable { required this.content, }); + /// Note with empty title and content. factory Note.empty() => Note( - id: uuid.v4(), deleted: false, pinned: false, createdTime: DateTime.now(), @@ -52,8 +73,8 @@ class Note extends Equatable { content: _emptyContent, ); + /// Note with the provided [content]. factory Note.content(String content) => Note( - id: uuid.v4(), deleted: false, pinned: false, createdTime: DateTime.now(), @@ -62,8 +83,8 @@ class Note extends Equatable { content: content, ); + /// Welcome note (localized). factory Note.welcome() => Note( - id: uuid.v4(), deleted: false, pinned: true, createdTime: DateTime.now(), @@ -72,31 +93,39 @@ class Note extends Equatable { content: '[{"insert":"${hardcodedLocalizations.welcomeNoteContent}\\n"}]', ); - // Manually setting the ID for imports - factory Note.fromJson(Map json) => _$NoteFromJson(json)..id = uuid.v4(); - - Map toJson() => _$NoteToJson(this); + /// Note from [json] data. + factory Note.fromJson(Map json) => _$NoteFromJson(json); - Id get isarId { - var hash = 0xcbf29ce484222325; + /// Note from [json] data, encrypted with [password]. + factory Note.fromJsonEncrypted(Map json, String password) { + return _$NoteFromJson(json) + ..title = (json['title'] as String).isEmpty ? '' : EncryptionUtils().decrypt(password, json['title'] as String) + ..content = EncryptionUtils().decrypt(password, json['content'] as String); + } - var i = 0; - while (i < id!.length) { - final codeUnit = id!.codeUnitAt(i++); - hash ^= codeUnit >> 8; - hash *= 0x100000001b3; - hash ^= codeUnit & 0xFF; - hash *= 0x100000001b3; - } + /// Note to JSON. + Map toJson() => _$NoteToJson(this); - return hash; + /// Returns this note with the [title] and the [content] encrypted with the [password]. + Note encrypted(String password) { + return this + ..title = isTitleEmpty ? '' : EncryptionUtils().encrypt(password, title) + ..content = EncryptionUtils().encrypt(password, content); } + /// Note content as plain text. @ignore String get plainText { return document.toPlainText(); } + /// Note content for the preview of the notes tiles. + /// + /// Formats the following rich text elements: + /// - Checkboxes (TODO: only partially, see https://github.com/maelchiotti/LocalMaterialNotes/issues/121) + /// + /// Skips the following rich text elements: + /// - Horizontal rules @ignore String get contentPreview { var content = ''; @@ -133,41 +162,56 @@ class Note extends Equatable { return content.trim(); } + /// Note content as markdown. @ignore String get markdown { return parchmentMarkdownCodec.encode(document); } + /// Note title and content to be shared as a single text. + /// + /// Uses the [contentPreview] for the content. @ignore String get shareText { return '$title\n\n$contentPreview'; } + /// Document containing the fleather content representation. @ignore ParchmentDocument get document { return ParchmentDocument.fromJson(jsonDecode(content) as List); } + /// Whether the title is empty. @ignore bool get isTitleEmpty { return title.isEmpty; } + /// Whether the content is empty. @ignore bool get isContentEmpty { return content == _emptyContent; } + /// Whether the preview of the content is empty. @ignore bool get isContentPreviewEmpty { return contentPreview.isEmpty; } + /// Whether the note is empty. + /// + /// Checks both the title and the content. @ignore bool get isEmpty { return isTitleEmpty && isContentEmpty; } + /// Returns whether the [search] matches the note. + /// + /// Checks if the [search] is directly present in the title and the content, + /// but also uses fuzzy search in the title. This cannot be done on the content for performance reasons. bool matchesSearch(String search) { final searchCleaned = search.toLowerCase().trim(); @@ -179,6 +223,9 @@ class Note extends Equatable { return titleContains || contentContains || titleMatches; } + /// Notes are sorted according to: + /// 1. Their pin state. + /// 2. The sort method chosen by the user. int compareTo(Note otherNote) { final sortMethod = SortMethod.fromPreference(); final sortAscending = PreferenceKey.sortAscending.getPreferenceOrDefault(); @@ -196,7 +243,7 @@ class Note extends Equatable { case SortMethod.title: return sortAscending ? title.compareTo(otherNote.title) : otherNote.title.compareTo(title); default: - throw Exception(); + throw Exception('The sort method is not valid: $sortMethod'); } } } diff --git a/lib/models/note/note.g.dart b/lib/models/note/note.g.dart index fe234b7a..e9b6ee00 100644 --- a/lib/models/note/note.g.dart +++ b/lib/models/note/note.g.dart @@ -37,18 +37,13 @@ const NoteSchema = CollectionSchema( name: r'editedTime', type: IsarType.dateTime, ), - r'id': PropertySchema( - id: 4, - name: r'id', - type: IsarType.string, - ), r'pinned': PropertySchema( - id: 5, + id: 4, name: r'pinned', type: IsarType.bool, ), r'title': PropertySchema( - id: 6, + id: 5, name: r'title', type: IsarType.string, ) @@ -57,7 +52,7 @@ const NoteSchema = CollectionSchema( serialize: _noteSerialize, deserialize: _noteDeserialize, deserializeProp: _noteDeserializeProp, - idName: r'isarId', + idName: r'id', indexes: { r'deleted': IndexSchema( id: 2416515181749931262, @@ -91,7 +86,7 @@ const NoteSchema = CollectionSchema( getId: _noteGetId, getLinks: _noteGetLinks, attach: _noteAttach, - version: '3.1.0+1', + version: '3.1.7', ); int _noteEstimateSize( @@ -101,12 +96,6 @@ int _noteEstimateSize( ) { var bytesCount = offsets.last; bytesCount += 3 + object.content.length * 3; - { - final value = object.id; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } bytesCount += 3 + object.title.length * 3; return bytesCount; } @@ -121,9 +110,8 @@ void _noteSerialize( writer.writeDateTime(offsets[1], object.createdTime); writer.writeBool(offsets[2], object.deleted); writer.writeDateTime(offsets[3], object.editedTime); - writer.writeString(offsets[4], object.id); - writer.writeBool(offsets[5], object.pinned); - writer.writeString(offsets[6], object.title); + writer.writeBool(offsets[4], object.pinned); + writer.writeString(offsets[5], object.title); } Note _noteDeserialize( @@ -137,10 +125,10 @@ Note _noteDeserialize( createdTime: reader.readDateTime(offsets[1]), deleted: reader.readBool(offsets[2]), editedTime: reader.readDateTime(offsets[3]), - id: reader.readStringOrNull(offsets[4]), - pinned: reader.readBool(offsets[5]), - title: reader.readString(offsets[6]), + pinned: reader.readBool(offsets[4]), + title: reader.readString(offsets[5]), ); + object.id = id; return object; } @@ -160,10 +148,8 @@ P _noteDeserializeProp

( case 3: return (reader.readDateTime(offset)) as P; case 4: - return (reader.readStringOrNull(offset)) as P; - case 5: return (reader.readBool(offset)) as P; - case 6: + case 5: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -171,17 +157,19 @@ P _noteDeserializeProp

( } Id _noteGetId(Note object) { - return object.isarId; + return object.id; } List> _noteGetLinks(Note object) { return []; } -void _noteAttach(IsarCollection col, Id id, Note object) {} +void _noteAttach(IsarCollection col, Id id, Note object) { + object.id = id; +} extension NoteQueryWhereSort on QueryBuilder { - QueryBuilder anyIsarId() { + QueryBuilder anyId() { return QueryBuilder.apply(this, (query) { return query.addWhereClause(const IdWhereClause.any()); }); @@ -205,64 +193,64 @@ extension NoteQueryWhereSort on QueryBuilder { } extension NoteQueryWhere on QueryBuilder { - QueryBuilder isarIdEqualTo(Id isarId) { + QueryBuilder idEqualTo(Id id) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IdWhereClause.between( - lower: isarId, - upper: isarId, + lower: id, + upper: id, )); }); } - QueryBuilder isarIdNotEqualTo(Id isarId) { + QueryBuilder idNotEqualTo(Id id) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), + IdWhereClause.lessThan(upper: id, includeUpper: false), ) .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), + IdWhereClause.greaterThan(lower: id, includeLower: false), ); } else { return query .addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: false), + IdWhereClause.greaterThan(lower: id, includeLower: false), ) .addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: false), + IdWhereClause.lessThan(upper: id, includeUpper: false), ); } }); } - QueryBuilder isarIdGreaterThan(Id isarId, {bool include = false}) { + QueryBuilder idGreaterThan(Id id, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( - IdWhereClause.greaterThan(lower: isarId, includeLower: include), + IdWhereClause.greaterThan(lower: id, includeLower: include), ); }); } - QueryBuilder isarIdLessThan(Id isarId, {bool include = false}) { + QueryBuilder idLessThan(Id id, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( - IdWhereClause.lessThan(upper: isarId, includeUpper: include), + IdWhereClause.lessThan(upper: id, includeUpper: include), ); }); } - QueryBuilder isarIdBetween( - Id lowerIsarId, - Id upperIsarId, { + QueryBuilder idBetween( + Id lowerId, + Id upperId, { bool includeLower = true, bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IdWhereClause.between( - lower: lowerIsarId, + lower: lowerId, includeLower: includeLower, - upper: upperIsarId, + upper: upperId, includeUpper: includeUpper, )); }); @@ -595,184 +583,42 @@ extension NoteQueryFilter on QueryBuilder { }); } - QueryBuilder idIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'id', - )); - }); - } - - QueryBuilder idIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'id', - )); - }); - } - - QueryBuilder idEqualTo( - String? value, { - bool caseSensitive = true, - }) { + QueryBuilder idEqualTo(Id value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'id', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder idGreaterThan( - String? value, { + Id value, { bool include = false, - bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, property: r'id', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder idLessThan( - String? value, { + Id value, { bool include = false, - bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, property: r'id', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder idBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder idStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder idEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'id', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder idContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'id', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder idMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'id', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder idIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: '', - )); - }); - } - - QueryBuilder idIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'id', - value: '', - )); - }); - } - - QueryBuilder isarIdEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'isarId', - value: value, - )); - }); - } - - QueryBuilder isarIdGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'isarId', - value: value, - )); - }); - } - - QueryBuilder isarIdLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'isarId', - value: value, - )); - }); - } - - QueryBuilder isarIdBetween( Id lower, Id upper, { bool includeLower = true, @@ -780,7 +626,7 @@ extension NoteQueryFilter on QueryBuilder { }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'isarId', + property: r'id', lower: lower, includeLower: includeLower, upper: upper, @@ -978,18 +824,6 @@ extension NoteQuerySortBy on QueryBuilder { }); } - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - QueryBuilder sortByPinned() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'pinned', Sort.asc); @@ -1076,18 +910,6 @@ extension NoteQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByIsarId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.asc); - }); - } - - QueryBuilder thenByIsarIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isarId', Sort.desc); - }); - } - QueryBuilder thenByPinned() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'pinned', Sort.asc); @@ -1138,12 +960,6 @@ extension NoteQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctById({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'id', caseSensitive: caseSensitive); - }); - } - QueryBuilder distinctByPinned() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'pinned'); @@ -1158,9 +974,9 @@ extension NoteQueryWhereDistinct on QueryBuilder { } extension NoteQueryProperty on QueryBuilder { - QueryBuilder isarIdProperty() { + QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isarId'); + return query.addPropertyName(r'id'); }); } @@ -1188,12 +1004,6 @@ extension NoteQueryProperty on QueryBuilder { }); } - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - QueryBuilder pinnedProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'pinned'); diff --git a/lib/pages/bin/bin_page.dart b/lib/pages/bin/bin_page.dart index 7e859e43..daabd3c3 100644 --- a/lib/pages/bin/bin_page.dart +++ b/lib/pages/bin/bin_page.dart @@ -1,17 +1,9 @@ +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:localmaterialnotes/common/placeholders/empty_placeholder.dart'; -import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart'; -import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart'; -import 'package:localmaterialnotes/common/widgets/note_tile.dart'; -import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/layout/layout_provider.dart'; -import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/constants/separators.dart'; -import 'package:localmaterialnotes/utils/constants/sizes.dart'; -import 'package:localmaterialnotes/utils/preferences/layout.dart'; -import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/common/actions/select.dart'; +import 'package:localmaterialnotes/common/widgets/notes_list.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; class BinPage extends ConsumerStatefulWidget { const BinPage(); @@ -22,54 +14,31 @@ class BinPage extends ConsumerStatefulWidget { class _BinPageState extends ConsumerState { @override - Widget build(BuildContext context) { - return ref.watch(binProvider).when( - data: (notes) { - if (notes.isEmpty) { - return EmptyPlaceholder.bin(); - } + void initState() { + super.initState(); + + BackButtonInterceptor.add(_interceptor); + } - final layout = ref.watch(layoutStateProvider) ?? Layout.fromPreference(); - final useSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault(); - final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault(); + @override + void dispose() { + BackButtonInterceptor.remove(_interceptor); + + super.dispose(); + } - // Use at least 2 columns for the grid view - final columnsCount = MediaQuery.of(context).size.width ~/ Sizes.custom.gridLayoutColumnWidth; - final crossAxisCount = columnsCount > 2 ? columnsCount : 2; + bool _interceptor(bool stopDefaultButtonEvent, RouteInfo info) { + if (!isSelectionModeNotifier.value) { + return false; + } - return layout == Layout.list - ? ListView.separated( - padding: showTilesBackground ? Paddings.custom.notesWithBackground : Paddings.custom.fab, - itemCount: notes.length, - itemBuilder: (context, index) { - return NoteTile(notes[index]); - }, - separatorBuilder: (BuildContext context, int index) { - return Padding( - padding: showTilesBackground - ? Paddings.custom.notesListViewWithBackgroundSeparation - : EdgeInsetsDirectional.zero, - child: useSeparators ? Separator.divider1indent8.horizontal : null, - ); - }, - ) - : AlignedGridView.count( - padding: Paddings.custom.notesWithBackground, - mainAxisSpacing: Sizes.custom.notesGridViewSpacing, - crossAxisSpacing: Sizes.custom.notesGridViewSpacing, - crossAxisCount: crossAxisCount, - itemCount: notes.length, - itemBuilder: (context, index) { - return NoteTile(notes[index]); - }, - ); - }, - error: (error, stackTrace) { - return const ErrorPlaceholder(); - }, - loading: () { - return const LoadingPlaceholder(); - }, - ); + exitSelectionMode(ref); + + return true; + } + + @override + Widget build(BuildContext context) { + return const NotesList.bin(); } } diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index aa3eef75..a3d0ced7 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -8,11 +8,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart'; import 'package:localmaterialnotes/common/routing/router.dart'; import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:localmaterialnotes/pages/editor/editor_field.dart'; -import 'package:localmaterialnotes/pages/editor/editor_toolbar.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; -import 'package:localmaterialnotes/providers/editor_controller/editor_controller_provider.dart'; +import 'package:localmaterialnotes/pages/editor/widgets/editor_field.dart'; +import 'package:localmaterialnotes/pages/editor/widgets/editor_toolbar.dart'; import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/constants/paddings.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; @@ -31,9 +30,6 @@ class EditorPage extends ConsumerStatefulWidget { class _EditorState extends ConsumerState { final titleController = TextEditingController(); - FleatherController? fleatherController; - - bool fleatherFieldHasFocus = false; @override void initState() { @@ -70,35 +66,35 @@ class _EditorState extends ConsumerState { } void _synchronizeContent(Note note) { - if (fleatherController == null) { + final editorController = fleatherControllerNotifier.value; + + if (editorController == null) { return; } - note.content = jsonEncode(fleatherController!.document.toDelta().toJson()); + fleatherControllerCanUndoNotifier.value = editorController.canUndo; + fleatherControllerCanRedoNotifier.value = editorController.canRedo; + + note.content = jsonEncode(editorController.document.toDelta().toJson()); ref.read(notesProvider.notifier).edit(note); } @override Widget build(BuildContext context) { - final note = ref.watch(currentNoteProvider); + final note = currentNoteNotifier.value; if (note == null) { return const LoadingPlaceholder(); } - final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); + final editorController = fleatherControllerNotifier.value ??= FleatherController( + document: note.document, + )..addListener(() => _synchronizeContent(note)); titleController.text = note.title; - if (fleatherController == null) { - fleatherController = FleatherController(document: note.document); - fleatherController!.addListener(() => _synchronizeContent(note)); - - Future(() { - ref.read(editorControllerProvider.notifier).set(fleatherController!); - }); - } + final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); return Column( children: [ @@ -123,7 +119,7 @@ class _EditorState extends ConsumerState { child: Focus( onFocusChange: (hasFocus) => fleatherFieldHasFocusNotifier.value = hasFocus, child: EditorField( - fleatherController: fleatherController!, + fleatherController: editorController, readOnly: widget._readOnly, autofocus: widget._autofocus, ), @@ -135,11 +131,11 @@ class _EditorState extends ConsumerState { ), ValueListenableBuilder( valueListenable: fleatherFieldHasFocusNotifier, - builder: (_, hasFocus, ___) { + builder: (context, hasFocus, child) { return showToolbar && hasFocus && KeyboardVisibilityProvider.isKeyboardVisible(context) ? ColoredBox( color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: EditorToolbar(fleatherController!), + child: EditorToolbar(editorController), ) : Container(); }, diff --git a/lib/pages/editor/editor_toolbar.dart b/lib/pages/editor/editor_toolbar.dart deleted file mode 100644 index 812a96ea..00000000 --- a/lib/pages/editor/editor_toolbar.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:fleather/fleather.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/constants/sizes.dart'; -import 'package:material_symbols_icons/material_symbols_icons.dart'; - -class EditorToolbar extends ConsumerStatefulWidget { - const EditorToolbar(this.fleatherController); - - final FleatherController fleatherController; - - @override - ConsumerState createState() => _EditorToolbarState(); -} - -class _EditorToolbarState extends ConsumerState { - Widget _buttonBuilder( - BuildContext context, - ParchmentAttribute attribute, - IconData icon, - bool isToggled, - VoidCallback? onPressed, - ) { - return isToggled - ? IconButton.filled( - visualDensity: VisualDensity.compact, - icon: Icon(icon), - onPressed: onPressed, - ) - : IconButton( - visualDensity: VisualDensity.compact, - icon: Icon(icon), - onPressed: onPressed, - ); - } - - void _insertRule() { - final controller = widget.fleatherController; - - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - final newSelection = controller.selection.copyWith( - baseOffset: index + 2, - extentOffset: index + 2, - ); - - controller.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection); - } - - @override - Widget build(BuildContext context) { - final buttons = [ - ToggleStyleButton( - attribute: ParchmentAttribute.bold, - icon: Icons.format_bold, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.italic, - icon: Icons.format_italic, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.strikethrough, - icon: Icons.format_strikethrough, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.block.bulletList, - icon: Icons.format_list_bulleted, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.block.numberList, - icon: Icons.format_list_numbered, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.inlineCode, - icon: Icons.code, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.block.code, - icon: Symbols.code_blocks, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - ToggleStyleButton( - attribute: ParchmentAttribute.block.quote, - icon: Icons.format_quote, - controller: widget.fleatherController, - childBuilder: _buttonBuilder, - ), - /* TODO Add the link button - * Right now it requires to copy too much code - * (cf. https://github.com/fleather-editor/fleather/issues/353) - IconButton( - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.link), - onPressed: () => {}, - ), - */ - IconButton( - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.horizontal_rule), - onPressed: _insertRule, - ), - ]; - - return SizedBox( - height: Sizes.custom.editorToolbarHeight, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: Paddings.padding4.vertical.add(Paddings.padding4.horizontal), - itemCount: buttons.length, - itemBuilder: (BuildContext context, int index) { - return buttons[index]; - }, - separatorBuilder: (context, index) { - return Padding(padding: Paddings.padding2.horizontal); - }, - ), - ); - } -} diff --git a/lib/pages/editor/about_sheet.dart b/lib/pages/editor/sheets/about_sheet.dart similarity index 89% rename from lib/pages/editor/about_sheet.dart rename to lib/pages/editor/sheets/about_sheet.dart index 1d6b749c..957c9450 100644 --- a/lib/pages/editor/about_sheet.dart +++ b/lib/pages/editor/sheets/about_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart'; -import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/extensions/date_time_extensions.dart'; @@ -10,7 +10,7 @@ class AboutSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final note = ref.watch(currentNoteProvider); + final note = currentNoteNotifier.value; if (note == null) { return const ErrorPlaceholder(); diff --git a/lib/pages/editor/editor_field.dart b/lib/pages/editor/widgets/editor_field.dart similarity index 63% rename from lib/pages/editor/editor_field.dart rename to lib/pages/editor/widgets/editor_field.dart index 1740def4..4d824445 100644 --- a/lib/pages/editor/editor_field.dart +++ b/lib/pages/editor/widgets/editor_field.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:url_launcher/url_launcher_string.dart'; class EditorField extends StatelessWidget { @@ -27,7 +28,7 @@ class EditorField extends StatelessWidget { @override Widget build(BuildContext context) { - return FleatherField( + final fleatherField = FleatherField( controller: fleatherController, autofocus: autofocus, readOnly: readOnly, @@ -41,5 +42,22 @@ class EditorField extends StatelessWidget { ), padding: Paddings.custom.bottomSystemUi, ); + + // If paragraph spacing should be used, return the editor directly without modifying its theme + if (PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault()) { + return fleatherField; + } + + final fleatherThemeFallback = FleatherThemeData.fallback(context); + + return FleatherTheme( + data: fleatherThemeFallback.copyWith( + paragraph: TextBlockTheme( + style: fleatherThemeFallback.paragraph.style, + spacing: const VerticalSpacing.zero(), + ), + ), + child: fleatherField, + ); } } diff --git a/lib/pages/editor/widgets/editor_toolbar.dart b/lib/pages/editor/widgets/editor_toolbar.dart new file mode 100644 index 00000000..cb7cef7c --- /dev/null +++ b/lib/pages/editor/widgets/editor_toolbar.dart @@ -0,0 +1,132 @@ +import 'package:fleather/fleather.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/constants/sizes.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; + +class EditorToolbar extends ConsumerStatefulWidget { + const EditorToolbar(this.fleatherController); + + final FleatherController fleatherController; + + @override + ConsumerState createState() => _EditorToolbarState(); +} + +class _EditorToolbarState extends ConsumerState { + Widget _buttonBuilder( + BuildContext context, + ParchmentAttribute attribute, + IconData icon, + bool isToggled, + VoidCallback? onPressed, + ) { + return Padding( + padding: Paddings.padding2.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: Sizes.custom.editorToolbarButtonHeight, + height: Sizes.custom.editorToolbarButtonWidth, + ), + child: RawMaterialButton( + shape: const CircleBorder(), + visualDensity: VisualDensity.compact, + fillColor: isToggled ? Theme.of(context).colorScheme.secondary : null, + elevation: 0, + onPressed: onPressed, + child: Icon(icon), + ), + ), + ); + } + + void _insertRule() { + final index = widget.fleatherController.selection.baseOffset; + final length = widget.fleatherController.selection.extentOffset - index; + final newSelection = widget.fleatherController.selection.copyWith( + baseOffset: index + 2, + extentOffset: index + 2, + ); + + widget.fleatherController.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection); + } + + @override + Widget build(BuildContext context) { + return FleatherToolbar( + padding: EdgeInsets.zero, + children: [ + Padding(padding: Paddings.padding2.horizontal), + ToggleStyleButton( + attribute: ParchmentAttribute.bold, + icon: Icons.format_bold, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.italic, + icon: Icons.format_italic, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.underline, + icon: Icons.format_underline, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.strikethrough, + icon: Icons.format_strikethrough, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + if (!PreferenceKey.showChecklistButton.getPreferenceOrDefault()) + ToggleStyleButton( + attribute: ParchmentAttribute.block.checkList, + icon: Icons.checklist, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.block.bulletList, + icon: Icons.format_list_bulleted, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.block.numberList, + icon: Icons.format_list_numbered, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.inlineCode, + icon: Icons.code, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.block.code, + icon: Symbols.code_blocks, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + ToggleStyleButton( + attribute: ParchmentAttribute.block.quote, + icon: Icons.format_quote, + controller: widget.fleatherController, + childBuilder: _buttonBuilder, + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.horizontal_rule), + onPressed: _insertRule, + ), + Padding(padding: Paddings.padding2.horizontal), + ], + ); + } +} diff --git a/lib/pages/notes/notes_page.dart b/lib/pages/notes/notes_page.dart index 42c98162..c64d814f 100644 --- a/lib/pages/notes/notes_page.dart +++ b/lib/pages/notes/notes_page.dart @@ -1,17 +1,9 @@ +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:localmaterialnotes/common/placeholders/empty_placeholder.dart'; -import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart'; -import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart'; -import 'package:localmaterialnotes/common/widgets/note_tile.dart'; -import 'package:localmaterialnotes/providers/layout/layout_provider.dart'; -import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/constants/separators.dart'; -import 'package:localmaterialnotes/utils/constants/sizes.dart'; -import 'package:localmaterialnotes/utils/preferences/layout.dart'; -import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/common/actions/select.dart'; +import 'package:localmaterialnotes/common/widgets/notes_list.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; class NotesPage extends ConsumerStatefulWidget { const NotesPage(); @@ -22,54 +14,31 @@ class NotesPage extends ConsumerStatefulWidget { class _NotesPageState extends ConsumerState { @override - Widget build(BuildContext context) { - return ref.watch(notesProvider).when( - data: (notes) { - if (notes.isEmpty) { - return EmptyPlaceholder.notes(); - } + void initState() { + super.initState(); + + BackButtonInterceptor.add(_interceptor); + } - final layout = ref.watch(layoutStateProvider) ?? Layout.fromPreference(); - final useSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault(); - final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault(); + @override + void dispose() { + BackButtonInterceptor.remove(_interceptor); + + super.dispose(); + } - // Use at least 2 columns for the grid view - final columnsCount = MediaQuery.of(context).size.width ~/ Sizes.custom.gridLayoutColumnWidth; - final crossAxisCount = columnsCount > 2 ? columnsCount : 2; + bool _interceptor(bool stopDefaultButtonEvent, RouteInfo info) { + if (!isSelectionModeNotifier.value) { + return false; + } - return layout == Layout.list - ? ListView.separated( - padding: showTilesBackground ? Paddings.custom.notesWithBackground : Paddings.custom.fab, - itemCount: notes.length, - itemBuilder: (context, index) { - return NoteTile(notes[index]); - }, - separatorBuilder: (BuildContext context, int index) { - return Padding( - padding: showTilesBackground - ? Paddings.custom.notesListViewWithBackgroundSeparation - : EdgeInsetsDirectional.zero, - child: useSeparators ? Separator.divider1indent8.horizontal : null, - ); - }, - ) - : AlignedGridView.count( - padding: Paddings.custom.notesWithBackground, - mainAxisSpacing: Sizes.custom.notesGridViewSpacing, - crossAxisSpacing: Sizes.custom.notesGridViewSpacing, - crossAxisCount: crossAxisCount, - itemCount: notes.length, - itemBuilder: (context, index) { - return NoteTile(notes[index]); - }, - ); - }, - error: (error, stackTrace) { - return const ErrorPlaceholder(); - }, - loading: () { - return const LoadingPlaceholder(); - }, - ); + exitSelectionMode(ref); + + return true; + } + + @override + Widget build(BuildContext context) { + return const NotesList.notes(); } } diff --git a/lib/pages/settings/dialogs/auto_export_dialog.dart b/lib/pages/settings/dialogs/auto_export_dialog.dart new file mode 100644 index 00000000..48635119 --- /dev/null +++ b/lib/pages/settings/dialogs/auto_export_dialog.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/common/widgets/encrypt_password_form.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; + +class AutoExportDialog extends StatefulWidget { + const AutoExportDialog({super.key}); + + @override + State createState() => _AutoExportDialogState(); +} + +class _AutoExportDialogState extends State { + bool _encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault(); + String? _password; + + late bool ok; + late int _frequencyIndex; + + final List _frequencyValues = [0.0, 1.0, 3.0, 7.0, 14.0, 30.0]; + + @override + void initState() { + super.initState(); + + _frequencyIndex = _frequencyValues.indexOf(PreferenceKey.autoExportFrequency.getPreferenceOrDefault()); + if (_frequencyIndex == -1) { + // Make sure that the index is not set to -1 in case the frequency isn't in the allowed values + _frequencyIndex = 0; + } + + _updateOk(); + } + + double get _frequencyValue { + return _frequencyValues[_frequencyIndex]; + } + + void _updateOk() { + ok = _frequencyValue == 0.0 || !_encrypt || (_encrypt && (_password?.isStrongPassword ?? false)); + } + + void _onFrequencyChanged(double value) { + setState(() { + _frequencyIndex = value.toInt(); + _updateOk(); + }); + } + + void _onChanged(bool encrypt, String? password) { + setState(() { + _encrypt = encrypt; + _password = password; + _updateOk(); + }); + } + + void _pop({bool cancel = false}) { + if (cancel) { + Navigator.pop(context); + + return; + } + + Navigator.pop(context, (_frequencyValue, _encrypt, _password)); + } + + @override + Widget build(BuildContext context) { + return AlertDialog.adaptive( + title: Text(localizations.settings_auto_export), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _frequencyValue == 0.0 + ? localizations.settings_auto_export_dialog_description_disabled + : localizations.settings_auto_export_dialog_description_enabled(_frequencyValue.toInt().toString()), + ), + Padding(padding: Paddings.padding8.vertical), + Slider( + value: _frequencyIndex.toDouble(), + max: _frequencyValues.length - 1, + divisions: _frequencyValues.length - 1, + label: _frequencyValue == 0.0 + ? localizations.settings_auto_export_disabled + : localizations.settings_auto_export_dialog_slider_label(_frequencyValue.toInt().toString()), + onChanged: _onFrequencyChanged, + ), + if (_frequencyValue != 0.0) ...[ + Padding(padding: Paddings.padding8.vertical), + EncryptionPasswordForm( + secondaryDescription: localizations.dialog_export_encryption_secondary_description_auto, + onChanged: _onChanged, + onEditingComplete: _pop, + ), + ], + ], + ), + ), + actions: [ + TextButton( + onPressed: () => _pop(cancel: true), + child: Text(localizations.button_cancel), + ), + TextButton( + onPressed: ok ? _pop : null, + child: Text(localizations.button_ok), + ), + ], + ); + } +} diff --git a/lib/pages/settings/dialogs/import_dialog.dart b/lib/pages/settings/dialogs/import_dialog.dart new file mode 100644 index 00000000..8b708913 --- /dev/null +++ b/lib/pages/settings/dialogs/import_dialog.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/common/widgets/password_field.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; + +class ImportDialog extends StatefulWidget { + const ImportDialog({ + super.key, + required this.title, + }); + + final String title; + + @override + State createState() => _ImportDialogState(); +} + +class _ImportDialogState extends State { + String? _password; + + late bool ok; + + @override + void initState() { + super.initState(); + + _updateOk(); + } + + void _updateOk() { + ok = _password?.isStrongPassword ?? false; + } + + void _onChanged(String? password) { + setState(() { + _password = password; + _updateOk(); + }); + } + + void _pop({bool cancel = false}) { + if (cancel) { + Navigator.pop(context); + + return; + } + + Navigator.pop(context, _password); + } + + @override + Widget build(BuildContext context) { + return AlertDialog.adaptive( + title: Text(widget.title), + content: SingleChildScrollView( + child: PasswordField( + description: localizations.dialog_import_encryption_password_description, + secondaryDescription: localizations.dialog_export_encryption_description, + onChanged: _onChanged, + onEditingComplete: _pop, + ), + ), + actions: [ + TextButton( + onPressed: () => _pop(cancel: true), + child: Text(localizations.button_cancel), + ), + TextButton( + onPressed: ok ? _pop : null, + child: Text(localizations.button_ok), + ), + ], + ); + } +} diff --git a/lib/pages/settings/dialogs/manual_export_dialog.dart b/lib/pages/settings/dialogs/manual_export_dialog.dart new file mode 100644 index 00000000..84023eb2 --- /dev/null +++ b/lib/pages/settings/dialogs/manual_export_dialog.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/common/widgets/encrypt_password_form.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; + +class ManualExportDialog extends StatefulWidget { + const ManualExportDialog({ + super.key, + }); + + @override + State createState() => _ManualExportDialogState(); +} + +class _ManualExportDialogState extends State { + bool _encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault(); + String? _password; + + late bool ok; + + @override + void initState() { + super.initState(); + + _updateOk(); + } + + void _updateOk() { + ok = !_encrypt || (_encrypt && (_password?.isStrongPassword ?? false)); + } + + void _onChanged(bool encrypt, String? password) { + setState(() { + _encrypt = encrypt; + _password = password; + _updateOk(); + }); + } + + void _pop({bool cancel = false}) { + if (cancel) { + Navigator.pop(context); + + return; + } + + Navigator.pop(context, (_encrypt, _password)); + } + + @override + Widget build(BuildContext context) { + return AlertDialog.adaptive( + title: Text(localizations.settings_export_json), + content: SingleChildScrollView( + child: EncryptionPasswordForm( + secondaryDescription: localizations.dialog_export_encryption_secondary_description_manual, + onChanged: _onChanged, + onEditingComplete: _pop, + ), + ), + actions: [ + TextButton( + onPressed: () => _pop(cancel: true), + child: Text(localizations.button_cancel), + ), + TextButton( + onPressed: ok ? _pop : null, + child: Text(localizations.button_ok), + ), + ], + ); + } +} diff --git a/lib/pages/settings/enums/settings_page.dart b/lib/pages/settings/enums/settings_page.dart new file mode 100644 index 00000000..ebc764f2 --- /dev/null +++ b/lib/pages/settings/enums/settings_page.dart @@ -0,0 +1,7 @@ +enum SettingsPage { + appearance, + behavior, + editor, + backup, + about, +} diff --git a/lib/pages/settings/pages/settings_about_page.dart b/lib/pages/settings/pages/settings_about_page.dart new file mode 100644 index 00000000..dff097a3 --- /dev/null +++ b/lib/pages/settings/pages/settings_about_page.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/utils/asset.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; +import 'package:localmaterialnotes/utils/constants/sizes.dart'; +import 'package:localmaterialnotes/utils/info_utils.dart'; +import 'package:simple_icons/simple_icons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Settings providing information about the application. +class SettingsAboutPage extends StatelessWidget { + const SettingsAboutPage({super.key}); + + String? _encodeQueryParameters(Map params) { + return params.entries.map((MapEntry e) { + return '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}'; + }).join('&'); + } + + /// Shows the about dialog. + Future _showAbout(BuildContext context) async { + showAboutDialog( + context: context, + useRootNavigator: false, + applicationName: localizations.app_name, + applicationVersion: InfoUtils().appVersion, + applicationIcon: Image.asset( + Asset.icon.path, + fit: BoxFit.fitWidth, + width: Sizes.size64.size, + ), + applicationLegalese: localizations.settings_licence_description, + children: [ + Padding(padding: Paddings.padding16.vertical), + Text( + localizations.app_tagline, + style: Theme.of(context).textTheme.titleSmall, + ), + Padding(padding: Paddings.padding8.vertical), + Text(localizations.app_about(localizations.app_name)), + ], + ); + } + + /// Opens the application's GitHub issues. + void _openGitHubIssues(_) { + launchUrl( + Uri( + scheme: 'https', + host: 'github.com', + path: 'maelchiotti/LocalMaterialNotes/issues', + ), + ); + } + + /// Opens the application's GitHub discussions. + void _openGitHubDiscussions(_) { + launchUrl( + Uri( + scheme: 'https', + host: 'github.com', + path: 'maelchiotti/LocalMaterialNotes/discussions', + ), + ); + } + + /// Sends an email to `contact@maelchiotti.dev` with some basic information. + void _sendMail(_) { + final appVersion = InfoUtils().appVersion; + final buildMode = InfoUtils().buildMode; + final androidVersion = InfoUtils().androidVersion; + final brand = InfoUtils().brand; + final model = InfoUtils().model; + + launchUrl( + Uri( + scheme: 'mailto', + path: 'contact@maelchiotti.dev', + query: _encodeQueryParameters({ + 'subject': '[Material Notes] ', + 'body': '\n\n\n----------\nv$appVersion\n$buildMode mode\nAndroid $androidVersion\n$brand $model', + }), + ), + ); + } + + /// Opens the application's GitHub repository. + void _openGitHub(_) { + launchUrl( + Uri( + scheme: 'https', + host: 'github.com', + path: 'maelchiotti/LocalMaterialNotes', + ), + ); + } + + /// Opens the application's license file. + void _openLicense(_) { + launchUrl( + Uri( + scheme: 'https', + host: 'github.com', + path: 'maelchiotti/LocalMaterialNotes/blob/main/LICENSE', + ), + ); + } + + @override + Widget build(BuildContext context) { + final appVersion = InfoUtils().appVersion; + + return CustomSettingsList( + sections: [ + SettingsSection( + title: Text(localizations.settings_about_application), + tiles: [ + SettingsTile( + leading: const Icon(Icons.info), + title: Text(localizations.app_name), + value: Text('v$appVersion'), + onPressed: _showAbout, + ), + SettingsTile( + leading: const Icon(Icons.build), + title: Text(localizations.settings_build_mode), + value: Text(InfoUtils().buildMode), + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_about_help), + tiles: [ + SettingsTile( + leading: const Icon(Icons.bug_report), + title: Text(localizations.settings_github_issues), + value: Text(localizations.settings_github_issues_description), + onPressed: _openGitHubIssues, + ), + SettingsTile( + leading: const Icon(Icons.forum), + title: Text(localizations.settings_github_discussions), + value: Text(localizations.settings_github_discussions_description), + onPressed: _openGitHubDiscussions, + ), + SettingsTile( + leading: const Icon(Icons.mail), + title: Text(localizations.settings_get_in_touch), + value: Text(localizations.settings_get_in_touch_description), + onPressed: _sendMail, + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_about_links), + tiles: [ + SettingsTile( + leading: const Icon(SimpleIcons.github), + title: Text(localizations.settings_github), + value: Text(localizations.settings_github_description), + onPressed: _openGitHub, + ), + SettingsTile( + leading: const Icon(Icons.balance), + title: Text(localizations.settings_licence), + value: Text(localizations.settings_licence_description), + onPressed: _openLicense, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/pages/settings_appearance_page.dart b/lib/pages/settings/pages/settings_appearance_page.dart new file mode 100644 index 00000000..56a0acc2 --- /dev/null +++ b/lib/pages/settings/pages/settings_appearance_page.dart @@ -0,0 +1,205 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:locale_names/locale_names.dart'; +import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart'; +import 'package:localmaterialnotes/l10n/localization_completion.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; +import 'package:localmaterialnotes/utils/locale_utils.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; +import 'package:localmaterialnotes/utils/theme_utils.dart'; +import 'package:restart_app/restart_app.dart'; + +/// Settings related to the appearance of the application. +class SettingsAppearancePage extends StatefulWidget { + const SettingsAppearancePage({super.key}); + + @override + State createState() => _SettingsAppearancePageState(); +} + +class _SettingsAppearancePageState extends State { + /// Asks the user to select the language of the application. + /// + /// Restarts the application if the language is changed. + Future _selectLanguage(BuildContext context) async { + await showAdaptiveDialog( + context: context, + builder: (context) { + return SimpleDialog( + clipBehavior: Clip.hardEdge, + title: Text(localizations.settings_language), + children: AppLocalizations.supportedLocales.map((locale) { + return RadioListTile( + value: locale, + groupValue: Localizations.localeOf(context), + title: Text(locale.nativeDisplayLanguage.capitalized), + subtitle: Text(LocalizationCompletion.getFormattedPercentage(locale)), + selected: Localizations.localeOf(context) == locale, + onChanged: (locale) => Navigator.of(context).pop(locale), + ); + }).toList(), + ); + }, + ).then((locale) async { + if (locale == null) { + return; + } + + LocaleUtils().setLocale(locale); + + // The Restart package crashes the app if used in debug mode + if (!kDebugMode) { + await Restart.restartApp(); + } + }); + } + + /// Asks the user to select the theme of the application. + Future _selectTheme(BuildContext context) async { + await showAdaptiveDialog( + context: context, + builder: (context) { + return SimpleDialog( + clipBehavior: Clip.hardEdge, + title: Text(localizations.settings_theme), + children: [ + RadioListTile( + value: ThemeMode.system, + groupValue: ThemeUtils().themeMode, + title: Text(localizations.settings_theme_system), + selected: ThemeUtils().themeMode == ThemeMode.system, + onChanged: (themeMode) => Navigator.of(context).pop(themeMode), + ), + RadioListTile( + value: ThemeMode.light, + groupValue: ThemeUtils().themeMode, + title: Text(localizations.settings_theme_light), + selected: ThemeUtils().themeMode == ThemeMode.light, + onChanged: (themeMode) => Navigator.of(context).pop(themeMode), + ), + RadioListTile( + value: ThemeMode.dark, + groupValue: ThemeUtils().themeMode, + title: Text(localizations.settings_theme_dark), + selected: ThemeUtils().themeMode == ThemeMode.dark, + onChanged: (themeMode) => Navigator.of(context).pop(themeMode), + ), + ], + ); + }, + ).then((themeMode) { + if (themeMode == null) { + return; + } + + ThemeUtils().setThemeMode(themeMode); + }); + } + + /// Toggles the dynamic theming. + void _toggleDynamicTheming(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.dynamicTheming.name, toggled); + }); + + dynamicThemingNotifier.value = toggled; + } + + /// Toggles the black theming. + void _toggleBlackTheming(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.blackTheming.name, toggled); + }); + + blackThemingNotifier.value = toggled; + } + + /// Toggles the setting to show background of the notes tiles. + void _toggleShowTilesBackground(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.showTilesBackground.name, toggled); + }); + + showTilesBackgroundNotifier.value = toggled; + } + + /// Toggles the setting to show the separators between the notes tiles. + void _toggleShowSeparators(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.showSeparators.name, toggled); + }); + + showSeparatorsNotifier.value = toggled; + } + + @override + Widget build(BuildContext context) { + final locale = LocaleUtils().appLocale.nativeDisplayLanguage.capitalized; + final showUseBlackTheming = Theme.of(context).colorScheme.brightness == Brightness.dark; + + final showSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault(); + final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault(); + + return CustomSettingsList( + sections: [ + SettingsSection( + title: Text(localizations.settings_appearance_application), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.language), + title: Text(localizations.settings_language), + value: Text(locale), + onPressed: _selectLanguage, + ), + SettingsTile.navigation( + leading: const Icon(Icons.palette), + title: Text(localizations.settings_theme), + value: Text(ThemeUtils().themeModeName), + onPressed: _selectTheme, + ), + SettingsTile.switchTile( + enabled: ThemeUtils().isDynamicThemingAvailable, + leading: const Icon(Icons.bolt), + title: Text(localizations.settings_dynamic_theming), + description: Text(localizations.settings_dynamic_theming_description), + initialValue: ThemeUtils().useDynamicTheming, + onToggle: _toggleDynamicTheming, + ), + SettingsTile.switchTile( + enabled: showUseBlackTheming, + leading: const Icon(Icons.nightlight), + title: Text(localizations.settings_black_theming), + description: Text(localizations.settings_black_theming_description), + initialValue: ThemeUtils().useBlackTheming, + onToggle: _toggleBlackTheming, + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_appearance_notes_tiles), + tiles: [ + SettingsTile.switchTile( + leading: const Icon(Icons.safety_divider), + title: Text(localizations.settings_show_separators), + description: Text(localizations.settings_show_separators_description), + initialValue: showSeparators, + onToggle: _toggleShowSeparators, + ), + SettingsTile.switchTile( + leading: const Icon(Icons.gradient), + title: Text(localizations.settings_show_tiles_background), + description: Text(localizations.settings_show_tiles_background_description), + initialValue: showTilesBackground, + onToggle: _toggleShowTilesBackground, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/pages/settings_backup_page.dart b/lib/pages/settings/pages/settings_backup_page.dart new file mode 100644 index 00000000..79bd5d26 --- /dev/null +++ b/lib/pages/settings/pages/settings_backup_page.dart @@ -0,0 +1,187 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:localmaterialnotes/pages/settings/dialogs/auto_export_dialog.dart'; +import 'package:localmaterialnotes/pages/settings/dialogs/manual_export_dialog.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; +import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; +import 'package:localmaterialnotes/utils/auto_export_utils.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/database_utils.dart'; +import 'package:localmaterialnotes/utils/extensions/uri_extension.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; +import 'package:localmaterialnotes/utils/snack_bar_utils.dart'; +import 'package:simple_icons/simple_icons.dart'; + +/// Settings related to backup of the notes. +class SettingsBackupPage extends ConsumerStatefulWidget { + const SettingsBackupPage({super.key}); + + @override + ConsumerState createState() => _SettingsBackupPageState(); +} + +class _SettingsBackupPageState extends ConsumerState { + /// Asks the user to configure the auto export as JSON. + Future autoExportAsJson(BuildContext context) async { + await showAdaptiveDialog<(double, bool, String?)>( + context: context, + builder: (context) => const AutoExportDialog(), + ).then((autoExportSettings) async { + if (autoExportSettings == null) { + return; + } + + final frequency = autoExportSettings.$1; + PreferencesUtils().set(PreferenceKey.autoExportFrequency.name, frequency); + + // If the auto export was disabled, just remove the encryption, last export date and password settings + if (frequency == 0.0) { + await PreferencesUtils().remove(PreferenceKey.autoExportEncryption); + await PreferencesUtils().remove(PreferenceKey.lastAutoExportDate); + await PreferencesUtils().deleteSecure(PreferenceKey.autoExportPassword); + + return; + } + + final encrypt = autoExportSettings.$2; + PreferencesUtils().set(PreferenceKey.autoExportEncryption.name, encrypt); + + // If the encryption was enabled, set the password. If not, make sure to delete it + // (even though it might not have been set previously) + if (encrypt) { + final password = autoExportSettings.$3!; + PreferencesUtils().setSecure(PreferenceKey.autoExportPassword, password); + } else { + await PreferencesUtils().deleteSecure(PreferenceKey.autoExportPassword); + } + + setState(() {}); + + // No need to await this, it can be performed in the background + AutoExportUtils().performAutoExportIfNeeded(); + }); + } + + /// Asks the user to configure the immediate export as JSON. + Future exportAsJson(BuildContext context) async { + await showAdaptiveDialog<(bool, String?)?>( + context: context, + builder: (context) => const ManualExportDialog(), + ).then((shouldEncrypt) async { + if (shouldEncrypt == null) { + return; + } + + final encrypt = shouldEncrypt.$1; + + try { + final password = shouldEncrypt.$2; + + if (await DatabaseUtils().manuallyExportAsJson(encrypt, password)) { + SnackBarUtils.info(localizations.settings_export_success).show(); + } + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + SnackBarUtils.info(exception.toString()).show(); + } + }); + } + + /// Asks the user to configure the immediate export as Markdown. + Future exportAsMarkdown(BuildContext context) async { + try { + if (await DatabaseUtils().exportAsMarkdown()) { + SnackBarUtils.info(localizations.settings_export_success).show(); + } + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + SnackBarUtils.info(exception.toString()).show(); + } + } + + /// Asks the user to choose a JSON file to import in the application. + Future import(BuildContext context) async { + try { + final imported = await DatabaseUtils().import(context); + + if (imported) { + await ref.read(notesProvider.notifier).get(); + await ref.read(binProvider.notifier).get(); + + SnackBarUtils.info(localizations.settings_import_success).show(); + } + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + SnackBarUtils.info(exception.toString()).show(); + } + } + + @override + Widget build(BuildContext context) { + final autoExportFrequency = PreferenceKey.autoExportFrequency.getPreferenceOrDefault(); + final autoExportEncryption = PreferenceKey.autoExportEncryption.getPreferenceOrDefault(); + final autoExportDirectory = AutoExportUtils().backupsDirectory.toDecodedString; + + return CustomSettingsList( + sections: [ + SettingsSection( + title: Text(localizations.settings_backup_export), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.settings_backup_restore), + title: Text(localizations.settings_auto_export), + value: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + autoExportFrequency == 0 + ? localizations.settings_auto_export_disabled + : localizations.settings_auto_export_value( + autoExportEncryption.toString(), + autoExportFrequency.toInt().toString(), + ), + style: Theme.of(context).textTheme.titleSmall, + ), + Text(localizations.settings_auto_export_description), + Text(localizations.settings_auto_export_directory(autoExportDirectory)), + ], + ), + onPressed: autoExportAsJson, + ), + SettingsTile.navigation( + leading: const Icon(SimpleIcons.json), + title: Text(localizations.settings_export_json), + value: Text(localizations.settings_export_json_description), + onPressed: exportAsJson, + ), + SettingsTile.navigation( + leading: const Icon(SimpleIcons.markdown), + title: Text(localizations.settings_export_markdown), + value: Text(localizations.settings_export_markdown_description), + onPressed: exportAsMarkdown, + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_backup_import), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.file_upload), + title: Text(localizations.settings_import), + value: Text(localizations.settings_import_description), + onPressed: import, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/pages/settings_behavior_page.dart b/lib/pages/settings/pages/settings_behavior_page.dart new file mode 100644 index 00000000..df890c0f --- /dev/null +++ b/lib/pages/settings/pages/settings_behavior_page.dart @@ -0,0 +1,183 @@ +import 'package:flag_secure/flag_secure.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/confirmations.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_direction.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; + +/// Settings related to the behavior of the application. +class SettingsBehaviorPage extends StatefulWidget { + const SettingsBehaviorPage({super.key}); + + @override + State createState() => _SettingsBehaviorPageState(); +} + +class _SettingsBehaviorPageState extends State { + /// Asks the user to choose which confirmations should be shown. + Future _selectConfirmations(BuildContext context) async { + final confirmationsPreference = Confirmations.fromPreference(); + + await showAdaptiveDialog( + context: context, + builder: (context) { + return SimpleDialog( + clipBehavior: Clip.hardEdge, + title: Text(localizations.settings_confirmations), + children: Confirmations.values.map((confirmationsValue) { + return RadioListTile( + value: confirmationsValue, + groupValue: confirmationsPreference, + title: Text(confirmationsValue.title), + selected: confirmationsPreference == confirmationsValue, + onChanged: (confirmations) => Navigator.of(context).pop(confirmations), + ); + }).toList(), + ); + }, + ).then((confirmations) { + if (confirmations == null) { + return; + } + + setState(() { + PreferencesUtils().set(PreferenceKey.confirmations.name, confirmations.name); + }); + }); + } + + /// Asks the user to choose which action should be triggered when swiping the notes tiles in the [swipeDirection]. + Future _selectSwipeAction(BuildContext context, SwipeDirection swipeDirection) async { + SwipeAction swipeActionPreference; + switch (swipeDirection) { + case SwipeDirection.right: + swipeActionPreference = swipeActionsNotifier.value.$1; + case SwipeDirection.left: + swipeActionPreference = swipeActionsNotifier.value.$2; + } + + await showAdaptiveDialog( + context: context, + builder: (context) { + return SimpleDialog( + clipBehavior: Clip.hardEdge, + title: Text(localizations.settings_confirmations), + children: SwipeAction.values.map((swipeAction) { + return RadioListTile( + value: swipeAction, + groupValue: swipeActionPreference, + title: Text(swipeAction.title), + selected: swipeActionPreference == swipeAction, + onChanged: (swipeAction) => Navigator.of(context).pop(swipeAction), + ); + }).toList(), + ); + }, + ).then((swipeAction) { + if (swipeAction == null) { + return; + } + + setState(() { + switch (swipeDirection) { + case SwipeDirection.right: + PreferencesUtils().set(PreferenceKey.swipeRightAction.name, swipeAction.name); + swipeActionsNotifier.value = (swipeAction, swipeActionsNotifier.value.$2); + case SwipeDirection.left: + PreferencesUtils().set(PreferenceKey.swipeLeftAction.name, swipeAction.name); + swipeActionsNotifier.value = (swipeActionsNotifier.value.$1, swipeAction); + } + }); + }); + } + + /// Toggles Android's `FLAG_SECURE` to hide the app from the recent apps and prevent screenshots. + Future _setFlagSecure(bool toggled) async { + setState(() { + PreferencesUtils().set(PreferenceKey.flagSecure.name, toggled); + }); + + toggled ? await FlagSecure.set() : await FlagSecure.unset(); + } + + @override + Widget build(BuildContext context) { + final confirmations = Confirmations.fromPreference(); + final flagSecure = PreferenceKey.flagSecure.getPreferenceOrDefault(); + + final swipeRightAction = swipeActionsNotifier.value.$1; + final swipeLeftAction = swipeActionsNotifier.value.$2; + + return CustomSettingsList( + sections: [ + SettingsSection( + title: Text(localizations.settings_behavior_application), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.warning), + title: Text(localizations.settings_confirmations), + value: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + confirmations.title, + style: Theme.of(context).textTheme.titleSmall, + ), + Text(localizations.settings_confirmations_description), + ], + ), + onPressed: _selectConfirmations, + ), + SettingsTile.switchTile( + leading: const Icon(Icons.security), + title: Text(localizations.settings_flag_secure), + description: Text(localizations.settings_flag_secure_description), + initialValue: flagSecure, + onToggle: _setFlagSecure, + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_behavior_swipe_actions), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.swipe_right), + title: Text(localizations.settings_swipe_action_right), + value: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + swipeRightAction.title, + style: Theme.of(context).textTheme.titleSmall, + ), + Text(localizations.settings_swipe_action_right_description), + ], + ), + onPressed: (context) => _selectSwipeAction(context, SwipeDirection.right), + ), + SettingsTile.navigation( + leading: const Icon(Icons.swipe_left), + title: Text(localizations.settings_swipe_action_left), + value: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + swipeLeftAction.title, + style: Theme.of(context).textTheme.titleSmall, + ), + Text(localizations.settings_swipe_action_left_description), + ], + ), + onPressed: (context) => _selectSwipeAction(context, SwipeDirection.left), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/pages/settings_editor_page.dart b/lib/pages/settings/pages/settings_editor_page.dart new file mode 100644 index 00000000..89497988 --- /dev/null +++ b/lib/pages/settings/pages/settings_editor_page.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; + +/// Settings related to the notes editor. +class SettingsEditorPage extends StatefulWidget { + const SettingsEditorPage({super.key}); + + @override + State createState() => _SettingsEditorPageState(); +} + +class _SettingsEditorPageState extends State { + /// Toggles the setting to show the undo/redo buttons in the editor's app bar. + void _toggleShowUndoRedoButtons(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.showUndoRedoButtons.name, toggled); + }); + } + + /// Toggles the setting to show the checklist button in the editor's app bar or toolbar. + /// + /// If the editor's toolbar is enabled, the checklist button is shown inside it. + /// Otherwise, it's shown in the editor's app bar. + void _toggleShowChecklistButton(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.showChecklistButton.name, toggled); + }); + } + + /// Toggles the setting to show the editor's toolbar. + void _toggleShowToolbar(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.showToolbar.name, toggled); + }); + } + + /// Toggles the setting to use spacing between the paragraphs. + void _toggleUseParagraphSpacing(bool toggled) { + setState(() { + PreferencesUtils().set(PreferenceKey.useParagraphsSpacing.name, toggled); + }); + } + + @override + Widget build(BuildContext context) { + final bool showUndoRedoButtons = PreferenceKey.showUndoRedoButtons.getPreferenceOrDefault(); + final bool showChecklistButton = PreferenceKey.showChecklistButton.getPreferenceOrDefault(); + final bool showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); + + final bool useParagraphsSpacing = PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault(); + + return CustomSettingsList( + sections: [ + SettingsSection( + title: Text(localizations.settings_editor_formatting), + tiles: [ + SettingsTile.switchTile( + leading: const Icon(Icons.undo), + title: Text(localizations.settings_show_undo_redo_buttons), + description: Text(localizations.settings_show_undo_redo_buttons_description), + initialValue: showUndoRedoButtons, + onToggle: _toggleShowUndoRedoButtons, + ), + SettingsTile.switchTile( + leading: const Icon(Icons.checklist), + title: Text(localizations.settings_show_checklist_button), + description: Text(localizations.settings_show_checklist_button_description), + initialValue: showChecklistButton, + onToggle: _toggleShowChecklistButton, + ), + SettingsTile.switchTile( + leading: const Icon(Icons.format_paint), + title: Text(localizations.settings_show_toolbar), + description: Text(localizations.settings_show_toolbar_description), + initialValue: showToolbar, + onToggle: _toggleShowToolbar, + ), + ], + ), + SettingsSection( + title: Text(localizations.settings_editor_appearance), + tiles: [ + SettingsTile.switchTile( + leading: const Icon(Icons.format_line_spacing), + title: Text(localizations.settings_use_paragraph_spacing), + description: Text(localizations.settings_use_paragraph_spacing_description), + initialValue: useParagraphsSpacing, + onToggle: _toggleUseParagraphSpacing, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/settings_actions.dart b/lib/pages/settings/settings_actions.dart deleted file mode 100644 index a5b3d489..00000000 --- a/lib/pages/settings/settings_actions.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:locale_names/locale_names.dart'; -import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart'; -import 'package:localmaterialnotes/providers/bin/bin_provider.dart'; -import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/utils/asset.dart'; -import 'package:localmaterialnotes/utils/constants/constants.dart'; -import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/constants/sizes.dart'; -import 'package:localmaterialnotes/utils/database_utils.dart'; -import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; -import 'package:localmaterialnotes/utils/info_utils.dart'; -import 'package:localmaterialnotes/utils/locale_utils.dart'; -import 'package:localmaterialnotes/utils/preferences/confirmations.dart'; -import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; -import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; -import 'package:localmaterialnotes/utils/snack_bar_utils.dart'; -import 'package:localmaterialnotes/utils/theme_utils.dart'; -import 'package:restart_app/restart_app.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SettingsActions { - void toggleBooleanSetting(PreferenceKey preferenceKey, bool toggled) { - PreferencesUtils().set(preferenceKey.name, toggled); - } - - Future selectLanguage(BuildContext context) async { - await showAdaptiveDialog( - context: context, - builder: (context) { - return SimpleDialog( - clipBehavior: Clip.hardEdge, - title: Text(localizations.settings_language), - children: AppLocalizations.supportedLocales.map((locale) { - return RadioListTile( - value: locale, - groupValue: Localizations.localeOf(context), - title: Text(locale.nativeDisplayLanguage.capitalized), - selected: Localizations.localeOf(context) == locale, - onChanged: (locale) => Navigator.of(context).pop(locale), - ); - }).toList(), - ); - }, - ).then((locale) async { - if (locale == null) { - return; - } - - LocaleUtils().setLocale(locale); - - // The Restart package crashes the app if used in debug mode - if (!kDebugMode) { - await Restart.restartApp(); - } - }); - } - - Future selectTheme(BuildContext context) async { - await showAdaptiveDialog( - context: context, - builder: (context) { - return SimpleDialog( - clipBehavior: Clip.hardEdge, - title: Text(localizations.settings_theme), - children: [ - RadioListTile( - value: ThemeMode.system, - groupValue: ThemeUtils().themeMode, - title: Text(localizations.settings_theme_system), - selected: ThemeUtils().themeMode == ThemeMode.system, - onChanged: (locale) => Navigator.of(context).pop(locale), - ), - RadioListTile( - value: ThemeMode.light, - groupValue: ThemeUtils().themeMode, - title: Text(localizations.settings_theme_light), - selected: ThemeUtils().themeMode == ThemeMode.light, - onChanged: (locale) => Navigator.of(context).pop(locale), - ), - RadioListTile( - value: ThemeMode.dark, - groupValue: ThemeUtils().themeMode, - title: Text(localizations.settings_theme_dark), - selected: ThemeUtils().themeMode == ThemeMode.dark, - onChanged: (locale) => Navigator.of(context).pop(locale), - ), - ], - ); - }, - ).then((themeMode) { - if (themeMode == null) { - return; - } - - ThemeUtils().setThemeMode(themeMode); - }); - } - - void toggleDynamicTheming(bool toggled) { - PreferencesUtils().set(PreferenceKey.dynamicTheming.name, toggled); - dynamicThemingNotifier.value = toggled; - } - - void toggleBlackTheming(bool toggled) { - PreferencesUtils().set(PreferenceKey.blackTheming.name, toggled); - blackThemingNotifier.value = toggled; - } - - Future selectConfirmations(BuildContext context) async { - final confirmationsPreference = Confirmations.fromPreference(); - - await showAdaptiveDialog( - context: context, - builder: (context) { - return SimpleDialog( - clipBehavior: Clip.hardEdge, - title: Text(localizations.settings_confirmations), - children: Confirmations.values.map((confirmationsValue) { - return RadioListTile( - value: confirmationsValue, - groupValue: confirmationsPreference, - title: Text(confirmationsValue.title), - selected: confirmationsPreference == confirmationsValue, - onChanged: (locale) => Navigator.of(context).pop(locale), - ); - }).toList(), - ); - }, - ).then((confirmationsValue) { - if (confirmationsValue == null) { - return; - } - - PreferencesUtils().set(PreferenceKey.confirmations.name, confirmationsValue.name); - }); - } - - Future backupAsJson(BuildContext context) async { - try { - if (await DatabaseUtils().exportAsJson()) { - SnackBarUtils.info(localizations.settings_export_success).show(); - } - } catch (exception, stackTrace) { - log(exception.toString(), stackTrace: stackTrace); - - SnackBarUtils.info(exception.toString()).show(); - } - } - - Future backupAsMarkdown(BuildContext context) async { - try { - if (await DatabaseUtils().exportAsMarkdown()) { - SnackBarUtils.info(localizations.settings_export_success).show(); - } - } catch (exception, stackTrace) { - log(exception.toString(), stackTrace: stackTrace); - - SnackBarUtils.info(exception.toString()).show(); - } - } - - Future import(BuildContext context, WidgetRef ref) async { - try { - final imported = await DatabaseUtils().import(); - - if (imported) { - await ref.read(notesProvider.notifier).get(); - await ref.read(binProvider.notifier).get(); - - SnackBarUtils.info(localizations.settings_import_success).show(); - } - } catch (exception, stackTrace) { - log(exception.toString(), stackTrace: stackTrace); - - SnackBarUtils.info(exception.toString()).show(); - } - } - - Future showAbout(BuildContext context) async { - showAboutDialog( - context: context, - useRootNavigator: false, - applicationName: localizations.app_name, - applicationVersion: InfoUtils().appVersion, - applicationIcon: Image.asset( - Asset.icon.path, - filterQuality: FilterQuality.medium, - fit: BoxFit.fitWidth, - width: Sizes.size64.size, - ), - applicationLegalese: localizations.settings_licence_description, - children: [ - Padding(padding: Paddings.padding16.vertical), - Text( - localizations.app_tagline, - style: Theme.of(context).textTheme.titleSmall, - ), - Padding(padding: Paddings.padding8.vertical), - Text(localizations.app_about(localizations.app_name)), - ], - ); - } - - void openGitHub(_) { - launchUrl( - Uri( - scheme: 'https', - host: 'github.com', - path: 'maelchiotti/LocalMaterialNotes', - ), - ); - } - - void openLicense(_) { - launchUrl( - Uri( - scheme: 'https', - host: 'github.com', - path: 'maelchiotti/LocalMaterialNotes/blob/main/LICENSE', - ), - ); - } - - void openIssues(_) { - launchUrl( - Uri( - scheme: 'https', - host: 'github.com', - path: 'maelchiotti/LocalMaterialNotes/issues', - ), - ); - } -} diff --git a/lib/pages/settings/settings_main_page.dart b/lib/pages/settings/settings_main_page.dart new file mode 100644 index 00000000..7fddb1ac --- /dev/null +++ b/lib/pages/settings/settings_main_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:localmaterialnotes/common/routing/router_route.dart'; +import 'package:localmaterialnotes/pages/settings/enums/settings_page.dart'; +import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; + +/// Page for the settings of the application. +class SettingsMainPage extends ConsumerStatefulWidget { + const SettingsMainPage(); + + @override + ConsumerState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends ConsumerState { + void _openSettingsPage(SettingsPage page) { + switch (page) { + case SettingsPage.appearance: + context.push(RouterRoute.settingsAppearance.fullPath!); + case SettingsPage.behavior: + context.push(RouterRoute.settingsBehavior.fullPath!); + case SettingsPage.editor: + context.push(RouterRoute.settingsEditor.fullPath!); + case SettingsPage.backup: + context.push(RouterRoute.settingsBackup.fullPath!); + case SettingsPage.about: + context.push(RouterRoute.settingsAbout.fullPath!); + } + } + + @override + Widget build(BuildContext context) { + return CustomSettingsList( + sections: [ + SettingsSection( + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.palette), + title: Text(localizations.settings_appearance), + description: Text(localizations.settings_appearance_description), + onPressed: (_) => _openSettingsPage(SettingsPage.appearance), + ), + SettingsTile.navigation( + leading: const Icon(Icons.swipe), + title: Text(localizations.settings_behavior), + description: Text(localizations.settings_behavior_description), + onPressed: (_) => _openSettingsPage(SettingsPage.behavior), + ), + SettingsTile.navigation( + leading: const Icon(Icons.format_color_text), + title: Text(localizations.settings_editor), + description: Text(localizations.settings_editor_description), + onPressed: (_) => _openSettingsPage(SettingsPage.editor), + ), + SettingsTile.navigation( + leading: const Icon(Icons.settings_backup_restore), + title: Text(localizations.settings_backup), + description: Text(localizations.settings_backup_description), + onPressed: (_) => _openSettingsPage(SettingsPage.backup), + ), + SettingsTile.navigation( + leading: const Icon(Icons.info), + title: Text(localizations.settings_about), + description: Text(localizations.settings_about_description), + onPressed: (_) => _openSettingsPage(SettingsPage.about), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart deleted file mode 100644 index 1236e7ce..00000000 --- a/lib/pages/settings/settings_page.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_settings_ui/flutter_settings_ui.dart'; -import 'package:locale_names/locale_names.dart'; -import 'package:localmaterialnotes/pages/settings/settings_actions.dart'; -import 'package:localmaterialnotes/providers/notes/notes_provider.dart'; -import 'package:localmaterialnotes/utils/constants/constants.dart'; -import 'package:localmaterialnotes/utils/constants/paddings.dart'; -import 'package:localmaterialnotes/utils/extensions/string_extension.dart'; -import 'package:localmaterialnotes/utils/info_utils.dart'; -import 'package:localmaterialnotes/utils/preferences/confirmations.dart'; -import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; -import 'package:localmaterialnotes/utils/theme_utils.dart'; -import 'package:simple_icons/simple_icons.dart'; - -class SettingsPage extends ConsumerStatefulWidget { - const SettingsPage(); - - @override - ConsumerState createState() => _SettingsPageState(); -} - -class _SettingsPageState extends ConsumerState { - final interactions = SettingsActions(); - - bool showUndoRedoButtons = PreferenceKey.showUndoRedoButtons.getPreferenceOrDefault(); - bool showChecklistButton = PreferenceKey.showChecklistButton.getPreferenceOrDefault(); - bool showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); - - bool showSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault(); - bool showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault(); - - @override - Widget build(BuildContext context) { - return SettingsList( - platform: DevicePlatform.android, - contentPadding: Paddings.custom.bottomSystemUi, - lightTheme: SettingsThemeData( - settingsListBackground: Theme.of(context).colorScheme.surface, - ), - darkTheme: SettingsThemeData( - settingsListBackground: Theme.of(context).colorScheme.surface, - ), - sections: [ - SettingsSection( - title: Text(localizations.settings_appearance), - tiles: [ - SettingsTile.navigation( - leading: const Icon(Icons.language), - title: Text(localizations.settings_language), - value: Text(Localizations.localeOf(context).nativeDisplayLanguage.capitalized), - onPressed: interactions.selectLanguage, - ), - SettingsTile.navigation( - leading: const Icon(Icons.palette), - title: Text(localizations.settings_theme), - value: Text(ThemeUtils().themeModeName), - onPressed: (context) async { - await interactions.selectTheme(context); - setState(() {}); - }, - ), - SettingsTile.switchTile( - enabled: ThemeUtils().isDynamicThemingAvailable, - leading: const Icon(Icons.bolt), - title: Text(localizations.settings_dynamic_theming), - description: Text(localizations.settings_dynamic_theming_description), - initialValue: ThemeUtils().useDynamicTheming, - onToggle: interactions.toggleDynamicTheming, - ), - SettingsTile.switchTile( - enabled: ThemeUtils().brightness == Brightness.dark, - leading: const Icon(Icons.nightlight), - title: Text(localizations.settings_black_theming), - description: Text(localizations.settings_black_theming_description), - initialValue: ThemeUtils().useBlackTheming, - onToggle: (toggled) { - interactions.toggleBlackTheming(toggled); - setState(() {}); - }, - ), - ], - ), - SettingsSection( - title: Text(localizations.settings_editor), - tiles: [ - SettingsTile.switchTile( - leading: const Icon(Icons.undo), - title: Text(localizations.settings_show_undo_redo_buttons), - description: Text(localizations.settings_show_undo_redo_buttons_description), - initialValue: showUndoRedoButtons, - onToggle: (toggled) { - interactions.toggleBooleanSetting(PreferenceKey.showUndoRedoButtons, toggled); - setState(() { - showUndoRedoButtons = toggled; - }); - }, - ), - SettingsTile.switchTile( - leading: const Icon(Icons.checklist), - title: Text(localizations.settings_show_checklist_button), - description: Text(localizations.settings_show_checklist_button_description), - initialValue: showChecklistButton, - onToggle: (toggled) { - interactions.toggleBooleanSetting(PreferenceKey.showChecklistButton, toggled); - setState(() { - showChecklistButton = toggled; - }); - }, - ), - SettingsTile.switchTile( - leading: const Icon(Icons.format_paint), - title: Text(localizations.settings_show_toolbar), - description: Text(localizations.settings_show_toolbar_description), - initialValue: showToolbar, - onToggle: (toggled) { - interactions.toggleBooleanSetting(PreferenceKey.showToolbar, toggled); - setState(() { - showToolbar = toggled; - }); - }, - ), - ], - ), - SettingsSection( - title: Text(localizations.settings_behavior), - tiles: [ - SettingsTile.navigation( - leading: const Icon(Icons.warning), - title: Text(localizations.settings_confirmations), - value: Text(Confirmations.fromPreference().title), - onPressed: (context) async { - await interactions.selectConfirmations(context); - setState(() {}); - }, - ), - SettingsTile.switchTile( - leading: const Icon(Icons.safety_divider), - title: Text(localizations.settings_show_separators), - description: Text(localizations.settings_show_separators_description), - initialValue: showSeparators, - onToggle: (toggled) { - interactions.toggleBooleanSetting(PreferenceKey.showSeparators, toggled); - setState(() { - showSeparators = toggled; - }); - ref.invalidate(notesProvider); // Refresh the notes and bin pages - }, - ), - SettingsTile.switchTile( - leading: const Icon(Icons.gradient), - title: Text(localizations.settings_show_tiles_background), - description: Text(localizations.settings_show_tiles_background_description), - initialValue: showTilesBackground, - onToggle: (toggled) { - interactions.toggleBooleanSetting(PreferenceKey.showTilesBackground, toggled); - setState(() { - showTilesBackground = toggled; - }); - ref.invalidate(notesProvider); // Refresh the notes and bin pages - }, - ), - ], - ), - SettingsSection( - title: Text(localizations.settings_backup), - tiles: [ - SettingsTile.navigation( - leading: const Icon(SimpleIcons.json), - title: Text(localizations.settings_export_json), - value: Text(localizations.settings_export_json_description), - onPressed: interactions.backupAsJson, - ), - SettingsTile.navigation( - leading: const Icon(SimpleIcons.markdown), - title: Text(localizations.settings_export_markdown), - value: Text(localizations.settings_export_markdown_description), - onPressed: interactions.backupAsMarkdown, - ), - SettingsTile.navigation( - leading: const Icon(Icons.file_upload), - title: Text(localizations.settings_import), - value: Text(localizations.settings_import_description), - onPressed: (context) => interactions.import(context, ref), - ), - ], - ), - SettingsSection( - title: Text(localizations.settings_about), - tiles: [ - SettingsTile( - leading: const Icon(Icons.info), - title: Text(localizations.app_name), - value: Text(InfoUtils().appVersion), - onPressed: interactions.showAbout, - ), - SettingsTile( - leading: const Icon(SimpleIcons.github), - title: Text(localizations.settings_github), - value: Text(localizations.settings_github_description), - onPressed: interactions.openGitHub, - ), - SettingsTile( - leading: const Icon(Icons.balance), - title: Text(localizations.settings_licence), - value: Text(localizations.settings_licence_description), - onPressed: interactions.openLicense, - ), - SettingsTile( - leading: const Icon(Icons.bug_report), - title: Text(localizations.settings_issue), - value: Text(localizations.settings_issue_description), - onPressed: interactions.openIssues, - ), - ], - ), - ], - ); - } -} diff --git a/lib/pages/settings/widgets/custom_settings_list.dart b/lib/pages/settings/widgets/custom_settings_list.dart new file mode 100644 index 00000000..73ef25cb --- /dev/null +++ b/lib/pages/settings/widgets/custom_settings_list.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:localmaterialnotes/utils/constants/paddings.dart'; + +class CustomSettingsList extends StatelessWidget { + const CustomSettingsList({ + super.key, + required this.sections, + }); + + final List sections; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SettingsList( + platform: DevicePlatform.android, + contentPadding: Paddings.custom.bottomSystemUi, + lightTheme: SettingsThemeData( + settingsListBackground: theme.colorScheme.surface, + titleTextColor: theme.textTheme.bodyMedium?.color, + ), + darkTheme: SettingsThemeData( + settingsListBackground: theme.colorScheme.surface, + titleTextColor: theme.textTheme.bodyMedium?.color, + ), + sections: sections, + ); + } +} diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart deleted file mode 100644 index 8c947dd6..00000000 --- a/lib/providers/base_provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:localmaterialnotes/utils/database_utils.dart'; - -mixin BaseProvider { - late final databaseUtils = DatabaseUtils(); -} diff --git a/lib/providers/bin/bin_provider.dart b/lib/providers/bin/bin_provider.dart index b1211ab5..092ad891 100644 --- a/lib/providers/bin/bin_provider.dart +++ b/lib/providers/bin/bin_provider.dart @@ -2,13 +2,13 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:localmaterialnotes/providers/base_provider.dart'; +import 'package:localmaterialnotes/utils/database_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'bin_provider.g.dart'; @riverpod -class Bin extends _$Bin with BaseProvider { +class Bin extends _$Bin { @override FutureOr> build() { return get(); @@ -18,8 +18,8 @@ class Bin extends _$Bin with BaseProvider { List notes = []; try { - notes = await databaseUtils.getAll(deleted: true); - } on Exception catch (exception, stackTrace) { + notes = await DatabaseUtils().getAll(deleted: true); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); } @@ -36,7 +36,7 @@ class Bin extends _$Bin with BaseProvider { Future empty() async { try { - await databaseUtils.emptyBin(); + await DatabaseUtils().emptyBin(); } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; @@ -49,7 +49,7 @@ class Bin extends _$Bin with BaseProvider { Future permanentlyDelete(Note permanentlyDeletedNote) async { try { - await databaseUtils.delete(permanentlyDeletedNote); + await DatabaseUtils().delete(permanentlyDeletedNote); } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; @@ -69,8 +69,8 @@ class Bin extends _$Bin with BaseProvider { } try { - await databaseUtils.deleteAll(notes); - } on Exception catch (exception, stackTrace) { + await DatabaseUtils().deleteAll(notes); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } @@ -87,8 +87,8 @@ class Bin extends _$Bin with BaseProvider { restoredNote.deleted = false; try { - await databaseUtils.put(restoredNote); - } on Exception catch (exception, stackTrace) { + await DatabaseUtils().put(restoredNote); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } @@ -107,8 +107,8 @@ class Bin extends _$Bin with BaseProvider { } try { - await databaseUtils.putAll(notes); - } on Exception catch (exception, stackTrace) { + await DatabaseUtils().putAll(notes); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } diff --git a/lib/providers/bin/bin_provider.g.dart b/lib/providers/bin/bin_provider.g.dart index e41b2793..a0c5bee2 100644 --- a/lib/providers/bin/bin_provider.g.dart +++ b/lib/providers/bin/bin_provider.g.dart @@ -6,7 +6,7 @@ part of 'bin_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$binHash() => r'318b346690c8597f9e87779f915ecf3a9bf62f6a'; +String _$binHash() => r'7a115c874d69d5ad6e75f0cf60972898e9a912c7'; /// See also [Bin]. @ProviderFor(Bin) diff --git a/lib/providers/current_note/current_note_provider.dart b/lib/providers/current_note/current_note_provider.dart deleted file mode 100644 index 6c8a7ec9..00000000 --- a/lib/providers/current_note/current_note_provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_note_provider.g.dart'; - -// ignore_for_file: use_setters_to_change_properties - -@Riverpod(keepAlive: true) -class CurrentNote extends _$CurrentNote { - @override - Note? build() { - return null; - } - - void set(Note note) { - state = note; - } - - void reset() { - state = null; - } -} diff --git a/lib/providers/current_note/current_note_provider.g.dart b/lib/providers/current_note/current_note_provider.g.dart deleted file mode 100644 index 7bd967be..00000000 --- a/lib/providers/current_note/current_note_provider.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_note_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentNoteHash() => r'3820bcba7cfe8a1bd751d7fce008658a598e4b8f'; - -/// See also [CurrentNote]. -@ProviderFor(CurrentNote) -final currentNoteProvider = NotifierProvider.internal( - CurrentNote.new, - name: r'currentNoteProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$currentNoteHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$CurrentNote = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/editor_controller/editor_controller_provider.dart b/lib/providers/editor_controller/editor_controller_provider.dart deleted file mode 100644 index a5b19b38..00000000 --- a/lib/providers/editor_controller/editor_controller_provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:fleather/fleather.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'editor_controller_provider.g.dart'; - -// ignore_for_file: use_setters_to_change_properties - -@Riverpod(keepAlive: true) -class EditorController extends _$EditorController { - @override - Raw? build() { - return null; - } - - void set(FleatherController fleatherController) { - state = fleatherController; - } - - void unset() { - state = null; - } -} diff --git a/lib/providers/editor_controller/editor_controller_provider.g.dart b/lib/providers/editor_controller/editor_controller_provider.g.dart deleted file mode 100644 index 3048bc72..00000000 --- a/lib/providers/editor_controller/editor_controller_provider.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'editor_controller_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$editorControllerHash() => r'daa52f679eec2218957f51064145f708efcbad46'; - -/// See also [EditorController]. -@ProviderFor(EditorController) -final editorControllerProvider = NotifierProvider.internal( - EditorController.new, - name: r'editorControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$editorControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$EditorController = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/layout/layout_provider.dart b/lib/providers/layout/layout_provider.dart deleted file mode 100644 index 82537232..00000000 --- a/lib/providers/layout/layout_provider.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:localmaterialnotes/utils/preferences/layout.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'layout_provider.g.dart'; - -// ignore_for_file: use_setters_to_change_properties - -@Riverpod(keepAlive: true) -class LayoutState extends _$LayoutState { - @override - Raw? build() { - return Layout.fromPreference(); - } - - void set(Layout layout) { - state = layout; - } -} diff --git a/lib/providers/layout/layout_provider.g.dart b/lib/providers/layout/layout_provider.g.dart deleted file mode 100644 index 3aa6090a..00000000 --- a/lib/providers/layout/layout_provider.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'layout_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$layoutStateHash() => r'8b990b1d966c7acf39f878724ea773403b1205d8'; - -/// See also [LayoutState]. -@ProviderFor(LayoutState) -final layoutStateProvider = NotifierProvider.internal( - LayoutState.new, - name: r'layoutStateProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$layoutStateHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$LayoutState = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/notes/notes_provider.dart b/lib/providers/notes/notes_provider.dart index d0ad0d58..7b7b8d0c 100644 --- a/lib/providers/notes/notes_provider.dart +++ b/lib/providers/notes/notes_provider.dart @@ -2,13 +2,13 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:localmaterialnotes/models/note/note.dart'; -import 'package:localmaterialnotes/providers/base_provider.dart'; +import 'package:localmaterialnotes/utils/database_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'notes_provider.g.dart'; @riverpod -class Notes extends _$Notes with BaseProvider { +class Notes extends _$Notes { @override FutureOr> build() { return get(); @@ -18,8 +18,8 @@ class Notes extends _$Notes with BaseProvider { List notes = []; try { - notes = await databaseUtils.getAll(deleted: false); - } on Exception catch (exception, stackTrace) { + notes = await DatabaseUtils().getAll(deleted: false); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); } @@ -39,11 +39,11 @@ class Notes extends _$Notes with BaseProvider { try { if (editedNote.isEmpty) { - await databaseUtils.delete(editedNote); + await DatabaseUtils().delete(editedNote); } else { - await databaseUtils.put(editedNote); + await DatabaseUtils().put(editedNote); } - } on Exception catch (exception, stackTrace) { + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } @@ -76,8 +76,8 @@ class Notes extends _$Notes with BaseProvider { } try { - await databaseUtils.putAll(notes); - } on Exception catch (exception, stackTrace) { + await DatabaseUtils().putAll(notes); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } @@ -110,8 +110,8 @@ class Notes extends _$Notes with BaseProvider { } try { - await databaseUtils.putAll(notes); - } on Exception catch (exception, stackTrace) { + await DatabaseUtils().putAll(notes); + } catch (exception, stackTrace) { log(exception.toString(), stackTrace: stackTrace); return false; } diff --git a/lib/providers/notes/notes_provider.g.dart b/lib/providers/notes/notes_provider.g.dart index e2ae1c11..3db4ad88 100644 --- a/lib/providers/notes/notes_provider.g.dart +++ b/lib/providers/notes/notes_provider.g.dart @@ -6,7 +6,7 @@ part of 'notes_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$notesHash() => r'e80fc51bd8684e2e88b546d7d58d19aa9289e648'; +String _$notesHash() => r'35f1e8c7232c2411f82483f44b883cebdb522496'; /// See also [Notes]. @ProviderFor(Notes) diff --git a/lib/providers/notifiers.dart b/lib/providers/notifiers.dart new file mode 100644 index 00000000..06963e13 --- /dev/null +++ b/lib/providers/notifiers.dart @@ -0,0 +1,28 @@ +import 'package:fleather/fleather.dart'; +import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/models/note/note.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/theme_utils.dart'; + +final themeModeNotifier = ValueNotifier(ThemeUtils().themeMode); +final dynamicThemingNotifier = ValueNotifier(ThemeUtils().useDynamicTheming); +final blackThemingNotifier = ValueNotifier(ThemeUtils().useBlackTheming); + +final isSelectionModeNotifier = ValueNotifier(false); + +final layoutNotifier = ValueNotifier(Layout.fromPreference()); +final showTilesBackgroundNotifier = ValueNotifier(PreferenceKey.showTilesBackground.getPreferenceOrDefault()); +final showSeparatorsNotifier = ValueNotifier(PreferenceKey.showSeparators.getPreferenceOrDefault()); + +final swipeActionsNotifier = ValueNotifier( + (SwipeAction.rightFromPreference(), SwipeAction.leftFromPreference()), +); + +final currentNoteNotifier = ValueNotifier(null); + +final fleatherControllerNotifier = ValueNotifier(null); +final fleatherControllerCanUndoNotifier = ValueNotifier(false); +final fleatherControllerCanRedoNotifier = ValueNotifier(false); +final fleatherFieldHasFocusNotifier = ValueNotifier(false); diff --git a/lib/providers/selection_mode/selection_mode_provider.dart b/lib/providers/selection_mode/selection_mode_provider.dart deleted file mode 100644 index 2c157a2d..00000000 --- a/lib/providers/selection_mode/selection_mode_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'selection_mode_provider.g.dart'; - -@Riverpod(keepAlive: true) -class SelectionMode extends _$SelectionMode { - @override - bool build() { - return false; - } - - void enterSelectionMode() { - state = true; - } - - void exitSelectionMode() { - state = false; - } -} diff --git a/lib/providers/selection_mode/selection_mode_provider.g.dart b/lib/providers/selection_mode/selection_mode_provider.g.dart deleted file mode 100644 index 73c2f638..00000000 --- a/lib/providers/selection_mode/selection_mode_provider.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'selection_mode_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$selectionModeHash() => r'ee4c15832eab0b67a50069888fbe232ed711b98f'; - -/// See also [SelectionMode]. -@ProviderFor(SelectionMode) -final selectionModeProvider = NotifierProvider.internal( - SelectionMode.new, - name: r'selectionModeProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$selectionModeHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SelectionMode = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/utils/auto_export_utils.dart b/lib/utils/auto_export_utils.dart new file mode 100644 index 00000000..02e79534 --- /dev/null +++ b/lib/utils/auto_export_utils.dart @@ -0,0 +1,116 @@ +import 'dart:io'; + +import 'package:localmaterialnotes/utils/database_utils.dart'; +import 'package:localmaterialnotes/utils/extensions/uri_extension.dart'; +import 'package:localmaterialnotes/utils/files_utils.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Utilities for the auto export functionality. +/// +/// This class is a singleton. +class AutoExportUtils { + static final AutoExportUtils _singleton = AutoExportUtils._internal(); + + factory AutoExportUtils() { + return _singleton; + } + + AutoExportUtils._internal(); + + /// Root directory where auto exports are located. + late Uri _autoExportDirectory; + + /// Path to the default download directory on Android devices. + final _downloadDirectoryPath = '/storage/emulated/0/Download'; + + /// Subdirectories to add after the export path. + final subDirectories = ['Material Notes', 'backups']; + + /// Precise directory where auto exports are located. + /// + /// It's a combination of [_autoExportDirectory] and [subDirectories]. + Uri get backupsDirectory { + final backupsDirectoryPath = joinAll([_autoExportDirectory.path, ...subDirectories]); + + return Uri.directory(backupsDirectoryPath); + } + + Future ensureInitialized() async { + await _setAutoExportDirectory(); + + await performAutoExportIfNeeded(); + } + + /// Returns the JSON file in which to write the exported data. + Future get getAutoExportFile async { + return getExportFile( + backupsDirectory.toDecodedString, + 'json', + ); + } + + /// Set the auto export directory. + /// + /// By default, the auto export directory is the Android's Download directory. + /// If it doesn't exist, the application documents directory is used. + Future _setAutoExportDirectory() async { + final downloadsDirectory = Directory(_downloadDirectoryPath); + + if (!downloadsDirectory.existsSync()) { + final externalStorageDirectory = await getApplicationDocumentsDirectory(); + + _autoExportDirectory = externalStorageDirectory.uri; + } + + _autoExportDirectory = downloadsDirectory.uri; + } + + /// Checks if an auto export should be performed. + /// + /// An auto export should be performed if it is enabled and either if: + /// - no auto export has been performed yet + /// - or the time difference between now and the last auto export is greater than the auto export frequency + /// chosen by the user + bool _shouldPerformAutoExport() { + final autoExportFrequency = PreferenceKey.autoExportFrequency.getPreferenceOrDefault(); + + // Auto export is disabled + if (autoExportFrequency == 0) { + return false; + } + + final lastAutoExportDate = DateTime.tryParse(PreferenceKey.lastAutoExportDate.getPreferenceOrDefault()); + + // If the last auto export date is null, perform the auto first auto export now + if (lastAutoExportDate == null) { + return true; + } + + final durationSinceLastAutoExport = DateTime.now().difference(lastAutoExportDate); + final autoExportFrequencyDuration = Duration(days: autoExportFrequency.toInt()); + + // If no auto export has been done for longer than the defined auto export frequency, + // then perform an auto export now + return durationSinceLastAutoExport > autoExportFrequencyDuration; + } + + /// Performs an auto export of the database if it is needed. + Future performAutoExportIfNeeded() async { + if (!_shouldPerformAutoExport()) { + return; + } + + final encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault(); + final password = await PreferenceKey.autoExportPassword.getPreferenceOrDefaultSecure(); + + DatabaseUtils().autoExportAsJson(encrypt, password); + + PreferencesUtils().set( + PreferenceKey.lastAutoExportDate.name, + DateTime.now().toIso8601String(), + ); + } +} diff --git a/lib/utils/constants/constants.dart b/lib/utils/constants/constants.dart index cfb03852..e004f3c2 100644 --- a/lib/utils/constants/constants.dart +++ b/lib/utils/constants/constants.dart @@ -1,22 +1,12 @@ import 'package:flutter/material.dart'; import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart'; import 'package:localmaterialnotes/utils/hardcoded_localizations_utils.dart'; -import 'package:localmaterialnotes/utils/theme_utils.dart'; import 'package:parchment/codecs.dart'; // ignore: depend_on_referenced_packages -import 'package:uuid/uuid.dart'; - -final themeModeNotifier = ValueNotifier(ThemeUtils().themeMode); -final dynamicThemingNotifier = ValueNotifier(ThemeUtils().useDynamicTheming); -final blackThemingNotifier = ValueNotifier(ThemeUtils().useBlackTheming); - -final fleatherFieldHasFocusNotifier = ValueNotifier(false); final navigatorKey = GlobalKey(debugLabel: 'Root navigator key'); final drawerKey = GlobalKey(debugLabel: 'Drawer key'); -final localizations = AppLocalizations.of(navigatorKey.currentContext!)!; +final localizations = AppLocalizations.of(navigatorKey.currentContext!); final hardcodedLocalizations = HardcodedLocalizationsUtils(); -const uuid = Uuid(); - const parchmentMarkdownCodec = ParchmentMarkdownCodec(); diff --git a/lib/utils/constants/sizes.dart b/lib/utils/constants/sizes.dart index 21346d6e..192d7865 100644 --- a/lib/utils/constants/sizes.dart +++ b/lib/utils/constants/sizes.dart @@ -19,13 +19,15 @@ enum Sizes { double get infinity => double.infinity; - double get searchAppBar => 8; + double get selectionAppBar => 8; double get searchBar => 48; int get gridLayoutColumnWidth => 384; - double get editorToolbarHeight => 48; + double get editorToolbarButtonHeight => 42; + + double get editorToolbarButtonWidth => 42; double get notesGridViewSpacing => 8; diff --git a/lib/utils/database_utils.dart b/lib/utils/database_utils.dart index 322a30a7..fa1b8d6d 100644 --- a/lib/utils/database_utils.dart +++ b/lib/utils/database_utils.dart @@ -1,17 +1,24 @@ import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; import 'dart:typed_data'; import 'package:archive/archive.dart'; +import 'package:flutter/material.dart'; import 'package:is_first_run/is_first_run.dart'; import 'package:isar/isar.dart'; import 'package:localmaterialnotes/models/note/note.dart'; +import 'package:localmaterialnotes/pages/settings/dialogs/import_dialog.dart'; +import 'package:localmaterialnotes/utils/auto_export_utils.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/extensions/date_time_extensions.dart'; +import 'package:localmaterialnotes/utils/files_utils.dart'; +import 'package:localmaterialnotes/utils/info_utils.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/sort_method.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; -import 'package:localmaterialnotes/utils/preferences/sort_method.dart'; +import 'package:localmaterialnotes/utils/snack_bar_utils.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sanitize_filename/sanitize_filename.dart'; -import 'package:shared_storage/shared_storage.dart' as saf; class DatabaseUtils { static final DatabaseUtils _singleton = DatabaseUtils._internal(); @@ -26,7 +33,7 @@ class DatabaseUtils { late String _databaseDirectory; late Isar _database; - Future init() async { + Future ensureInitialized() async { _databaseDirectory = (await getApplicationDocumentsDirectory()).path; _database = await Isar.open( [NoteSchema], @@ -57,7 +64,7 @@ class DatabaseUtils { ? await sortedByPinned.thenByTitle().findAll() : await sortedByPinned.thenByTitleDesc().findAll(); default: - throw Exception(); + throw Exception('The sort methode is not set: $sortMethod'); } } @@ -75,13 +82,13 @@ class DatabaseUtils { Future delete(Note note) async { await _database.writeTxn(() async { - await _database.notes.delete(note.isarId); + await _database.notes.delete(note.id); }); } Future deleteAll(List notes) async { await _database.writeTxn(() async { - await _database.notes.deleteAll(notes.map((note) => note.isarId).toList()); + await _database.notes.deleteAll(notes.map((note) => note.id).toList()); }); } @@ -91,51 +98,47 @@ class DatabaseUtils { }); } - Future isBinEmpty() async { - return await _database.notes.where().deletedEqualTo(true).isEmpty(); - } - - Future _getDirectory() async { - final directory = await saf.openDocumentTree(); + Future _exportAsJson(bool encrypt, String? password, File file) async { + var notes = await getAll(); - if (directory == null) { - return null; + if (encrypt && password != null && password.isNotEmpty) { + notes = notes.map((note) => note.encrypted(password)).toList(); } - return directory; - } + final version = InfoUtils().appVersion; - Future _export(Uri directory, String mimeType, String extension, Uint8List bytes) async { - final timestamp = DateTime.timestamp(); - final filename = 'materialnotes_export_${timestamp.filename}'; + final exportData = { + 'version': version, + 'encrypted': encrypt, + 'notes': notes, + }; + final exportDataAsJson = jsonEncode(exportData); - await saf.createFile( - directory, - mimeType: mimeType, - displayName: filename, - bytes: bytes, - ); + return await writeStringToFile(file, exportDataAsJson); + } - return true; + Future autoExportAsJson(bool encrypt, String password) async { + final file = await AutoExportUtils().getAutoExportFile; + + return await _exportAsJson(encrypt, password, file); } - Future exportAsJson() async { - final directory = await _getDirectory(); + Future manuallyExportAsJson(bool encrypt, String? password) async { + final exportDirectory = await pickDirectory(); - if (directory == null) { + if (exportDirectory == null) { return false; } - final notes = await getAll(); - final notesAsJson = jsonEncode(notes); + final file = getExportFile(exportDirectory, 'json'); - return await _export(directory, 'application/json', 'json', Uint8List.fromList(utf8.encode(notesAsJson))); + return await _exportAsJson(encrypt, password, file); } Future exportAsMarkdown() async { - final directory = await _getDirectory(); + final exportDirectory = await pickDirectory(); - if (directory == null) { + if (exportDirectory == null) { return false; } @@ -155,29 +158,69 @@ class DatabaseUtils { return false; } - return await _export(directory, 'application/zip', 'zip', Uint8List.fromList(encodedArchive)); + final file = getExportFile(exportDirectory, 'zip'); + + return await writeBytesToFile(file, encodedArchive); } - Future import() async { - final importFiles = await saf.openDocument( - grantWritePermission: false, - mimeType: 'application/json', - ); + Future import(BuildContext context) async { + final importPlatformFile = await pickSingleFile(typeGroupJson); - if (importFiles == null || importFiles.isEmpty) { + if (importPlatformFile == null) { return false; } - final importedData = await saf.getDocumentContent(importFiles.first); - - if (importedData == null) { - throw Exception(localizations.error_read_file); + final importedString = await importPlatformFile.readAsString(); + var importedJson = jsonDecode(importedString); + + List? notes; + + // If the imported JSON is just a list, then it's the old export format (before v1.5.0) that just contains + // the notes list. Otherwise, it's the new export format (after v1.5.0) that contains other data. + if (importedJson is List) { + notes = importedJson.map((noteAsJson) { + return Note.fromJson(noteAsJson as Map); + }).toList(); + } else { + importedJson = importedJson as Map; + + final encrypted = importedJson['encrypted'] as bool; + final notesAsJson = importedJson['notes'] as List; + + if (encrypted && context.mounted) { + final password = await showAdaptiveDialog( + context: context, + builder: (context) => ImportDialog(title: localizations.settings_import), + ); + + if (password == null) { + return false; + } + + try { + notes = notesAsJson.map((noteAsJsonEncrypted) { + return Note.fromJsonEncrypted( + noteAsJsonEncrypted as Map, + password, + ); + }).toList(); + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + SnackBarUtils.error( + localizations.dialog_import_encryption_password_error, + duration: const Duration(seconds: 8), + ).show(); + + return false; + } + } else { + notes = notesAsJson.map((noteAsJson) { + return Note.fromJson(noteAsJson as Map); + }).toList(); + } } - final importedString = utf8.decode(importedData); - final notesJson = jsonDecode(importedString) as List; - final notes = notesJson.map((e) => Note.fromJson(e as Map)).toList(); - await putAll(notes); return true; diff --git a/lib/utils/encryption_utils.dart b/lib/utils/encryption_utils.dart new file mode 100644 index 00000000..3ed6d491 --- /dev/null +++ b/lib/utils/encryption_utils.dart @@ -0,0 +1,62 @@ +import 'package:encrypt/encrypt.dart'; + +/// Utilities for AES encryption and decryption of strings based on a user provided password. +class EncryptionUtils { + /// Generates a random initialization vector. + IV get generateIv { + return IV.fromSecureRandom(16); + } + + /// Generates a key based on the user provided [password]. + /// + /// The [password] must be exactly 32 characters long. + Key generateKey(String password) { + if (password.length != 32) { + throw Exception( + 'The password for AES encryption and decryption must be exactly 32 characters long, not ${password.length}', + ); + } + + return Key.fromUtf8(password); + } + + /// Returns the encrypter to perform AES CBC encryption and decryption based on the user provided [password]. + Encrypter getEncrypter(String password) { + final key = generateKey(password); + + return Encrypter(AES(key, mode: AESMode.cbc)); + } + + /// Extracts the initialization vector and the cipher from the [encryptedText]. + /// + /// The initialization vector is stored in the first 24 characters of the [encryptedText]. + /// The rest of the [encryptedText] corresponds to the cipher. + (IV, String) extractIvAndCipher(String encryptedText) { + final ivBase64 = encryptedText.substring(0, 24); + final iv = IV.fromBase64(ivBase64); + + final cipherBase64 = encryptedText.substring(24); + + return (iv, cipherBase64); + } + + /// Encrypts the [text] with the user provided [password]. + String encrypt(String password, String text) { + final encrypter = getEncrypter(password); + + final iv = generateIv; + final cipher = encrypter.encrypt(text, iv: iv); + + final initVectorAndEncryptedText = iv.base64 + cipher.base64; + + return initVectorAndEncryptedText; + } + + /// Decrypts the [text] with the user provided [password]. + String decrypt(String password, String text) { + final encrypter = getEncrypter(password); + final ivAndCipher = extractIvAndCipher(text); + + return encrypter.decrypt64(ivAndCipher.$2, iv: ivAndCipher.$1); + } +} diff --git a/lib/utils/extensions/go_router_extension.dart b/lib/utils/extensions/go_router_extension.dart index acf7a942..f5bd8c09 100644 --- a/lib/utils/extensions/go_router_extension.dart +++ b/lib/utils/extensions/go_router_extension.dart @@ -1,7 +1,7 @@ import 'package:go_router/go_router.dart'; extension GoRouterExtension on GoRouter { - String location() { + String get location { final RouteMatch lastMatch = routerDelegate.currentConfiguration.last; final RouteMatchList matchList = lastMatch is ImperativeRouteMatch ? lastMatch.matches : routerDelegate.currentConfiguration; diff --git a/lib/utils/extensions/string_extension.dart b/lib/utils/extensions/string_extension.dart index 68d13878..bee07e9b 100644 --- a/lib/utils/extensions/string_extension.dart +++ b/lib/utils/extensions/string_extension.dart @@ -4,4 +4,9 @@ extension StringExtension on String { String get capitalized { return toBeginningOfSentenceCase(this) ?? this; } + + bool get isStrongPassword { + return RegExp(r'''^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~`)\%\-(_+=;:,.<>/?"'[{\]}\|^]).{12,}$''') + .hasMatch(this); + } } diff --git a/lib/utils/extensions/uri_extension.dart b/lib/utils/extensions/uri_extension.dart new file mode 100644 index 00000000..46efdf02 --- /dev/null +++ b/lib/utils/extensions/uri_extension.dart @@ -0,0 +1,5 @@ +extension UriExtension on Uri { + String get toDecodedString { + return path.replaceAll('%20', ' '); + } +} diff --git a/lib/utils/files_utils.dart b/lib/utils/files_utils.dart new file mode 100644 index 00000000..c92d7932 --- /dev/null +++ b/lib/utils/files_utils.dart @@ -0,0 +1,58 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:localmaterialnotes/utils/extensions/date_time_extensions.dart'; +import 'package:path/path.dart'; + +const typeGroupJson = XTypeGroup( + label: 'JSON', + extensions: ['json'], +); + +File getExportFile(String directory, String extension) { + final timestamp = DateTime.timestamp(); + final fileName = 'materialnotes_export_${timestamp.filename}.$extension'; + final filePath = join(directory, fileName); + final fileUri = Uri.file(filePath); + + return File.fromUri(fileUri)..createSync(recursive: true); +} + +Future pickDirectory() async { + final directory = await getDirectoryPath(); + + if (directory == null) { + return null; + } + + return directory; +} + +Future pickSingleFile(XTypeGroup typeGroup) async { + return await openFile(acceptedTypeGroups: [typeGroup]); +} + +Future writeStringToFile(File file, String contents) async { + try { + await file.writeAsString(contents, mode: FileMode.writeOnly); + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + return false; + } + + return true; +} + +Future writeBytesToFile(File file, List bytes) async { + try { + await file.writeAsBytes(bytes, mode: FileMode.writeOnly); + } catch (exception, stackTrace) { + log(exception.toString(), stackTrace: stackTrace); + + return false; + } + + return true; +} diff --git a/lib/utils/flag_secure_utils.dart b/lib/utils/flag_secure_utils.dart new file mode 100644 index 00000000..6c011ce0 --- /dev/null +++ b/lib/utils/flag_secure_utils.dart @@ -0,0 +1,9 @@ +import 'package:flag_secure/flag_secure.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; + +/// Sets `FLAG_SECURE` to `true` if the corresponding setting was enabled by the user. +Future setFlagSecureIfNeeded() async { + if (PreferenceKey.flagSecure.getPreferenceOrDefault()) { + await FlagSecure.set(); + } +} diff --git a/lib/utils/hardcoded_localizations_utils.dart b/lib/utils/hardcoded_localizations_utils.dart index 193fe1ae..2ae0e750 100644 --- a/lib/utils/hardcoded_localizations_utils.dart +++ b/lib/utils/hardcoded_localizations_utils.dart @@ -2,6 +2,7 @@ import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.da import 'package:localmaterialnotes/l10n/app_localizations/app_localizations_en.g.dart'; import 'package:localmaterialnotes/l10n/app_localizations/app_localizations_es.g.dart'; import 'package:localmaterialnotes/l10n/app_localizations/app_localizations_fr.g.dart'; +import 'package:localmaterialnotes/l10n/app_localizations/app_localizations_ru.g.dart'; import 'package:localmaterialnotes/l10n/app_localizations/app_localizations_tr.g.dart'; import 'package:localmaterialnotes/utils/locale_utils.dart'; @@ -10,6 +11,7 @@ class HardcodedLocalizationsUtils { AppLocalizationsEn(), AppLocalizationsEs(), AppLocalizationsFr(), + AppLocalizationsRu(), AppLocalizationsTr(), ]; diff --git a/lib/utils/info_utils.dart b/lib/utils/info_utils.dart index ffbbd761..7afbf1e2 100644 --- a/lib/utils/info_utils.dart +++ b/lib/utils/info_utils.dart @@ -1,4 +1,6 @@ import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:package_info_plus/package_info_plus.dart'; class InfoUtils { @@ -13,7 +15,7 @@ class InfoUtils { late final PackageInfo _packageInfo; late final AndroidDeviceInfo _androidDeviceInfo; - Future init() async { + Future ensureInitialized() async { _packageInfo = await PackageInfo.fromPlatform(); _androidDeviceInfo = await DeviceInfoPlugin().androidInfo; } @@ -21,4 +23,11 @@ class InfoUtils { String get appVersion => _packageInfo.version; int get androidVersion => _androidDeviceInfo.version.sdkInt; + + String get brand => _androidDeviceInfo.brand; + + String get model => _androidDeviceInfo.model; + + String get buildMode => + kReleaseMode ? localizations.settings_build_mode_release : localizations.settings_build_mode_debug; } diff --git a/lib/utils/locale_utils.dart b/lib/utils/locale_utils.dart index c73e1db4..4bc97f51 100644 --- a/lib/utils/locale_utils.dart +++ b/lib/utils/locale_utils.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; @@ -11,15 +10,9 @@ class LocaleUtils { } Locale get appLocale { - final localePreferenceLanguageCode = PreferencesUtils().get(PreferenceKey.locale); + final localePreferenceLanguageCode = PreferenceKey.locale.getPreferenceOrDefault(); - if (localePreferenceLanguageCode != null) { - return Locale(localePreferenceLanguageCode); - } else { - return AppLocalizations.supportedLocales.contains(deviceLocale) - ? deviceLocale - : Locale(PreferenceKey.locale.defaultValue as String); - } + return Locale(localePreferenceLanguageCode); } void setLocale(Locale locale) { diff --git a/lib/utils/preferences/confirmations.dart b/lib/utils/preferences/enums/confirmations.dart similarity index 74% rename from lib/utils/preferences/confirmations.dart rename to lib/utils/preferences/enums/confirmations.dart index aef5e43e..7689334b 100644 --- a/lib/utils/preferences/confirmations.dart +++ b/lib/utils/preferences/enums/confirmations.dart @@ -3,33 +3,27 @@ import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; enum Confirmations { - none('none'), - irreversible('irreversible'), - all('all'), + none, + irreversible, + all, ; - final String name; + factory Confirmations.fromPreference() { + final preference = PreferencesUtils().get(PreferenceKey.confirmations); - const Confirmations(this.name); + return preference != null + ? Confirmations.values.byName(preference) + : PreferenceKey.confirmations.defaultValue as Confirmations; + } String get title { switch (this) { - case Confirmations.none: + case none: return localizations.confirmations_title_none; - case Confirmations.irreversible: + case irreversible: return localizations.confirmations_title_irreversible; - case Confirmations.all: + case all: return localizations.confirmations_title_all; - default: - throw Exception(); } } - - factory Confirmations.fromPreference() { - final preference = PreferencesUtils().get(PreferenceKey.confirmations); - - return preference != null - ? Confirmations.values.byName(preference) - : PreferenceKey.confirmations.defaultValue as Confirmations; - } } diff --git a/lib/utils/preferences/layout.dart b/lib/utils/preferences/enums/layout.dart similarity index 83% rename from lib/utils/preferences/layout.dart rename to lib/utils/preferences/enums/layout.dart index 2578b2f2..d972edf4 100644 --- a/lib/utils/preferences/layout.dart +++ b/lib/utils/preferences/enums/layout.dart @@ -2,14 +2,10 @@ import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; enum Layout { - list('list'), - grid('grid'), + list, + grid, ; - final String name; - - const Layout(this.name); - factory Layout.fromPreference() { final preference = PreferencesUtils().get(PreferenceKey.layout); diff --git a/lib/utils/preferences/sort_method.dart b/lib/utils/preferences/enums/sort_method.dart similarity index 79% rename from lib/utils/preferences/sort_method.dart rename to lib/utils/preferences/enums/sort_method.dart index fb29bb1b..e8a102e0 100644 --- a/lib/utils/preferences/sort_method.dart +++ b/lib/utils/preferences/enums/sort_method.dart @@ -2,15 +2,11 @@ import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; enum SortMethod { - date('date'), - title('title'), - ascending('ascending'), + date, + title, + ascending, ; - final String name; - - const SortMethod(this.name); - factory SortMethod.fromPreference() { final preference = PreferencesUtils().get(PreferenceKey.sortMethod); diff --git a/lib/utils/preferences/enums/swipe_action.dart b/lib/utils/preferences/enums/swipe_action.dart new file mode 100644 index 00000000..0eb3f891 --- /dev/null +++ b/lib/utils/preferences/enums/swipe_action.dart @@ -0,0 +1,45 @@ +import 'package:localmaterialnotes/utils/constants/constants.dart'; +import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; +import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; + +enum SwipeAction { + disabled, + delete, + pin, + ; + + factory SwipeAction.rightFromPreference() { + final preference = PreferencesUtils().get(PreferenceKey.swipeRightAction); + + return preference != null + ? SwipeAction.values.byName(preference) + : PreferenceKey.swipeRightAction.defaultValue as SwipeAction; + } + + factory SwipeAction.leftFromPreference() { + final preference = PreferencesUtils().get(PreferenceKey.swipeLeftAction); + + return preference != null + ? SwipeAction.values.byName(preference) + : PreferenceKey.swipeLeftAction.defaultValue as SwipeAction; + } + + bool get isEnabled { + return this != disabled; + } + + bool get isDisabled { + return this == disabled; + } + + String get title { + switch (this) { + case disabled: + return localizations.swipe_action_disabled; + case delete: + return localizations.swipe_action_delete; + case pin: + return localizations.swipe_action_pin; + } + } +} diff --git a/lib/utils/preferences/enums/swipe_direction.dart b/lib/utils/preferences/enums/swipe_direction.dart new file mode 100644 index 00000000..2766d49f --- /dev/null +++ b/lib/utils/preferences/enums/swipe_direction.dart @@ -0,0 +1,4 @@ +enum SwipeDirection { + right, + left, +} diff --git a/lib/utils/preferences/preference_key.dart b/lib/utils/preferences/preference_key.dart index 14ce7bbf..f145c887 100644 --- a/lib/utils/preferences/preference_key.dart +++ b/lib/utils/preferences/preference_key.dart @@ -1,7 +1,8 @@ -import 'package:localmaterialnotes/utils/preferences/confirmations.dart'; -import 'package:localmaterialnotes/utils/preferences/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/confirmations.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/layout.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/sort_method.dart'; +import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; -import 'package:localmaterialnotes/utils/preferences/sort_method.dart'; enum PreferenceKey { // Appearance @@ -9,16 +10,26 @@ enum PreferenceKey { theme(0), dynamicTheming(true), blackTheming(false), + showSeparators(false), + showTilesBackground(false), + + // Behavior + flagSecure(false), + confirmations(Confirmations.irreversible), + swipeRightAction(SwipeAction.delete), + swipeLeftAction(SwipeAction.pin), // Editor showUndoRedoButtons(true), showChecklistButton(true), showToolbar(true), + useParagraphsSpacing(true), - // Behavior - showSeparators(false), - showTilesBackground(false), - confirmations(Confirmations.irreversible), + // Backup + autoExportFrequency(0.0), + autoExportEncryption(false), + autoExportPassword(''), + lastAutoExportDate(''), // Notes sortMethod(SortMethod.date), @@ -41,4 +52,8 @@ enum PreferenceKey { return PreferencesUtils().get(this) ?? defaultValue as T; } + + Future getPreferenceOrDefaultSecure() async { + return await PreferencesUtils().getSecure(this) ?? defaultValue as String; + } } diff --git a/lib/utils/preferences/preferences_utils.dart b/lib/utils/preferences/preferences_utils.dart index bc44821d..81d35166 100644 --- a/lib/utils/preferences/preferences_utils.dart +++ b/lib/utils/preferences/preferences_utils.dart @@ -1,3 +1,4 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -11,9 +12,13 @@ class PreferencesUtils { PreferencesUtils._internal(); late final SharedPreferences _preferences; + late final FlutterSecureStorage _secureStorage; - Future init() async { + Future ensureInitialized() async { _preferences = await SharedPreferences.getInstance(); + _secureStorage = const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); } void set(String key, T value) { @@ -41,4 +46,24 @@ class PreferencesUtils { return _preferences.get(preferenceKey.name) as T?; } + + Future remove(PreferenceKey preferenceKey) async { + await _preferences.remove(preferenceKey.name); + } + + Future clear() async { + await _preferences.clear(); + } + + void setSecure(PreferenceKey preferenceKey, String value) { + _secureStorage.write(key: preferenceKey.name, value: value); + } + + Future getSecure(PreferenceKey preferenceKey) async { + return await _secureStorage.read(key: preferenceKey.name); + } + + Future deleteSecure(PreferenceKey preferenceKey) async { + await _secureStorage.delete(key: preferenceKey.name); + } } diff --git a/lib/utils/snack_bar_utils.dart b/lib/utils/snack_bar_utils.dart index dc6bc402..60166030 100644 --- a/lib/utils/snack_bar_utils.dart +++ b/lib/utils/snack_bar_utils.dart @@ -2,16 +2,24 @@ import 'package:flutter/material.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; class SnackBarUtils { - SnackBarUtils.info(String message) : text = message; + SnackBarUtils.info( + String message, { + this.duration = const Duration(seconds: 4), + }) : text = message; - SnackBarUtils.error(String error) : text = '${localizations.error_error}: $error'; + SnackBarUtils.error( + String error, { + this.duration = const Duration(seconds: 4), + }) : text = '${localizations.error_error}: $error'; final String text; + final Duration duration; void show({BuildContext? context}) { ScaffoldMessenger.of(context ?? navigatorKey.currentContext!).showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, + duration: duration, content: Text(text), ), ); diff --git a/lib/utils/theme_utils.dart b/lib/utils/theme_utils.dart index 23fbc583..d0ed533a 100644 --- a/lib/utils/theme_utils.dart +++ b/lib/utils/theme_utils.dart @@ -1,5 +1,6 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; +import 'package:localmaterialnotes/providers/notifiers.dart'; import 'package:localmaterialnotes/utils/constants/constants.dart'; import 'package:localmaterialnotes/utils/preferences/preference_key.dart'; import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart'; @@ -17,7 +18,7 @@ class ThemeUtils { late final bool isDynamicThemingAvailable; - Future init() async { + Future ensureInitialized() async { isDynamicThemingAvailable = await DynamicColorPlugin.getCorePalette() != null; } @@ -29,10 +30,6 @@ class ThemeUtils { return PreferenceKey.blackTheming.getPreferenceOrDefault(); } - Brightness get brightness { - return Theme.of(navigatorKey.currentContext!).brightness; - } - ThemeMode get themeMode { final themeModePreference = PreferenceKey.theme.getPreferenceOrDefault(); @@ -67,7 +64,7 @@ class ThemeUtils { final ColorScheme colorScheme; if (useDynamicTheming && lightDynamicColorScheme != null) { // TODO: remove when dynamic_colors is updated to support new roles - // cf. https://github.com/material-foundation/flutter-packages/issues/582 + // See https://github.com/material-foundation/flutter-packages/issues/582 final temporaryColorScheme = ColorScheme.fromSeed( seedColor: lightDynamicColorScheme.primary, ); @@ -95,7 +92,7 @@ class ThemeUtils { if (useDynamicTheming && darkDynamicColorScheme != null) { // TODO: remove when dynamic_colors is updated to support new roles - // cf. https://github.com/material-foundation/flutter-packages/issues/582 + // See https://github.com/material-foundation/flutter-packages/issues/582 final temporaryColorScheme = ColorScheme.fromSeed( brightness: Brightness.dark, seedColor: darkDynamicColorScheme.primary, @@ -104,7 +101,7 @@ class ThemeUtils { colorScheme = useBlackTheming ? darkDynamicColorScheme.copyWith( // TODO: remove when dynamic_colors is updated to support new roles - // cf. https://github.com/material-foundation/flutter-packages/issues/582 + // See https://github.com/material-foundation/flutter-packages/issues/582 // ignore: deprecated_member_use background: Colors.black, surface: Colors.black, diff --git a/pubspec.lock b/pubspec.lock index 07f07aa2..d6d8da62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" after_layout: dependency: "direct main" description: @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.3" ansicolor: dependency: transitive description: @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + url: "https://pub.dev" + source: hosted + version: "1.5.3" async: dependency: transitive description: @@ -245,26 +253,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" url: "https://pub.dev" source: hosted - version: "0.5.11" + version: "0.6.4" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "0d48e002438950f9582e574ef806b2bea5719d8d14c0f9f754fbad729bcf3b19" + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 url: "https://pub.dev" source: hosted - version: "0.5.14" + version: "0.6.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "2952837953022de610dacb464f045594854ced6506ac7f76af28d4a6490e189b" + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 url: "https://pub.dev" source: hosted - version: "0.5.14" + version: "0.6.3" dart_style: dependency: transitive description: @@ -285,18 +293,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + sha256: "93429694c9253d2871b3af80cf11b3cbb5c65660d402ed7bf69854ce4a089f82" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.1" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" diff_match_patch: dependency: transitive description: @@ -313,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.7.0" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" equatable: dependency: "direct main" description: @@ -345,6 +361,70 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "8bcc3af859e9d47fab9c7dc315537406511a894ab578e198bd8f9ed745ea5a01" + url: "https://pub.dev" + source: hosted + version: "0.5.1+2" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "38ebf91ecbcfa89a9639a0854ccaed8ab370c75678938eebca7d34184296f0bb" + url: "https://pub.dev" + source: hosted + version: "0.5.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" + url: "https://pub.dev" + source: hosted + version: "0.9.4+1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -353,19 +433,35 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flag_secure: + dependency: "direct main" + description: + name: flag_secure + sha256: b0562046bb9f23de17591f78632df0e3437ee98e708b1676c0af0c8cf9c3cccb + url: "https://pub.dev" + source: hosted + version: "2.0.0" fleather: dependency: "direct main" description: name: fleather - sha256: "00033c24bb8676a83727ceff691bcc27b26b68b54365921825070561ecdb2dd5" + sha256: f370c7d9c497ca4d6d74b0e6e47f01a5c4c040008767c1ee907021b1b18cf90c url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_hooks: dependency: "direct main" description: @@ -440,10 +536,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" flutter_riverpod: dependency: "direct main" description: @@ -452,6 +548,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_settings_ui: dependency: "direct main" description: @@ -514,10 +658,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554 + sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722 url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.2.2" graphs: dependency: transitive description: @@ -602,26 +746,26 @@ packages: dependency: "direct main" description: name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" + sha256: d06cca880885fe016d20a53847ce8605cde595e38de00a613b086fa8fd900790 + url: "https://pub.isar-community.dev" source: hosted - version: "3.1.0+1" + version: "3.1.7" isar_flutter_libs: dependency: "direct main" description: name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" + sha256: d5036c0cb92b15025301fb7260e7a7557393c855ed2f9903f2b1fbb56f423bc5 + url: "https://pub.isar-community.dev" source: hosted - version: "3.1.0+1" + version: "3.1.7" isar_generator: dependency: "direct dev" description: name: isar_generator - sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" - url: "https://pub.dev" + sha256: c88bcdfc4a6407ff9672364bf62330d0314e88753dd0b442179f2f802a5f2179 + url: "https://pub.isar-community.dev" source: hosted - version: "3.1.0+1" + version: "3.1.7" js: dependency: transitive description: @@ -714,10 +858,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: a2c78726048c755f0f90fd2b7c8799cd94338e2e9b7ab6498ae56503262c14bc + sha256: "43297fe7f7267b399ed6f2856ccd387775882445e9e695c427026070e3842b29" url: "https://pub.dev" source: hosted - version: "4.2762.0" + version: "4.2777.1" meta: dependency: transitive description: @@ -746,18 +890,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" parchment: dependency: transitive description: @@ -786,10 +930,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -854,6 +998,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: @@ -946,10 +1098,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6 + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.5.1" riverpod_annotation: dependency: "direct main" description: @@ -962,18 +1114,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "5b36ad2f2b562cffb37212e8d59390b25499bf045b732276e30a207b16a25f61" + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "70198738c3047ae4f6517ef1a2011a8514a980a52576c7f629a3a08810319a02" + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.10" rxdart: dependency: transitive description: @@ -994,82 +1146,74 @@ packages: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_storage: - dependency: "direct main" - description: - name: shared_storage - sha256: cf20428d06af065311b71e09cbfbbfe431e979a3bf9180001c1952129b7c708f + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "2.4.0" shelf: dependency: transitive description: @@ -1284,7 +1428,7 @@ packages: source: hosted version: "3.1.1" uuid: - dependency: "direct main" + dependency: transitive description: name: uuid sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" @@ -1351,10 +1495,10 @@ packages: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" xdg_directories: dependency: transitive description: @@ -1389,4 +1533,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.3 <4.0.0" - flutter: ">=3.22.2" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index e4323b09..4d1aea0c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,12 +2,12 @@ name: localmaterialnotes description: Simple, local, material design notes repository: https://github.com/maelchiotti/LocalMaterialNotes -version: 1.4.0+9 +version: 1.5.0+10 publish_to: none environment: sdk: ">=3.4.3 <4.0.0" - flutter: ^3.22.2 + flutter: ^3.22.0 dependencies: after_layout: ^1.2.0 @@ -15,61 +15,70 @@ dependencies: back_button_interceptor: ^7.0.3 collection: ^1.18.0 cupertino_icons: ^1.0.8 - device_info_plus: ^10.1.0 + device_info_plus: ^10.1.1 dynamic_color: ^1.7.0 + encrypt: ^5.0.3 equatable: ^2.0.5 - fleather: ^1.16.0 + file_selector: ^1.0.3 + flag_secure: ^2.0.0 + fleather: ^1.17.0 flutter: sdk: flutter + flutter_displaymode: ^0.6.0 flutter_hooks: ^0.20.5 flutter_keyboard_visibility: ^6.0.0 flutter_localizations: sdk: flutter - flutter_native_splash: ^2.4.0 + flutter_native_splash: ^2.4.1 flutter_riverpod: ^2.5.1 + flutter_secure_storage: ^9.2.2 flutter_settings_ui: ^3.0.1 flutter_staggered_grid_view: ^0.7.0 fuzzywuzzy: ^1.1.6 - go_router: ^14.2.0 + go_router: ^14.2.2 intl: ^0.19.0 is_first_run: ^1.0.0 - isar: ^3.1.0+1 - isar_flutter_libs: ^3.1.0+1 + isar: + version: ^3.1.7 + hosted: https://pub.isar-community.dev/ + isar_flutter_libs: + version: ^3.1.7 + hosted: https://pub.isar-community.dev/ json_annotation: ^4.9.0 locale_names: ^1.1.1 - material_symbols_icons: ^4.2762.0 - package_info_plus: ^8.0.0 + material_symbols_icons: ^4.2777.1 + package_info_plus: ^8.0.1 path: ^1.9.0 - path_provider: ^2.1.3 + path_provider: ^2.1.4 quick_actions: ^1.0.7 receive_sharing_intent: ^1.8.0 restart_app: ^1.2.1 riverpod_annotation: ^2.3.5 sanitize_filename: ^1.0.5 - share_plus: ^9.0.0 - shared_preferences: ^2.2.3 - shared_storage: ^0.8.1 + share_plus: ^10.0.0 + shared_preferences: ^2.3.1 simple_icons: ^10.1.3 url_launcher: ^6.3.0 - uuid: ^4.4.0 yaml: ^3.1.2 dev_dependencies: build_runner: ^2.4.11 - custom_lint: ^0.5.11 + custom_lint: ^0.6.4 # TODO: replace when the monochrome feature is released on pub.dev - # (cf. https://github.com/fluttercommunity/flutter_launcher_icons/issues/555) + # See https://github.com/fluttercommunity/flutter_launcher_icons/issues/555 flutter_launcher_icons: git: url: https://github.com/fluttercommunity/flutter_launcher_icons ref: master flutter_test: sdk: flutter - isar_generator: ^3.1.0+1 + isar_generator: + version: ^3.1.7 + hosted: https://pub.isar-community.dev/ json_serializable: ^6.8.0 lint: ^2.3.0 - riverpod_generator: ^2.3.3 - riverpod_lint: ^2.1.1 + riverpod_generator: ^2.4.0 + riverpod_lint: ^2.3.10 flutter: uses-material-design: true