diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 56b2e83e..fbfa3eae 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -24,7 +24,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.3" + flutter-version: "3.22.2" channel: 'stable' cache: true diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 2f5a3d9b..6116d902 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -25,7 +25,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.3" + flutter-version: "3.22.2" channel: 'stable' cache: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58d08362..7827ccf8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.19.3' # or, you can use 1.22 + flutter-version: '3.22.2' # or, you can use 1.22 channel: 'stable' cache: true - run: flutter test --coverage diff --git a/android/.gitignore b/android/.gitignore index 6f568019..48f3d4c5 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,4 +1,4 @@ -gradle-wrapper.jar +# gradle-wrapper.jar /.gradle /captures/ /gradlew diff --git a/android/app/build.gradle b/android/app/build.gradle index 08ef8d71..36c63997 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -115,6 +115,8 @@ android { } } + + flutter { source '../..' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8f9718c..161e27e9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,21 +5,21 @@ android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - - - - - - + + android:name="android.permission.SCHEDULE_EXACT_ALARM"/> + + + + + + - + + + - - - + + + Merkmale +
    +
  • Moderner und benutzerfreundlicher Interface
  • +
+

Wecker

+
    +
  • Anpassbare Zeitpläne (täglich, wöchentlich, bestimmte Wochentage, bestimmte Daten, Datumsbereich)
  • +
  • Anpassbare Melodie, ansteigende Lautstärke und Vibrationen
  • +
  • Anpassbare Schlummerdauer und maximale Schlummeranzahl
  • +
  • Wecker-Aufgaben (Matheaufgaben, Text neu eingeben, Sequenz, weitere folgen)
  • +
  • Wecker filtern (alle, heute, morgen, pausiert, deaktiviert, abgeschlossen)
  • +
+

Uhr

+
    +
  • Anpassbare Anzeige der Uhrzeit
  • +
  • Weltuhren mit relativer Zeitdifferenz
  • +
  • Suchen und hinzufügen von Städten
  • +
+

Timer

+
    +
  • Anpassbare Melodie, ansteigende Lautstärke und Vibrationen
  • +
  • Timer-Voreinstellungen
  • +
  • Timer filtern (alle, aktiv, pausiert, gestoppt)
  • +
+

Stoppuhr

+
    +
  • Rundenverlauf mit Rundenzeiten und vergangenen Zeiten
  • +
  • Vergleich von Runden
  • +
+

Darstellung

+
    +
  • Material You Thema
  • +
  • Stark anpassbare Farbthemen
  • +
  • Stark anpassbare Stilthemen
  • +
diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt new file mode 100644 index 00000000..56c31f54 --- /dev/null +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -0,0 +1 @@ +Moderne und funktionsreich Uhr, Wecker, Timer und Stoppuhr. diff --git a/fastlane/metadata/android/en-US/changelogs/101.txt b/fastlane/metadata/android/en-US/changelogs/101.txt deleted file mode 100644 index fbe06977..00000000 --- a/fastlane/metadata/android/en-US/changelogs/101.txt +++ /dev/null @@ -1,4 +0,0 @@ -🐛 Fixes - -* Fix app crashing after updating from 0.2.10 - diff --git a/fastlane/metadata/android/en-US/changelogs/102.txt b/fastlane/metadata/android/en-US/changelogs/102.txt deleted file mode 100644 index fbe06977..00000000 --- a/fastlane/metadata/android/en-US/changelogs/102.txt +++ /dev/null @@ -1,4 +0,0 @@ -🐛 Fixes - -* Fix app crashing after updating from 0.2.10 - diff --git a/fastlane/metadata/android/en-US/changelogs/103.txt b/fastlane/metadata/android/en-US/changelogs/103.txt deleted file mode 100644 index fbe06977..00000000 --- a/fastlane/metadata/android/en-US/changelogs/103.txt +++ /dev/null @@ -1,4 +0,0 @@ -🐛 Fixes - -* Fix app crashing after updating from 0.2.10 - diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt new file mode 100644 index 00000000..ecf6e5f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -0,0 +1,14 @@ +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Go to General > Display > Reliability > Show Foreground Notification +* Added current lap card for stopwatch. Now you can view the current lap time in real time. +* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm. +* Changed timer and stopwatch notification so time appears in title +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically getting disabled +* Fixed timer dismiss actions being mapped incorrectly +* Fixed timer add length not working correctly after timer rings + diff --git a/fastlane/metadata/android/en-US/changelogs/252.txt b/fastlane/metadata/android/en-US/changelogs/252.txt new file mode 100644 index 00000000..ecf6e5f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/252.txt @@ -0,0 +1,14 @@ +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Go to General > Display > Reliability > Show Foreground Notification +* Added current lap card for stopwatch. Now you can view the current lap time in real time. +* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm. +* Changed timer and stopwatch notification so time appears in title +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically getting disabled +* Fixed timer dismiss actions being mapped incorrectly +* Fixed timer add length not working correctly after timer rings + diff --git a/fastlane/metadata/android/en-US/changelogs/253.txt b/fastlane/metadata/android/en-US/changelogs/253.txt new file mode 100644 index 00000000..ecf6e5f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/253.txt @@ -0,0 +1,14 @@ +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Go to General > Display > Reliability > Show Foreground Notification +* Added current lap card for stopwatch. Now you can view the current lap time in real time. +* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm. +* Changed timer and stopwatch notification so time appears in title +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically getting disabled +* Fixed timer dismiss actions being mapped incorrectly +* Fixed timer add length not working correctly after timer rings + diff --git a/fastlane/metadata/android/en-US/changelogs/261.txt b/fastlane/metadata/android/en-US/changelogs/261.txt new file mode 100644 index 00000000..1c4c99e9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/261.txt @@ -0,0 +1,32 @@ +This is a beta release. Please report any issues via GitHub or email. + +🚀 Features + +* Added option to select directory for ringtones (random ringtone will be selected from the directory each time) +* Added multiselect for lists +* Added option to shuffle alarm ringtone +* Added backup and restore for alarms, timers, themes etc. +* Added numpad input for timers +* Added option to reduce volume while solving alarm tasks +* Added quick home screen actions for alarms and timers +* Added option to start ringtone at random position +* Added background service to keep app alive +* Added analog clock to clock tab + +✨ Enhancements + +* Made alarm tasks reorderable +* Added better logging system +* Added alarm labels to alarm notifications + +🐛 Fixes + +* Fixed non-deletable items getting deleted by list actions +* Fixed range weekly schedule not working +* Fixed system navigation bar color +* Fixed database for cities +* Fixed skipped alarms being visible to the system +* Fixed foreground notification foreground type +* Fixed date picker being stuck in the past for range alarms + + diff --git a/fastlane/metadata/android/en-US/changelogs/262.txt b/fastlane/metadata/android/en-US/changelogs/262.txt new file mode 100644 index 00000000..1c4c99e9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/262.txt @@ -0,0 +1,32 @@ +This is a beta release. Please report any issues via GitHub or email. + +🚀 Features + +* Added option to select directory for ringtones (random ringtone will be selected from the directory each time) +* Added multiselect for lists +* Added option to shuffle alarm ringtone +* Added backup and restore for alarms, timers, themes etc. +* Added numpad input for timers +* Added option to reduce volume while solving alarm tasks +* Added quick home screen actions for alarms and timers +* Added option to start ringtone at random position +* Added background service to keep app alive +* Added analog clock to clock tab + +✨ Enhancements + +* Made alarm tasks reorderable +* Added better logging system +* Added alarm labels to alarm notifications + +🐛 Fixes + +* Fixed non-deletable items getting deleted by list actions +* Fixed range weekly schedule not working +* Fixed system navigation bar color +* Fixed database for cities +* Fixed skipped alarms being visible to the system +* Fixed foreground notification foreground type +* Fixed date picker being stuck in the past for range alarms + + diff --git a/fastlane/metadata/android/en-US/changelogs/263.txt b/fastlane/metadata/android/en-US/changelogs/263.txt new file mode 100644 index 00000000..0d55031d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/263.txt @@ -0,0 +1,33 @@ +This is a beta release. Please report any issues via GitHub or email. + +🚀 Features + +* Added option to select directory for ringtones (random ringtone will be selected from the directory each time) +* Added multiselect for lists +* Added option to shuffle alarm ringtone +* Added backup and restore for alarms, timers, themes etc. +* Added numpad input for timers +* Added option to reduce volume while solving alarm tasks +* Added quick home screen actions for alarms and timers +* Added option to start ringtone at random position +* Added background service to keep app alive +* Added analog clock to clock tab + +✨ Enhancements + +* Made alarm tasks reorderable +* Added better logging system +* Added alarm labels to alarm notifications +* Updated translations + +🐛 Fixes + +* Fixed non-deletable items getting deleted by list actions +* Fixed range weekly schedule not working +* Fixed system navigation bar color +* Fixed database for cities +* Fixed skipped alarms being visible to the system +* Fixed foreground notification foreground type +* Fixed date picker being stuck in the past for range alarms + + diff --git a/fastlane/metadata/android/en-US/changelogs/91.txt b/fastlane/metadata/android/en-US/changelogs/91.txt deleted file mode 100644 index 531da423..00000000 --- a/fastlane/metadata/android/en-US/changelogs/91.txt +++ /dev/null @@ -1,7 +0,0 @@ -✨ Enhancements - -* Add Material You colors and styles -* Add system dark mode -* Add Material 3 compliant navigation bar and style -* Add adaptive launcher icon with Material You support -* Change app icon diff --git a/fastlane/metadata/android/en-US/changelogs/92.txt b/fastlane/metadata/android/en-US/changelogs/92.txt deleted file mode 100644 index 531da423..00000000 --- a/fastlane/metadata/android/en-US/changelogs/92.txt +++ /dev/null @@ -1,7 +0,0 @@ -✨ Enhancements - -* Add Material You colors and styles -* Add system dark mode -* Add Material 3 compliant navigation bar and style -* Add adaptive launcher icon with Material You support -* Change app icon diff --git a/fastlane/metadata/android/en-US/changelogs/93.txt b/fastlane/metadata/android/en-US/changelogs/93.txt deleted file mode 100644 index 531da423..00000000 --- a/fastlane/metadata/android/en-US/changelogs/93.txt +++ /dev/null @@ -1,7 +0,0 @@ -✨ Enhancements - -* Add Material You colors and styles -* Add system dark mode -* Add Material 3 compliant navigation bar and style -* Add adaptive launcher icon with Material You support -* Change app icon diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 4270a365..3440bdeb 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -4,10 +4,10 @@

Alarms

    -
  • Customizable schedules (Daily, Weekly, Specific week days, Specific dates, Date range)
  • -
  • Configure Melody, rising volume and vibrations
  • -
  • Configure Snooze length and max snoozes
  • -
  • Alarm tasks (Math problems, Retype text, Sequence, more to come)
  • +
  • Customizable schedules (daily, weekly, specific week days, specific dates, date range)
  • +
  • Configure melody, rising volume and vibrations
  • +
  • Configure snooze length and max snoozes
  • +
  • Alarm tasks (math problems, retype text, sequence, more to come)
  • Filter alarms (all, today, tomorrow, snoozed, disabled, completed)

Clock

@@ -18,7 +18,7 @@

Timer

    -
  • Configure Melody, rising volume and vibrations
  • +
  • Configure melody, rising volume and vibrations
  • Timer presets
  • Filter timers (all, running, paused, stopped)
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index b03b9d5c..7b7984cb 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -A modern and powerful clock, alarms, timer and stopwatch app for Android! +Modern and powerful clock, alarms, timer and stopwatch. diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 00000000..d0f8238e --- /dev/null +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,35 @@ +

Funciones

+
    +
  • Interfaz moderna y fácil de usar
  • +
+

Alarmas

+
    +
  • Horarios personalizables (diarios, semanales, días específicos de la semana, fechas específicas, rango de fechas)
  • +
  • Configura la melodía, el aumento del volumen y las vibraciones
  • +
  • Configurar la duración de la repetición y las repeticiones máximas
  • +
  • Tareas de alarma (problemas matemáticos, volver a escribir texto, secuencia, más por venir)
  • +
  • Filtrar alarmas (todas, hoy, mañana, pospuestas, desactivadas, completadas)
  • +
+

Reloj

+
    +
  • Pantalla de reloj personalizable
  • +
  • Relojes mundiales con diferencia horaria relativa
  • +
  • Buscar y añadir ciudades
  • +
+

Temporizador

+
    +
  • Configura la melodía, el aumento del volumen y las vibraciones
  • +
  • Ajustes preestablecidos del temporizador
  • +
  • Temporizadores de filtro (todos, en ejecución, en pausa, detenidos)
  • +
+

Cronómetro

+
    +
  • Historial de vueltas con tiempos de vuelta y tiempos transcurridos
  • +
  • Comparaciones de vueltas
  • +
Apariencia +
    +
  • Temas Material You
  • +
  • Temas de color altamente personalizables
  • +
  • Temas de estilo altamente personalizables
  • +
diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 00000000..62c625cc --- /dev/null +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Reloj, alarmas, cronómetro y cronómetro moderno y potente. diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 00000000..fdd3d856 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,35 @@ +

Fonctionnalités

+
    +
  • Interface moderne et facile à utiliser
  • +
+

Alarmes

+
    +
  • Programmes personnalisables (quotidiens, hebdomadaires, jours spécifiques, dates spécifiques, plage de dates)
  • +
  • Configurer la mélodie, l'augmentation du volume et les vibrations
  • +
  • Configurer la durée et le nombre maximal de répétitions
  • +
  • Tâches d'alarme (problèmes mathématiques, retapez du texte, séquence, etc.)
  • +
  • Filtrer les alarmes (toutes, aujourd'hui, demain, répétées, désactivées, terminées)
  • +
+

Horloge

+
    +
  • Affichage de l'horloge personnalisable
  • +
  • Horloges mondiales avec décalage horaire relatif
  • +
  • Rechercher et ajouter des villes
  • +
+

Minuterie

+
    +
  • Configurer la mélodie, l'augmentation du volume et les vibrations
  • +
  • Préréglages de minuterie
  • +
  • Filtrer les minuteurs (tous, en cours d'exécution, en pause, arrêtés)
  • +
+

Chronomètre

+
    +
  • Historique des tours avec temps du tour et temps écoulés
  • +
  • Comparaisons de tours
  • +
Apparence +
    +
  • Thèmes Material You
  • +
  • Thèmes de couleurs personnalisables
  • +
  • Thèmes de style personnalisables
  • +
diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 00000000..1f556399 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Horloge, alarmes, minuterie et chronomètre modernes et puissants. diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt new file mode 100644 index 00000000..65974a1f --- /dev/null +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -0,0 +1,35 @@ +

Funzionalità

+
    +
  • Interfaccia moderna e facile da usare
  • +
+

Allarmi

+
    +
  • Pianificazioni personalizzabili (giornaliere, settimanali, giorni della settimana specifici, date specifiche, intervalli di date)
  • +
  • Configura la melodia, l'aumento del volume e le vibrazioni
  • +
  • Configurare la lunghezza e la durata massima delle ripetizioni
  • +
  • Attività per le allarmi (problemi matematici, riscrivere testo, sequenze, e altro in arrivo)
  • +
  • Filtra gli allarmi (tutti, oggi, domani, posticipati, disabilitati, completati)
  • +
+

Orologio

+
    +
  • Schermata dell'orologio personalizzabile
  • +
  • Orologi di tutto il mondo con differenza di fuso orario relativo
  • +
  • Cercare e aggiungere città
  • +
+

Temporizzatore

+
    +
  • Configura la melodia, l'aumento del volume e le vibrazioni
  • +
  • Preimpostazioni del temporizzatore
  • +
  • Filtro del temporizzatore(tutti, in esecuzione, in pausa, interrotti)
  • +
+

Cronometro

+
    +
  • Cronologia dei giri con i tempi del giro e i tempi trascorsi
  • +
  • Comparazioni dei giri
  • +
Aspetto +
    +
  • Temi di Material You
  • +
  • Temi di colore altamente personalizzabili
  • +
  • Temi di stile altamente personalizzabili
  • +
diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt new file mode 100644 index 00000000..7fdc4b36 --- /dev/null +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -0,0 +1 @@ +Potente e moderno orologio, allarmi, temporizzatore e cronometro. diff --git a/fastlane/metadata/android/no-NO/full_description.txt b/fastlane/metadata/android/no-NO/full_description.txt new file mode 100644 index 00000000..0b14d8c3 --- /dev/null +++ b/fastlane/metadata/android/no-NO/full_description.txt @@ -0,0 +1,35 @@ +

Funksjoner

+
    +
  • Moderne og lettfattelig grensesnitt
  • +
+

Alarmer

+
    +
  • Tilpassbare timeplaner, (daglig, ukentlig, spesifikke ukedager, spesifikke dager, datofølge)
  • +
  • Velg melodi, økende lydstyrke, og vibrasjon
  • +
  • Slumringsvarighet, og maks. antall slumringer
  • +
  • Alarmgjøremål (matematikkproblemer, gjenskriving av tekst, sekvensering, og andre planlagte ting)
  • +
  • Filtrering av alarmer, (alle, i dag, i morgen, slumrede, avskrudde, fullførte)
  • +
+

Klokke

+
    +
  • Tilpassbar klokkevisning
  • +
  • Verdensklokke med relativ tidsforskyvelse
  • +
  • Søk etter og legg til byer
  • +
+

Tidsur

+
    +
  • Velg melodi, økende lydstyrke, og vibrasjon
  • +
  • Tidsurforhåndsstillinger
  • +
  • Filtrering av tidsur (alle, kjørende, pausede, stoppede)
  • +
+

Stoppeklokke

+
    +
  • Rundehistorikk med rundetider og forløpt tid
  • +
  • Rundesammenligninger
  • +
Utseende +
    +
  • Drakt av materiell deig
  • +
  • Tilpassbar fargepalett
  • +
  • Tilpassbare stilvalg
  • +
diff --git a/fastlane/metadata/android/no-NO/short_description.txt b/fastlane/metadata/android/no-NO/short_description.txt new file mode 100644 index 00000000..0c1f2c1e --- /dev/null +++ b/fastlane/metadata/android/no-NO/short_description.txt @@ -0,0 +1 @@ +Moderne og kraftig ur, alarmklokke, tidsur, og stoppeklokke. diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt new file mode 100644 index 00000000..95a07d23 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -0,0 +1,35 @@ +

Funkcje

+
    +
  • Nowoczesny i łatwy w użyciu interfejs
  • +
+

Alarmy

+
    +
  • Konfigurowalne harmonogramy (dzienny, tygodniowy, na określone dni tygodnia, na określone daty, zakres dat)
  • +
  • Konfiguracja melodii, rosnącej głośności i wibracji
  • +
  • Konfiguracja długości drzemki i maksymalnej liczby drzemek
  • +
  • Zadania pobudkowe (działania matematyczne, przepisywanie tekstu, sekwencja, kolejne w przyszłości)
  • +
  • Filtrowanie alarmów (wszystkie, dzisiejsze, jutrzejsze, odłożone, wyłączone, zakończone)
  • +
+

Zegar

+
    +
  • Konfigurowalny wyświetlanie zegara
  • +
  • Zegary światowe ze względną różnicą czasu
  • +
  • Wyszukiwanie i dodawanie miast
  • +
+

Czasomierz

+
    +
  • Konfiguracja melodii, rosnącej głośności i wibracji
  • +
  • Wstępne ustawienia czasomierza
  • +
  • Filtrowanie ustawionych minutników (wszystkie, uruchomione, wstrzymane, zatrzymane)
  • +
+

Stoper

+
    +
  • Historia okrążeń z czasami okrążeń i czasami, które łącznie upłynęły przy danych okrążeniach
  • +
  • Porównania okrążeń
  • +
Wygląd +
    +
  • Motywy Material You
  • +
  • Wysoce konfigurowalne motywy kolorystyczne
  • +
  • Wysoce konfigurowalne motywy stylów
  • +
diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt new file mode 100644 index 00000000..9460d702 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -0,0 +1 @@ +Nowoczesny i potężny zegar, budzik, minutnik i stoper. diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt new file mode 100644 index 00000000..2f0f305a --- /dev/null +++ b/fastlane/metadata/android/pt/short_description.txt @@ -0,0 +1 @@ +Relógio, alarme, temporizador e cronómetro moderno e poderoso. 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..9c5a0ca4 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,35 @@ +

Возможности

+
    +
  • Современный и простой в использовании интерфейс
  • +
+

Будильники

+
    +
  • Настраиваемые расписания (ежедневные, еженедельные, определенные дни недели, конкретные даты, диапазон дат)
  • +
  • Настройка мелодии, нарастающей громкости и вибрации
  • +
  • Настройка длительности и максимального количества повторов режима отложить
  • +
  • Задачи для будильника (решение математических примеров, ввод текста, последовательности и др.)
  • +
  • Фильтрация будильников (все, сегодня, завтра, отложенные, отключенные, выполненные)
  • +
+

Часы

+
    +
  • Настраиваемый вид циферблата
  • +
  • Мировые часы с отображением разницы во времени
  • +
  • Поиск и добавление городов
  • +
+

Таймер

+
    +
  • Настройка мелодии, нарастающей громкости и вибрации
  • +
  • Предустановки таймера
  • +
  • Фильтрация таймеров (все, работающие, на паузе, остановленные)
  • +
+

Секундомер

+
    +
  • История кругов с временем круга и общим временем
  • +
  • Сравнение кругов
  • +
Внешний вид +
    +
  • Темы Material You
  • +
  • Широкие возможности настройки цветовых тем
  • +
  • Широкие возможности настройки тем стиля
  • +
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..4aa6e86d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Современные и многофункциональные часы, будильник, таймер и секундомер. diff --git a/fastlane/metadata/android/tr-TR/full_description.txt b/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 00000000..980c8d3d --- /dev/null +++ b/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,35 @@ +

Özellikler

+
    +
  • Modern ve kullanımı kolay arayüz
  • +
+

Alarm

+
    +
  • Özelleştirilebilir programlar (günlük, haftalık, haftanın belirli günleri, belirli tarihler, tarih aralığı)
  • +
  • Melodiyi, artan ses seviyesini ve titreşimleri yapılandırma
  • +
  • Erteleme uzunluğunu ve maksimum erteleme sayısını yapılandırma
  • +
  • Görev tanımlı alarmlar (matematik problemleri, metni yeniden yazma, sıralama, dahası gelecek)
  • +
  • Alarmları filtreleme (tümü, bugün, yarın, ertelendi, devre dışı bırakıldı, tamamlandı)
  • +
+

Saat

+
    +
  • Özelleştirilebilir saat ekranı
  • +
  • Göreceli zaman farkı olan dünya saatleri
  • +
  • Şehirleri ara ve ekle
  • +
+

Zamanlayıcı

+
    +
  • Melodiyi, artan ses seviyesini ve titreşimleri yapılandırma
  • +
  • Zamanlayıcı ön ayarları
  • +
  • Zamanlayıcıları filtreleme (tümü, çalışıyor, duraklatıldı, durduruldu)
  • +
+

Kronometre

+
    +
  • Tur süreleri ve geçen sürelerle birlikte tur geçmişi
  • +
  • Tur karşılaştırmaları
  • +
Görünüm +
    +
  • Materyal You temaları
  • +
  • Son derece özelleştirilebilir renk temaları
  • +
  • Son derece özelleştirilebilir tarz temaları
  • +
diff --git a/fastlane/metadata/android/tr-TR/short_description.txt b/fastlane/metadata/android/tr-TR/short_description.txt new file mode 100644 index 00000000..7fd178a8 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/short_description.txt @@ -0,0 +1 @@ +Modern ve güçlü saat, alarm, zamanlayıcı ve kronometre. diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt new file mode 100644 index 00000000..a54ea3cd --- /dev/null +++ b/fastlane/metadata/android/uk/full_description.txt @@ -0,0 +1,35 @@ +

Функції

+
    +
  • Сучасний і простий у використанні інтерфейс
  • +
+

Будильники

+
    +
  • Настроювані розклади (щодня, щотижня, конкретні дні тижня, конкретні дати, діапазон дат)
  • +
  • Налаштувати мелодію, підвищення гучності та вібрацію
  • +
  • Налаштувати тривалість затримки та максимальну кількість затримок
  • +
  • Завдання для пробудження (математичні задачі, повторне введення тексту, послідовність, ще буде)
  • +
  • Фільтр будильників (усі, сьогодні, завтра, відкладені, вимкнені, завершені)
  • +
+

Годинник

+
    +
  • Настроюваний дисплей годинника
  • +
  • Світовий годинник із відносною різницею в часі
  • +
  • Пошук і додавання міст
  • +
+

Таймер

+
    +
  • Налаштувати мелодію, збільшення гучності та вібрація
  • +
  • Попередні налаштування таймера
  • +
  • Фільтри таймерів (усі, запущені, призупинені, зупинені)
  • +
+

Секундомір

+
    +
  • "Історія кіл з часами кіл та загальним часом
  • +
  • Порівняння кіл
  • +
Зовнішній вигляд +
    +
  • Теми Material You
  • +
  • Налаштовувані кольорові теми
  • +
  • Налаштовуваніі теми стилю
  • +
diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt new file mode 100644 index 00000000..532404fe --- /dev/null +++ b/fastlane/metadata/android/uk/short_description.txt @@ -0,0 +1 @@ +Сучасний і потужний годинник, будильник, таймер і секундомір. diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 00000000..b48f2c9f --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,35 @@ +

Tính năng

+
    +
  • Giao diện hiện đại và dễ sử dụng
  • +
+

Báo thức

+
    +
  • Lịch trình có thể tùy chỉnh (hàng ngày, hàng tuần, các ngày cụ thể trong tuần, ngày cụ thể, phạm vi ngày)
  • +
  • Định cấu hình giai điệu, tăng âm lượng và rung
  • +
  • Định cấu hình thời lượng hoãn và thời gian hoãn tối đa
  • +
  • Nhiệm vụ báo thức (bài toán, gõ lại văn bản, trình tự, nhiều nội dung khác sắp tới)
  • +
  • Bộ lọc báo thức (tất cả, hôm nay, ngày mai, đã hoãn, đã tắt, đã hoàn thành)
  • +
+

Đồng hồ

+
    +
  • Hiển thị đồng hồ có thể tùy chỉnh
  • +
  • Đồng hồ thế giới có chênh lệch thời gian tương đối
  • +
  • Tìm kiếm và thêm thành phố
  • +
+

Bộ hẹn giờ

+
    +
  • Định cấu hình giai điệu, tăng âm lượng và rung
  • +
  • Cài đặt trước bộ hẹn giờ
  • +
  • Lọc bộ hẹn giờ (tất cả, đang chạy, đã tạm dừng, đã dừng)
  • +
+

Đồng hồ bấm giờ

+
    +
  • Lịch sử vòng chạy với thời gian vòng chạy và thời gian đã trôi qua
  • +
  • So sánh vòng
  • +
Diện mạo +
    +
  • Chủ đề của Material You
  • +
  • Chủ đề màu sắc có khả năng tùy chỉnh cao
  • +
  • Chủ đề phong cách có khả năng tùy chỉnh cao
  • +
diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 00000000..9dec655b --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Đồng hồ, báo thức, hẹn giờ và đồng hồ bấm giờ hiện đại và mạnh mẽ. diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt new file mode 100644 index 00000000..4f597552 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -0,0 +1,35 @@ +

功能

+
    +
  • 现代易用的界面
  • +
+

闹钟

+
    +
  • 可自定义的时间计划(每天,每周,特定星期与日期,日期范围)
  • +
  • 设置铃声、渐响音量与振动
  • +
  • 配置贪睡时长与最大次数
  • +
  • 闹钟任务(数学题,打字,数列及更多)
  • +
  • 筛选闹钟(所有,今天,明天,稍后再响,已禁用,已完成)
  • +
+

时钟

+
    +
  • 可自定义的时钟显示
  • +
  • 显示相对时差的世界时钟
  • +
  • 搜索并添加城市
  • +
+

计时器

+
    +
  • 设置铃声、渐响音量与振动
  • +
  • 计时器预设
  • +
  • 筛选计时器(所有,进行中,已暂停,已停止)
  • +
+

秒表

+
    +
  • 显示每圈用时与总用时
  • +
  • 圈与圈比较
  • +
外观 +
    +
  • Material You 主题
  • +
  • 高度可定制的颜色主题
  • +
  • 高度可定制的样式主题
  • +
diff --git a/fastlane/metadata/android/zh-CN/short_description.txt b/fastlane/metadata/android/zh-CN/short_description.txt new file mode 100644 index 00000000..0a565e10 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/short_description.txt @@ -0,0 +1 @@ +时尚又强大的时钟,闹钟,计时器和秒表应用。 diff --git a/lib/settings/data/alarm_app_settings_schema.dart b/lib/alarm/data/alarm_app_settings_schema.dart similarity index 78% rename from lib/settings/data/alarm_app_settings_schema.dart rename to lib/alarm/data/alarm_app_settings_schema.dart index 89a3a6c3..46214132 100644 --- a/lib/settings/data/alarm_app_settings_schema.dart +++ b/lib/alarm/data/alarm_app_settings_schema.dart @@ -1,5 +1,6 @@ import 'package:clock_app/alarm/data/alarm_settings_schema.dart'; import 'package:clock_app/alarm/types/notification_action.dart'; +import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/notifications/widgets/notification_actions/area_notification_action.dart'; import 'package:clock_app/notifications/widgets/notification_actions/buttons_notification_action.dart'; @@ -73,10 +74,37 @@ SettingGroup alarmAppSettingsSchema = SettingGroup( ]), SettingGroup("Filters", (context) => AppLocalizations.of(context)!.filtersSettingGroup, [ + // CustomizableListSetting( + // "Tasks", + // (context) => AppLocalizations.of(context)!.tasksSetting, + // [], + // // kDebugMode + // // ? [AlarmTask(AlarmTaskType.math), AlarmTask(AlarmTaskType.sequence)] + // // : [], + // alarmTaskSchemasMap.keys.map((key) => AlarmTask(key)).toList(), + // addCardBuilder: (item) => AlarmTaskCard(task: item, isAddCard: true), + // cardBuilder: (item, [onDelete, onDuplicate]) => AlarmTaskCard( + // task: item, + // isAddCard: false, + // onPressDelete: onDelete, + // onPressDuplicate: onDuplicate, + // ), + // valueDisplayBuilder: (context, setting) { + // return Text("${setting.value.length} tasks"); + // }, + // itemPreviewBuilder: (item) => TryAlarmTaskButton(alarmTask: item), + // // onChange: (context, value)async{ + // // await appSettings.save(); + // // } + // ), + SwitchSetting("Show Filters", (context) => AppLocalizations.of(context)!.showFiltersSetting, true), SwitchSetting("Show Sort", (context) => AppLocalizations.of(context)!.showSortSetting, true), + SwitchSetting("Show Next Alarm", + (context) => AppLocalizations.of(context)!.showNextAlarm, false), + ]), SettingGroup( "Notifications", diff --git a/lib/alarm/data/alarm_events_sort_options.dart b/lib/alarm/data/alarm_events_sort_options.dart new file mode 100644 index 00000000..fcbd93a6 --- /dev/null +++ b/lib/alarm/data/alarm_events_sort_options.dart @@ -0,0 +1,27 @@ +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/alarm/types/alarm_event.dart'; +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +final List> alarmEventSortOptions = [ + ListSortOption((context) => "Earlies start date", sortStartDateAscending), + ListSortOption((context) => "Latest start date", sortStartDateDescending), + ListSortOption((context) => "Earlies event date", sortEventDateAscending), + ListSortOption((context) => "Latest event date", sortEventDateDescending), +]; + +int sortStartDateAscending(AlarmEvent a, AlarmEvent b) { + return a.startDate.compareTo(b.startDate); +} + +int sortStartDateDescending(AlarmEvent a, AlarmEvent b) { + return b.startDate.compareTo(a.startDate); +} + +int sortEventDateAscending(AlarmEvent a, AlarmEvent b) { + return a.eventTime.compareTo(b.eventTime); +} + +int sortEventDateDescending(AlarmEvent a, AlarmEvent b) { + return b.eventTime.compareTo(a.eventTime); +} diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index 84e19fe0..ffb055b5 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -10,6 +10,7 @@ import 'package:clock_app/alarm/types/schedules/weekly_alarm_schedule.dart'; import 'package:clock_app/alarm/widgets/alarm_task_card.dart'; import 'package:clock_app/alarm/widgets/try_alarm_task_button.dart'; import 'package:clock_app/audio/audio_channels.dart'; +import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/logic/tags.dart'; @@ -19,7 +20,6 @@ import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/types/weekday.dart'; import 'package:clock_app/common/utils/ringtones.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/screens/ringtones_screen.dart'; import 'package:clock_app/settings/screens/tags_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; @@ -77,20 +77,6 @@ SettingGroup alarmSettingsSchema = SettingGroup( ), ], ), - // DynamicToggleSetting( - // "Week Days", - // (context) => AppLocalizations.of(context)!.alarmWeekdaysSetting, - // () { - // return weekdays - // .map((weekday) => SelectSettingOption( - // (context) => weekday.getAbbreviation(context), weekday)) - // .toList(); - // }, - // enableConditions: [ - // ValueCondition(["Type"], (value) => value == WeeklyAlarmSchedule) - // ], - // ), - ToggleSetting( "Week Days", (context) => AppLocalizations.of(context)!.alarmWeekdaysSetting, @@ -194,13 +180,12 @@ SettingGroup alarmSettingsSchema = SettingGroup( ], // shouldCloseOnSelect: false, ), - SelectSetting( - "Audio Channel", - (context) => AppLocalizations.of(context)!.audioChannelSetting, - audioChannelOptions, - onChange: (context, index) { - RingtonePlayer.stop(); - }, + SwitchSetting( + "start_melody_at_random_pos", + (context) => AppLocalizations.of(context)!.startMelodyAtRandomPos, + false, + getDescription: (context) => AppLocalizations.of(context)! + .startMelodyAtRandomPosDescription, ), SliderSetting( "Volume", @@ -209,6 +194,15 @@ SettingGroup alarmSettingsSchema = SettingGroup( 100, 100, unit: "%"), + SliderSetting( + "task_volume", + (context) => AppLocalizations.of(context)!.volumeWhileTasks, + 0, + 100, + 50, + unit: "%", + getDescription: (context) => "Percentage of base volume", + ), SwitchSetting( "Rising Volume", (context) => AppLocalizations.of(context)!.risingVolumeSetting, @@ -224,6 +218,14 @@ SettingGroup alarmSettingsSchema = SettingGroup( enableConditions: [ ValueCondition(["Rising Volume"], (value) => value == true) ]), + SelectSetting( + "Audio Channel", + (context) => AppLocalizations.of(context)!.audioChannelSetting, + audioChannelOptions, + onChange: (context, index) { + RingtonePlayer.stop(); + }, + ), ], ), SwitchSetting("Vibration", @@ -291,10 +293,13 @@ SettingGroup alarmSettingsSchema = SettingGroup( "Length", ], ), - ListSetting( + CustomizableListSetting( "Tasks", (context) => AppLocalizations.of(context)!.tasksSetting, [], + // kDebugMode + // ? [AlarmTask(AlarmTaskType.math), AlarmTask(AlarmTaskType.sequence)] + // : [], alarmTaskSchemasMap.keys.map((key) => AlarmTask(key)).toList(), addCardBuilder: (item) => AlarmTaskCard(task: item, isAddCard: true), cardBuilder: (item, [onDelete, onDuplicate]) => AlarmTaskCard( @@ -307,9 +312,6 @@ SettingGroup alarmSettingsSchema = SettingGroup( return Text("${setting.value.length} tasks"); }, itemPreviewBuilder: (item) => TryAlarmTaskButton(alarmTask: item), - // onChange: (context, value)async{ - // await appSettings.save(); - // } ), DynamicMultiSelectSetting( "Tags", diff --git a/lib/alarm/data/alarm_sort_options.dart b/lib/alarm/data/alarm_sort_options.dart index 90d972cc..3c3747fb 100644 --- a/lib/alarm/data/alarm_sort_options.dart +++ b/lib/alarm/data/alarm_sort_options.dart @@ -14,7 +14,7 @@ final List> alarmSortOptions = [ ListSortOption((context) => AppLocalizations.of(context)!.timeOfDayDesc, sortTimeOfDayDescending), ]; -int sortRemainingTimeDescending(Alarm a, Alarm b) { +int sortRemainingTimeAscending(Alarm a, Alarm b) { if (a.currentScheduleDateTime == null && b.currentScheduleDateTime == null) { return 0; } else if (a.currentScheduleDateTime == null) { @@ -29,7 +29,7 @@ int sortRemainingTimeDescending(Alarm a, Alarm b) { return remainingB.compareTo(remainingA); } -int sortRemainingTimeAscending(Alarm a, Alarm b) { +int sortRemainingTimeDescending(Alarm a, Alarm b) { if (a.currentScheduleDateTime == null && b.currentScheduleDateTime == null) { return 0; } else if (a.currentScheduleDateTime == null) { diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 7823a6ed..62f868d7 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -1,11 +1,13 @@ +import 'dart:developer'; import 'dart:isolate'; import 'dart:ui'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; -import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:flutter/foundation.dart'; import 'package:clock_app/alarm/logic/schedule_alarm.dart'; @@ -13,7 +15,6 @@ import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/ringing_manager.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; import 'package:clock_app/alarm/utils/alarm_id.dart'; import 'package:clock_app/common/utils/time_of_day.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; @@ -21,18 +22,24 @@ import 'package:clock_app/timer/utils/timer_id.dart'; const String stopAlarmPortName = "stopAlarmPort"; const String updatePortName = "updatePort"; +const String setAlarmVolumePortName = "setAlarmVolumePort"; @pragma('vm:entry-point') void triggerScheduledNotification(int scheduleId, Json params) async { - debugPrint("Alarm triggered: $scheduleId"); + FlutterError.onError = (FlutterErrorDetails details) { + logger.f("Error in triggerScheduledNotification isolate: ${details.exception.toString()}"); + }; + + logger.t( + "[triggerScheduledNotification] Alarm isolate triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); // print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}"); if (params == null) { - debugPrint("Params was null when triggering alarm"); + logger.e("Params was null when triggering alarm"); return; } if (params['type'] == null) { - debugPrint("Params Type was null when triggering alarm"); + logger.e("Params Type was null when triggering alarm"); return; } @@ -50,6 +57,8 @@ void triggerScheduledNotification(int scheduleId, Json params) async { stopScheduledNotification(message); }); + // Isolate.current.addOnExitListener(receivePort.sendPort); + if (notificationType == ScheduledNotificationType.alarm) { triggerAlarm(scheduleId, params); } else if (notificationType == ScheduledNotificationType.timer) { @@ -70,36 +79,52 @@ void stopScheduledNotification(List message) { } else if (notificationType == ScheduledNotificationType.timer) { stopTimer(scheduleId, action); } + + logger.t( + "[stopScheduledNotification] Alarm stop triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); } void triggerAlarm(int scheduleId, Json params) async { + logger.i("Alarm triggered $scheduleId"); if (params == null) { - if (kDebugMode) { - print("Params was null when triggering alarm"); - } + logger.e("Params was null when triggering alarm"); return; } Alarm? alarm = getAlarmById(scheduleId); DateTime now = DateTime.now(); + // Note: this won't effect the variable `alarm` as we have already retrieved that await updateAlarms("triggerAlarm(): Updating all alarms on trigger"); - // Ignore in the following cases: - // 1. Alarm was deleted and somehow wasn't cancelled - // 2. Alarm is disabled and somehow wasn't cancelled - // 3. Alarm is set to skip the next alarm - // 4. Alarm is set to ring in the future but somehow was triggered - // 5. Alarm is ringing 1 hour later than its time - if (alarm == null || - alarm.isEnabled == false || - alarm.shouldSkipNextAlarm || - alarm.currentScheduleDateTime == null || - now.millisecondsSinceEpoch < - alarm.currentScheduleDateTime!.millisecondsSinceEpoch || - now.millisecondsSinceEpoch > - alarm.currentScheduleDateTime!.millisecondsSinceEpoch + - 1000 * 60 * 60) { + // Skip the alarm in the following cases: + if (alarm == null) { + logger.i("Skipping alarm $scheduleId because it doesn't exist"); + return; + } + if (alarm.isEnabled == false) { + logger.i("Skipping alarm $scheduleId because it is disabled"); + return; + } + if (alarm.shouldSkipNextAlarm) { + logger.i( + "Skipping alarm $scheduleId because it is set to skip the next alarm"); + return; + } + if (alarm.currentScheduleDateTime == null) { + logger.i("Skipping alarm $scheduleId because it has no scheduled date"); + return; + } + if (now.millisecondsSinceEpoch < + alarm.currentScheduleDateTime!.millisecondsSinceEpoch) { + logger.i( + "Skipping alarm $scheduleId because it is set to ring in the future. Current time: $now, Scheduled time: ${alarm.currentScheduleDateTime}"); + return; + } + if (now.millisecondsSinceEpoch > + alarm.currentScheduleDateTime!.millisecondsSinceEpoch + 1000 * 60 * 60) { + logger.i( + "Skipping alarm $scheduleId because it was set to ring more than an hour ago. Current time: $now, Scheduled time: ${alarm.currentScheduleDateTime}"); return; } @@ -111,19 +136,32 @@ void triggerAlarm(int scheduleId, Json params) async { // Remove any existing alarm notifications if (RingingManager.isAlarmRinging) { - await AlarmNotificationManager.removeNotification( - ScheduledNotificationType.alarm); + await removeAlarmNotification(ScheduledNotificationType.alarm); } RingtonePlayer.playAlarm(alarm); RingingManager.ringAlarm(scheduleId); + /* + Ports to set the volume of the alarm. As the RingtonePlayer only. + As the RingtonePlayer only exists in this isolate, when other isolate + (e.g the main UI isolate) want to change the alarm volumen, they have to send + message over a port. + In this case, this is used by the AlarmNotificationScreen to lower the volume + of alarm while solving tasks. + */ + ReceivePort receivePort = ReceivePort(); + IsolateNameServer.removePortNameMapping(setAlarmVolumePortName); + IsolateNameServer.registerPortWithName( + receivePort.sendPort, setAlarmVolumePortName); + receivePort.listen((message) { + setVolume(message[0]); + }); + String timeFormatString = await loadTextFile("time_format_string"); String title = alarm.label.isEmpty ? "Alarm Ringing..." : alarm.label; - // AlarmNotificationManager.appVisibilityWhenCreated = fgbg - - AlarmNotificationManager.showFullScreenNotification( + showAlarmNotification( type: ScheduledNotificationType.alarm, scheduleIds: [scheduleId], title: title, @@ -137,10 +175,11 @@ void triggerAlarm(int scheduleId, Json params) async { } void setVolume(double volume) { - RingtonePlayer.setVolume(volume); + RingtonePlayer.setVolume(volume / 100); } void stopAlarm(int scheduleId, AlarmStopAction action) async { + logger.i("[stopAlarm] Stopping alarm $scheduleId with action: ${action.name}"); if (action == AlarmStopAction.snooze) { await updateAlarmById(scheduleId, (alarm) async => await alarm.snooze()); // await createSnoozeNotification(scheduleId); @@ -158,6 +197,7 @@ void stopAlarm(int scheduleId, AlarmStopAction action) async { } void triggerTimer(int scheduleId, Json params) async { + logger.i("[triggerTimer] Timer triggered $scheduleId"); ClockTimer? timer = getTimerById(scheduleId); if (timer == null || !timer.isRunning) { @@ -175,14 +215,13 @@ void triggerTimer(int scheduleId, Json params) async { // Remove any existing timer notifications if (RingingManager.isTimerRinging) { - await AlarmNotificationManager.removeNotification( - ScheduledNotificationType.timer); + await removeAlarmNotification(ScheduledNotificationType.timer); } RingtonePlayer.playTimer(timer); RingingManager.ringTimer(scheduleId); - AlarmNotificationManager.showFullScreenNotification( + showAlarmNotification( type: ScheduledNotificationType.timer, scheduleIds: RingingManager.ringingTimerIds, snoozeActionLabel: '+${timer.addLength.floor()}:00', @@ -194,18 +233,12 @@ void triggerTimer(int scheduleId, Json params) async { } void stopTimer(int scheduleId, AlarmStopAction action) async { + logger.i("Stopping timer $scheduleId with action: ${action.name}"); ClockTimer? timer = getTimerById(scheduleId); if (timer == null) return; if (action == AlarmStopAction.snooze) { - await scheduleSnoozeAlarm( - scheduleId, - Duration(minutes: timer.addLength.floor()), - ScheduledNotificationType.timer, - "stopTimer(): ${timer.addLength.floor()} added to timer", - ); updateTimerById(scheduleId, (timer) async { - timer.setTime(const TimeDuration(minutes: 1)); - await timer.start(); + await timer.snooze(); }); } else if (action == AlarmStopAction.dismiss) { // If there was an alarm already ringing when the timer was triggered, we diff --git a/lib/alarm/logic/alarm_reminder_notifications.dart b/lib/alarm/logic/alarm_reminder_notifications.dart index 7989a165..9ae05cd0 100644 --- a/lib/alarm/logic/alarm_reminder_notifications.dart +++ b/lib/alarm/logic/alarm_reminder_notifications.dart @@ -15,7 +15,7 @@ Future cancelAlarmReminderNotification(int id) async { } Future createAlarmReminderNotification( - int id, DateTime time, bool tasksRequired) async { + int id, String label, DateTime time, bool tasksRequired) async { await cancelAlarmReminderNotification(id); bool shouldShow = appSettings .getGroup("Alarm") @@ -47,7 +47,7 @@ Future createAlarmReminderNotification( content: NotificationContent( id: id, channelKey: reminderNotificationChannelKey, - title: "Upcoming alarm", + title: "Upcoming alarm${label.isEmpty ? "" : ": $label"}", body: time.toTimeOfDay().formatToString(timeFormatString), category: NotificationCategory.Reminder, payload: { @@ -91,7 +91,7 @@ Future createSnoozeNotification(int id, DateTime time) async { content: NotificationContent( id: id, channelKey: reminderNotificationChannelKey, - title: "Snoozed alarm", + title: "Snoozed alarm${alarm.label.isEmpty ? "" : ": ${alarm.label}"}", body: time.toTimeOfDay().formatToString(timeFormatString), // wakeUpScreen: true, category: NotificationCategory.Reminder, diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart index 568141b4..02bcf06d 100644 --- a/lib/alarm/logic/alarm_time.dart +++ b/lib/alarm/logic/alarm_time.dart @@ -1,29 +1,22 @@ +import 'package:clock/clock.dart'; import 'package:clock_app/common/types/time.dart'; -import 'package:clock_app/common/utils/date_time.dart'; +import 'package:clock_app/developer/logic/logger.dart'; // Calculates the DateTime when the provided `time` will next occur -DateTime getDailyAlarmDate( +DateTime getScheduleDateForTime( Time time, { - DateTime? scheduledDate, + DateTime? scheduleStartDate, + int interval = 1, }) { - if (scheduledDate != null && scheduledDate.isAfter(DateTime.now())) { - return DateTime(scheduledDate.year, scheduledDate.month, scheduledDate.day, - time.hour, time.minute, time.second); - } + DateTime now = clock.now(); // If a date has not been provided, assume it to be today - scheduledDate = DateTime.now(); - DateTime alarmTime; + DateTime scheduleDate = scheduleStartDate ?? now; + DateTime alarmTime = DateTime(scheduleDate.year, scheduleDate.month, + scheduleDate.day, time.hour, time.minute, time.second); - if (time.toHours() > scheduledDate.toHours()) { - // If the time is in the future, set the alarm for today - alarmTime = DateTime(scheduledDate.year, scheduledDate.month, - scheduledDate.day, time.hour, time.minute, time.second); - } else { - // If the time has already passed, set the alarm for tomorrow - DateTime nextDateTime = scheduledDate.add(const Duration(days: 1)); - alarmTime = DateTime(nextDateTime.year, nextDateTime.month, - nextDateTime.day, time.hour, time.minute, time.second); + while (!alarmTime.isAfter(now)) { + alarmTime = alarmTime.add(Duration(days: interval)); } return alarmTime; @@ -31,8 +24,8 @@ DateTime getDailyAlarmDate( // Calculates the DateTime when the provided `time` will next occur on the // provided `weekday` -DateTime getWeeklyAlarmDate(Time time, int weekday) { - DateTime dateTime = getDailyAlarmDate(time); +DateTime getWeeklyScheduleDateForTIme(Time time, int weekday) { + DateTime dateTime = getScheduleDateForTime(time); while (dateTime.weekday != weekday) { dateTime = dateTime.add(const Duration(days: 1)); } diff --git a/lib/alarm/logic/new_alarm_snackbar.dart b/lib/alarm/logic/new_alarm_snackbar.dart index e0d2f881..25985cee 100644 --- a/lib/alarm/logic/new_alarm_snackbar.dart +++ b/lib/alarm/logic/new_alarm_snackbar.dart @@ -1,30 +1,76 @@ import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -String getNewAlarmSnackbarText(Alarm alarm) { +String getRemainingAlarmTimeText(BuildContext context, Alarm alarm) { Duration etaNextAlarm = alarm.currentScheduleDateTime!.difference(DateTime.now().toLocal()); String etaText = ''; + AppLocalizations localizations = AppLocalizations.of(context)!; + + if (etaNextAlarm.inDays > 0) { + etaText = localizations.daysString(etaNextAlarm.inDays); + } else if (etaNextAlarm.inHours > 0) { + int hours = etaNextAlarm.inHours; + int minutes = etaNextAlarm.inMinutes % 60; + if (minutes > 0) { + etaText = localizations.combinedTime(localizations.hoursString(hours), + localizations.minutesString(minutes)); + } else { + etaText = localizations.hoursString(hours); + } + } else if (etaNextAlarm.inMinutes > 0) { + int minutes = etaNextAlarm.inMinutes; + etaText = localizations.minutesString(minutes); + } else { + etaText = localizations.lessThanOneMinute; + } + + return etaText; +} + +String getShortRemainingAlarmTimeText(BuildContext context, Alarm alarm) { + Duration etaNextAlarm = + alarm.currentScheduleDateTime!.difference(DateTime.now().toLocal()); + + String etaText = ''; + + AppLocalizations localizations = AppLocalizations.of(context)!; + if (etaNextAlarm.inDays > 0) { - int days = etaNextAlarm.inDays; - String dayTextSuffix = days <= 1 ? 'day' : 'days'; - etaText = '$days $dayTextSuffix'; + etaText = localizations.daysString(etaNextAlarm.inDays); } else if (etaNextAlarm.inHours > 0) { int hours = etaNextAlarm.inHours; int minutes = etaNextAlarm.inMinutes % 60; - String hourTextSuffix = hours <= 1 ? 'hour' : 'hours'; - String minuteTextSuffix = minutes <= 1 ? 'minute' : 'minutes'; - String hoursText = '$hours $hourTextSuffix'; - String minutesText = minutes == 0 ? '' : ' and $minutes $minuteTextSuffix'; - etaText = '$hoursText$minutesText'; + if (minutes > 0) { + etaText = '${localizations.shortHoursString(hours)} ${localizations.shortMinutesString(minutes)}'; + } else { + etaText = localizations.shortHoursString(hours); + } } else if (etaNextAlarm.inMinutes > 0) { int minutes = etaNextAlarm.inMinutes; - String minuteTextSuffix = minutes <= 1 ? 'minute' : 'minutes'; - etaText = '$minutes $minuteTextSuffix'; + etaText = localizations.shortMinutesString(minutes); } else { - etaText = 'less than 1 minute'; + etaText = localizations.shortMinutesString(1); } - return 'Alarm will ring in $etaText'; + return etaText; +} + +String getNewAlarmText(BuildContext context, Alarm alarm) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + final etaText = getRemainingAlarmTimeText(context, alarm); + + return localizations.alarmRingInMessage(etaText); +} + +String getNextAlarmText(BuildContext context, Alarm alarm) { + AppLocalizations localizations = AppLocalizations.of(context)!; + + final etaText = getShortRemainingAlarmTimeText(context, alarm); + + return localizations.nextAlarmIn(etaText); } diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index 43e2916c..8062a423 100644 --- a/lib/alarm/logic/schedule_alarm.dart +++ b/lib/alarm/logic/schedule_alarm.dart @@ -8,6 +8,7 @@ import 'package:clock_app/common/types/schedule_id.dart'; import 'package:clock_app/common/utils/date_time.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/utils/time_of_day.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; Future scheduleAlarm( @@ -18,8 +19,10 @@ Future scheduleAlarm( bool alarmClock = true, bool snooze = false, }) async { - if (startDate.isBefore(DateTime.now())) { - throw Exception('Attempted to schedule alarm in the past ($startDate)'); + DateTime now = DateTime.now(); + if (startDate.isBefore(now)) { + throw Exception( + 'Attempted to schedule alarm in the past. Schedule time: $startDate, current time: $now'); } if (!Platform.environment.containsKey('FLUTTER_TEST')) { @@ -68,7 +71,7 @@ Future scheduleAlarm( scheduleIds.add(ScheduleId(id: scheduleId)); await saveList(name, scheduleIds); - // + // // if (type == ScheduledNotificationType.alarm && !snooze) { // } // @@ -88,8 +91,11 @@ Future scheduleAlarm( 'type': type.name, }, ); + + logger.t( + 'Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); } - } +} Future cancelAlarm(int scheduleId, ScheduledNotificationType type) async { if (!Platform.environment.containsKey('FLUTTER_TEST')) { @@ -113,6 +119,8 @@ Future cancelAlarm(int scheduleId, ScheduledNotificationType type) async { } AndroidAlarmManager.cancel(scheduleId); + + logger.i('Canceled alarm $scheduleId of type ${type.name}'); } } @@ -128,4 +136,7 @@ Future scheduleSnoozeAlarm(int scheduleId, Duration delay, if (!Platform.environment.containsKey('FLUTTER_TEST')) { await createSnoozeNotification(scheduleId, DateTime.now().add(delay)); } + + logger.t( + 'Scheduled snooze alarm $scheduleId for ${DateTime.now().add(delay)} with type ${type.name}: $description'); } diff --git a/lib/alarm/screens/alarm_events_screen.dart b/lib/alarm/screens/alarm_events_screen.dart index 3222f224..b76fb4d8 100644 --- a/lib/alarm/screens/alarm_events_screen.dart +++ b/lib/alarm/screens/alarm_events_screen.dart @@ -3,16 +3,18 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:clock_app/alarm/data/alarm_events_list_filters.dart'; +import 'package:clock_app/alarm/data/alarm_events_sort_options.dart'; import 'package:clock_app/alarm/types/alarm_event.dart'; import 'package:clock_app/alarm/widgets/alarm_event_card.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:pick_or_save/pick_or_save.dart'; class AlarmEventsScreen extends StatefulWidget { const AlarmEventsScreen({ @@ -23,8 +25,6 @@ class AlarmEventsScreen extends StatefulWidget { State createState() => _AlarmEventsScreenState(); } - - class _AlarmEventsScreenState extends State { final _listController = PersistentListController(); List searchedItems = []; @@ -45,8 +45,7 @@ class _AlarmEventsScreenState extends State { TextTheme textTheme = theme.textTheme; return Scaffold( - appBar: - AppTopBar(title: Text("Alarm Logs", style: textTheme.titleMedium)), + appBar: const AppTopBar(title: "Alarm Logs"), body: Stack( children: [ Column( @@ -59,8 +58,6 @@ class _AlarmEventsScreenState extends State { itemBuilder: (event) => AlarmEventCard( key: ValueKey(event), event: event, - - ), // onTapItem: (fileItem, index) { // // widget.setting.setValue(context, themeItem); @@ -73,6 +70,7 @@ class _AlarmEventsScreenState extends State { placeholderText: "No alarm events", reloadOnPop: true, listFilters: alarmEventsListFilters, + sortOptions: alarmEventSortOptions, ), ), ], @@ -86,40 +84,41 @@ class _AlarmEventsScreenState extends State { }, ), FAB( - index: 1, - icon: Icons.file_download, - bottomPadding: 8, - onPressed: () async { + index: 1, + icon: Icons.file_download, + bottomPadding: 8, + onPressed: () async { + try { final events = await loadList('alarm_events'); - await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: - Uint8List.fromList(utf8.encode(listToString(events))), - fileName: - "chrono_alarm_events_${DateTime.now().toIso8601String()}.json", - ) - ], - )); - }), + await FilePicker.platform.saveFile( + bytes: Uint8List.fromList(utf8.encode(listToString(events))), + fileName: + "chrono_alarm_events_${DateTime.now().toIso8601String().split(".")[0]}.json", + ); + } catch (e) { + logger.e("Error saving alarm events file: ${e.toString()}"); + } + }, + ), FAB( index: 2, icon: Icons.file_upload, bottomPadding: 8, onPressed: () async { - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - getCachedFilePath: true, - ), - ); - if (result != null && result.isNotEmpty) { - File file = File(result[0]); - final data = utf8.decode(file.readAsBytesSync()); - final alarmEvents = listFromString(data); - for (var event in alarmEvents) { - _listController.addItem(event); + try { + FilePickerResult? result = await FilePicker.platform + .pickFiles(type: FileType.any, allowMultiple: false); + + if (result != null && result.files.isNotEmpty) { + File file = File(result.files.single.path!); + final data = utf8.decode(file.readAsBytesSync()); + final alarmEvents = listFromString(data); + for (var event in alarmEvents) { + _listController.addItem(event); + } } + } catch (e) { + logger.e("Error loading alarm events file: ${e.toString()}"); } }), diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 89ad197b..d4e82fe6 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -1,10 +1,14 @@ -import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'dart:ui'; + +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/utils/alarm_id.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/common/widgets/clock/clock.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/routes.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/navigation/types/alignment.dart'; import 'package:clock_app/notifications/widgets/notification_actions/slide_notification_action.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; @@ -37,16 +41,20 @@ class _AlarmNotificationScreenState extends State { void _setNextWidget() { setState(() { if (_currentIndex < 0) { + IsolateNameServer.lookupPortByName(setAlarmVolumePortName) + ?.send([alarm.volume]); _currentWidget = actionWidget; } else if (_currentIndex >= alarm.tasks.length) { if (widget.onPop != null) { widget.onPop!(); Navigator.of(context).pop(true); } else { - AlarmNotificationManager.dismissNotification(widget.scheduleId, - widget.dismissType, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, widget.dismissType, + ScheduledNotificationType.alarm); } } else { + IsolateNameServer.lookupPortByName(setAlarmVolumePortName) + ?.send([alarm.volume * alarm.volumeDuringTasks / 100]); // RingtonePlayer.setVolume(0); _currentWidget = alarm.tasks[_currentIndex].builder(_setNextWidget); } @@ -60,8 +68,8 @@ class _AlarmNotificationScreenState extends State { Alarm? currentAlarm = getAlarmById(widget.scheduleId); if (currentAlarm == null) { - AlarmNotificationManager.dismissNotification(widget.scheduleId, - widget.dismissType, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, widget.dismissType, + ScheduledNotificationType.alarm); return; } alarm = currentAlarm; @@ -81,15 +89,15 @@ class _AlarmNotificationScreenState extends State { snoozeLabel: "Snooze", ); - debugPrint(e.toString()); + logger.e(e.toString()); } _setNextWidget(); } void _snoozeAlarm() { - AlarmNotificationManager.snoozeAlarm( - widget.scheduleId, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, AlarmDismissType.snooze, + ScheduledNotificationType.alarm); } @override @@ -109,22 +117,34 @@ class _AlarmNotificationScreenState extends State { children: [ if (_currentIndex <= 0) Expanded( - flex: 1, - child: Column( - children: [ - const Spacer(), - const Clock( - // dateTime: Date, - horizontalAlignment: ElementAlignment.center, - shouldShowDate: false, - shouldShowSeconds: false, - ), - const SizedBox(height: 8), - Text( - "Alarm", - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const Spacer(), + if (alarm.label.isNotEmpty) + Text( + alarm.label, + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + const DigitalClock( + // dateTime: Date, + horizontalAlignment: ElementAlignment.center, + shouldShowDate: false, + shouldShowSeconds: false, + ), + const SizedBox(height: 8), + Text( + "Alarm", + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), ), ), Expanded( diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index cc96b771..df76acd3 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -2,10 +2,13 @@ import 'package:clock_app/alarm/data/alarm_list_filters.dart'; import 'package:clock_app/alarm/data/alarm_sort_options.dart'; import 'package:clock_app/alarm/logic/new_alarm_snackbar.dart'; import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/alarm/utils/next_alarm.dart'; import 'package:clock_app/alarm/widgets/alarm_card.dart'; import 'package:clock_app/alarm/widgets/alarm_description.dart'; import 'package:clock_app/alarm/widgets/alarm_time_picker.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/common/logic/customize_screen.dart'; +import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/types/time.dart'; @@ -14,22 +17,17 @@ import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/common/widgets/time_picker.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; -import 'package:great_list_view/great_list_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -typedef AlarmCardBuilder = Widget Function( - BuildContext context, - int index, - AnimatedWidgetBuilderData data, -); - class AlarmScreen extends StatefulWidget { - const AlarmScreen({super.key}); + const AlarmScreen({super.key, this.actionController}); + + final QuickActionController? actionController; @override State createState() => _AlarmScreenState(); @@ -40,6 +38,8 @@ class _AlarmScreenState extends State { late Setting _showInstantAlarmButton; late Setting _showFilters; late Setting _showSort; + late Setting _showNextAlarm; + late Alarm? nextAlarm; void update(value) { setState(() {}); @@ -53,20 +53,28 @@ class _AlarmScreenState extends State { _showInstantAlarmButton = appSettings .getGroup("Developer Options") .getSetting("Show Instant Alarm Button"); - _showFilters = appSettings - .getGroup("Alarm") - .getGroup("Filters") - .getSetting("Show Filters"); - _showSort = appSettings - .getGroup("Alarm") - .getGroup("Filters") - .getSetting("Show Sort"); + final filtersGroup = appSettings.getGroup("Alarm").getGroup("Filters"); + _showFilters = filtersGroup.getSetting("Show Filters"); + _showSort = filtersGroup.getSetting("Show Sort"); + _showNextAlarm = filtersGroup.getSetting("Show Next Alarm"); + // appSettings.getGroup("Accessibility").getSetting("Left Handed Mode"); _showInstantAlarmButton.addListener(update); _showFilters.addListener(update); + _showNextAlarm.addListener(update); _showSort.addListener(update); + // ListenerManager.addOnChangeListener("alarms", update); + + nextAlarm = getNextAlarm(); + + widget.actionController?.setAction((action) { + if (action == "add_alarm") { + _selectTime(); + } + }); + // ListenerManager().addListener(); } @@ -75,6 +83,8 @@ class _AlarmScreenState extends State { _showInstantAlarmButton.removeListener(update); _showFilters.removeListener(update); _showSort.removeListener(update); + _showNextAlarm.removeListener(update); + // ListenerManager.removeOnChangeListener("alarms", update); super.dispose(); } @@ -119,14 +129,18 @@ class _AlarmScreenState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmSnackbarText(alarm), fab: true, navBar: true)); + ScaffoldMessenger.of(context).showSnackBar(getThemedSnackBar( + context, + getNewAlarmText(context, alarm), + fab: true, + navBar: true)); }); } Future _handleEnableChangeAlarm(Alarm alarm, bool value) async { if (!alarm.canBeDisabledWhenSnoozed && !value && alarm.isSnoozed) { - showSnackBar(context, AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, + showSnackBar(context, + AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, fab: true, navBar: true); } else { await alarm.setIsEnabled(value, @@ -140,8 +154,12 @@ class _AlarmScreenState extends State { List alarms, bool value) async { for (var alarm in alarms) { if (!alarm.canBeDisabledWhenSnoozed && !value && alarm.isSnoozed) { - showSnackBar(context, AppLocalizations.of(context)!.cannotDisableAlarmWhileSnoozedSnackbar, - fab: true, navBar: true); + showSnackBar( + context, + AppLocalizations.of(context)! + .cannotDisableAlarmWhileSnoozedSnackbar, + fab: true, + navBar: true); } else { await alarm.setIsEnabled(value, "_handleEnableChangeMultipleAlarms(): Alarm enable set to $value by user"); @@ -173,31 +191,97 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } - @override - Widget build(BuildContext context) { - Future selectTime() async { - final PickerResult? timePickerResult = - await showTimePickerDialog( - context: context, - initialTime: TimeOfDay.now(), - title: AppLocalizations.of(context)!.selectTime, - cancelText: AppLocalizations.of(context)!.cancelButton, - confirmText: AppLocalizations.of(context)!.saveButton, - useSimple: false, - ); - - if (timePickerResult != null) { - Alarm alarm = Alarm.fromTimeOfDay(timePickerResult.value); - if (timePickerResult.isCustomize) { - await _openCustomizeAlarmScreen(alarm, onSave: (newAlarm) async { - _listController.addItem(newAlarm); - }, isNewAlarm: true); - } else { - _listController.addItem(alarm); - } + void handleAddAlarmActon() { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + _selectTime(); + } + + List> _getListFilterItems() { + List> listFilterItems = + _showFilters.value ? [...alarmListFilters] : []; + + if (nextAlarm != null && _showNextAlarm.value) { + if (nextAlarm!.currentScheduleDateTime != null) { + listFilterItems.insert( + 0, + ListFilter( + (context) => getNextAlarmText(context, nextAlarm!), + (alarm) => alarm.id == nextAlarm!.id, + )); } } + return listFilterItems; + } + + Future _selectTime() async { + final PickerResult? timePickerResult = + await showTimePickerDialog( + context: context, + initialTime: TimeOfDay.now(), + title: AppLocalizations.of(context)!.selectTime, + cancelText: AppLocalizations.of(context)!.cancelButton, + confirmText: AppLocalizations.of(context)!.saveButton, + useSimple: false, + ); + + if (timePickerResult != null) { + Alarm alarm = Alarm.fromTimeOfDay(timePickerResult.value); + if (timePickerResult.isCustomize) { + await _openCustomizeAlarmScreen(alarm, onSave: (newAlarm) async { + _listController.addItem(newAlarm); + }, isNewAlarm: true); + } else { + _listController.addItem(alarm); + } + } + } + + List> _getCustomActions() { + if (!_showFilters.value) return []; + + return [ + ListFilterCustomAction( + name: AppLocalizations.of(context)!.enableAllFilteredAlarmsAction, + icon: Icons.alarm_on_rounded, + action: (alarms) { + _handleEnableChangeMultiple(alarms, true); + }), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.disableAllFilteredAlarmsAction, + icon: Icons.alarm_off_rounded, + action: (alarms) { + _handleEnableChangeMultiple(alarms, false); + }), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.skipAllFilteredAlarmsAction, + icon: Icons.skip_next_rounded, + action: (alarms) { + _handleSkipChangeMultiple(alarms, true); + }), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.cancelSkipAllFilteredAlarmsAction, + icon: Icons.skip_next_rounded, + action: (alarms) { + _handleSkipChangeMultiple(alarms, false); + }), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.shuffleAlarmMelodiesAction, + icon: Icons.shuffle_rounded, + action: (alarms) async { + List randomIndices = + await getNRandomRingtoneIndices(alarms.length); + for (var alarm in alarms) { + final setting = alarm.settings.getSetting("Melody") + as DynamicSelectSetting; + setting.setIndex(context, randomIndices.removeAt(0)); + } + }), + ]; + } + + @override + Widget build(BuildContext context) { return Stack( children: [ PersistentListView( @@ -221,42 +305,18 @@ class _AlarmScreenState extends State { }, placeholderText: AppLocalizations.of(context)!.noAlarmMessage, reloadOnPop: true, - listFilters: _showFilters.value ? alarmListFilters : [], - customActions: _showFilters.value - ? [ - ListFilterCustomAction( - name: AppLocalizations.of(context)!.enableAllFilteredAlarmsAction, - icon: Icons.alarm_on_rounded, - action: (alarms) { - _handleEnableChangeMultiple(alarms, true); - }), - ListFilterCustomAction( - name: AppLocalizations.of(context)!.disableAllFilteredAlarmsAction, - icon: Icons.alarm_off_rounded, - action: (alarms) { - _handleEnableChangeMultiple(alarms, false); - }), - ListFilterCustomAction( - name: AppLocalizations.of(context)!.skipAllFilteredAlarmsAction, - icon: Icons.skip_next_rounded, - action: (alarms) { - _handleSkipChangeMultiple(alarms, true); - }), - ListFilterCustomAction( - name: AppLocalizations.of(context)!.cancelSkipAllFilteredAlarmsAction, - icon: Icons.skip_next_rounded, - action: (alarms) { - _handleSkipChangeMultiple(alarms, false); - }), - ] - : [], + onSaveItems: (items) { + nextAlarm = getNextAlarm(); + setState(() {}); + }, + isSelectable: true, + // header: getNextAlarmWidget(), + listFilters: _getListFilterItems(), + customActions: _getCustomActions(), sortOptions: _showSort.value ? alarmSortOptions : [], ), FAB( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - selectTime(); - }, + onPressed: handleAddAlarmActon, ), if (_showInstantAlarmButton.value) FAB( diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 971b80ad..d4f004e3 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -79,8 +79,11 @@ class Alarm extends CustomizableListItem { String get label => _settings.getSetting("Label").value; Type get scheduleType => _settings.getSetting("Type").value; FileItem get ringtone => _settings.getSetting("Melody").value; + bool get shouldStartMelodyAtRandomPos => + _settings.getSetting("start_melody_at_random_pos").value; bool get vibrate => _settings.getSetting("Vibration").value; double get volume => _settings.getSetting("Volume").value; + double get volumeDuringTasks => _settings.getSetting("task_volume").value; double get snoozeLength => _settings.getSetting("Length").value; List get tasks => _settings.getSetting("Tasks").value; List get tags => _settings.getSetting("Tags").value; @@ -135,7 +138,6 @@ class Alarm extends CustomizableListItem { Alarm.fromAlarm(Alarm alarm) : _isEnabled = alarm._isEnabled, - // _isFinished = alarm._isFinished, _time = alarm._time, _snoozeCount = alarm._snoozeCount, _snoozeTime = alarm._snoozeTime, @@ -148,7 +150,6 @@ class Alarm extends CustomizableListItem { @override void copyFrom(dynamic other) { _isEnabled = other._isEnabled; - // _isFinished = other._isFinished; _time = other._time; _snoozeCount = other._snoozeCount; _snoozeTime = other._snoozeTime; @@ -175,9 +176,13 @@ class Alarm extends CustomizableListItem { _settings.getSetting(name).setValueWithoutNotify(value); } + // Skipping the alarm doesn't actually remove the scheduled alarm. Instead, it + // just doesn't ring the alarm when it triggers. We don't remove the schedule + // because it is required to chain-schedule the next alarms in daily/weekly/date/range schedules void skip() { _skippedTime = currentScheduleDateTime; - updateReminderNotification(); + // We reschedule the alarm as a non-alarmclock so it is no longer visible to the system + schedule("skip(): Update alarm on skip"); } void cancelSkip() { @@ -248,7 +253,9 @@ class Alarm extends CustomizableListItem { // So we cancel all others and schedule the active one for (var schedule in _schedules) { if (schedule.runtimeType == scheduleType) { - await schedule.schedule(_time, description); + // If alarm is skipped, we do not want it to show to the system, + // So we set alarmClock param of AlarmManager to false + await schedule.schedule(_time, description, _skippedTime == null); } else { await schedule.cancel(); } @@ -262,7 +269,7 @@ class Alarm extends CustomizableListItem { currentScheduleDateTime != null && !shouldSkipNextAlarm) { await createAlarmReminderNotification( - id, currentScheduleDateTime!, tasks.isNotEmpty); + id, label, currentScheduleDateTime!, tasks.isNotEmpty); } else { for (var schedule in _schedules) { cancelAlarmReminderNotification(schedule.currentAlarmRunnerId); @@ -300,6 +307,7 @@ class Alarm extends CustomizableListItem { } void handleDismiss() { + _snoozeCount = 0; if (scheduleType == OnceAlarmSchedule && shouldDeleteAfterRinging || shouldDeleteAfterFinish && isFinished) { _markedForDeletion = true; @@ -346,9 +354,9 @@ class Alarm extends CustomizableListItem { } } - // void _delete() { - // _markedForDeletion = true; - // } + void setRingtone(BuildContext context, int index) { + ; + } void setTime(Time time) { _time = time; @@ -442,4 +450,8 @@ class Alarm extends CustomizableListItem { 'settings': _settings.valueToJson(), 'skippedTime': _skippedTime?.millisecondsSinceEpoch, }; + + bool isEqualTo(Alarm other) { + return _time == other._time && _settings.isEqualTo(other._settings); + } } diff --git a/lib/alarm/types/alarm_event.dart b/lib/alarm/types/alarm_event.dart index db66c538..fc65ef7c 100644 --- a/lib/alarm/types/alarm_event.dart +++ b/lib/alarm/types/alarm_event.dart @@ -1,7 +1,7 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/types/notification_type.dart'; -import 'package:flutter/foundation.dart'; +import 'package:clock_app/common/utils/id.dart'; // enum AlarmEventType{ // schedule, @@ -26,7 +26,7 @@ class AlarmEvent extends ListItem { required this.scheduleId, required this.startDate, required this.isActive, - }) : id = UniqueKey().hashCode; + }) : id = getId(); AlarmEvent.fromJson(Json json) { if (json == null) { diff --git a/lib/alarm/types/alarm_runner.dart b/lib/alarm/types/alarm_runner.dart index 2d7e0b20..f40aa2f5 100644 --- a/lib/alarm/types/alarm_runner.dart +++ b/lib/alarm/types/alarm_runner.dart @@ -1,22 +1,23 @@ import 'package:clock_app/alarm/logic/schedule_alarm.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/notification_type.dart'; -import 'package:flutter/material.dart'; +import 'package:clock_app/common/utils/id.dart'; class AlarmRunner extends JsonSerializable { late int _id; DateTime? _currentScheduleDateTime; - int get id => _id; + get id => _id; DateTime? get currentScheduleDateTime => _currentScheduleDateTime; AlarmRunner() { - _id = UniqueKey().hashCode; + _id = getId(); } - Future schedule(DateTime dateTime, String description) async { + Future schedule(DateTime dateTime, String description, + [bool alarmClock = false]) async { _currentScheduleDateTime = dateTime; - await scheduleAlarm(_id, dateTime, description); + await scheduleAlarm(_id, dateTime, description, alarmClock: alarmClock); } Future cancel() async { @@ -27,10 +28,10 @@ class AlarmRunner extends JsonSerializable { AlarmRunner.fromJson(Json? json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); int millisecondsSinceEpoch = json['currentScheduleDateTime'] ?? 0; _currentScheduleDateTime = millisecondsSinceEpoch == 0 ? null diff --git a/lib/alarm/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart index a1c83c02..24daea1f 100644 --- a/lib/alarm/types/alarm_task.dart +++ b/lib/alarm/types/alarm_task.dart @@ -1,6 +1,7 @@ import 'package:clock_app/alarm/data/alarm_task_schemas.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; @@ -55,21 +56,21 @@ class AlarmTask extends CustomizableListItem { AlarmTask(this.type) : _schema = alarmTaskSchemasMap[type]!.copy(), - _id = UniqueKey().hashCode; + _id = getId(); AlarmTask.from(AlarmTask task) : type = task.type, - _id = UniqueKey().hashCode, + _id = getId(), _schema = task._schema.copy(); AlarmTask.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); type = AlarmTaskType.math; _schema = alarmTaskSchemasMap[type]!.copy(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); type = AlarmTaskType.values.byName(json['type']); _schema = alarmTaskSchemasMap[type]!.copy(); _schema.loadFromJson(json['schema']); diff --git a/lib/alarm/types/ringing_manager.dart b/lib/alarm/types/ringing_manager.dart index 33ac40f5..8f46d942 100644 --- a/lib/alarm/types/ringing_manager.dart +++ b/lib/alarm/types/ringing_manager.dart @@ -1,3 +1,5 @@ + +// This class is used to keep track of what alarms/timers are currently ringing class RingingManager { static int _ringingAlarmId = -1; static final List _ringingTimerIds = []; diff --git a/lib/alarm/types/schedules/alarm_schedule.dart b/lib/alarm/types/schedules/alarm_schedule.dart index f03b1640..50b86f10 100644 --- a/lib/alarm/types/schedules/alarm_schedule.dart +++ b/lib/alarm/types/schedules/alarm_schedule.dart @@ -11,7 +11,11 @@ abstract class AlarmSchedule extends JsonSerializable { AlarmSchedule(); List get alarmRunners; - Future schedule(Time time, String description); + Future schedule( + Time time, + String description, [ + bool alarmClock = false, + ]); Future cancel(); bool hasId(int id); } diff --git a/lib/alarm/types/schedules/daily_alarm_schedule.dart b/lib/alarm/types/schedules/daily_alarm_schedule.dart index 6bc8577a..4d2ae0a3 100644 --- a/lib/alarm/types/schedules/daily_alarm_schedule.dart +++ b/lib/alarm/types/schedules/daily_alarm_schedule.dart @@ -24,9 +24,10 @@ class DailyAlarmSchedule extends AlarmSchedule { super(); @override - Future schedule(Time time,String description) async { - DateTime alarmDate = getDailyAlarmDate(time); - await _alarmRunner.schedule(alarmDate,description); + Future schedule(Time time, String description, + [bool alarmClock = false]) async { + DateTime alarmDate = getScheduleDateForTime(time); + await _alarmRunner.schedule(alarmDate, description, alarmClock); } @override diff --git a/lib/alarm/types/schedules/dates_alarm_schedule.dart b/lib/alarm/types/schedules/dates_alarm_schedule.dart index 5214effb..0da8db19 100644 --- a/lib/alarm/types/schedules/dates_alarm_schedule.dart +++ b/lib/alarm/types/schedules/dates_alarm_schedule.dart @@ -55,7 +55,7 @@ class DatesAlarmSchedule extends AlarmSchedule { } @override - Future schedule(Time time, String description) async { + Future schedule(Time time, String description, [bool alarmClock = false]) async { List dates = _datesSetting.value; for (int i = 0; i < dates.length; i++) { @@ -71,7 +71,7 @@ class DatesAlarmSchedule extends AlarmSchedule { // We also schedule just the next upcoming date // When that schedule is finished, we will schedule the next one and so on if (date.isAfter(DateTime.now())) { - await _alarmRunner.schedule(date, description); + await _alarmRunner.schedule(date, description, alarmClock); _isFinished = false; return; } diff --git a/lib/alarm/types/schedules/once_alarm_schedule.dart b/lib/alarm/types/schedules/once_alarm_schedule.dart index e0e6e5be..9a7b8988 100644 --- a/lib/alarm/types/schedules/once_alarm_schedule.dart +++ b/lib/alarm/types/schedules/once_alarm_schedule.dart @@ -25,13 +25,13 @@ class OnceAlarmSchedule extends AlarmSchedule { super(); @override - Future schedule(Time time, String description) async { + Future schedule(Time time, String description, [bool alarmClock = false]) async { // If the alarm has already been scheduled in the past, disable it. if (currentScheduleDateTime?.isBefore(DateTime.now()) ?? false) { _isDisabled = true; } else { - DateTime alarmDate = getDailyAlarmDate(time); - await _alarmRunner.schedule(alarmDate, description); + DateTime alarmDate = getScheduleDateForTime(time); + await _alarmRunner.schedule(alarmDate, description, alarmClock); _isDisabled = false; } } diff --git a/lib/alarm/types/schedules/range_alarm_schedule.dart b/lib/alarm/types/schedules/range_alarm_schedule.dart index 8af5bd44..b86514a3 100644 --- a/lib/alarm/types/schedules/range_alarm_schedule.dart +++ b/lib/alarm/types/schedules/range_alarm_schedule.dart @@ -10,7 +10,7 @@ class RangeAlarmSchedule extends AlarmSchedule { late final AlarmRunner _alarmRunner; late final DateTimeSetting _datesRangeSetting; late final SelectSetting _intervalSetting; - bool _isFinished = true; + bool _isFinished = false; RangeInterval get interval => _intervalSetting.value; DateTime get startDate => _datesRangeSetting.value.first; @@ -42,21 +42,22 @@ class RangeAlarmSchedule extends AlarmSchedule { } @override - Future schedule(Time time,String description) async { + Future schedule(Time time, String description, [bool alarmClock = false]) async { + int intervalDays = interval == RangeInterval.daily ? 1 : 7; // All the dates are not scheduled at once // Instead we schedule the next date after the current one is finished - - DateTime alarmDate = getDailyAlarmDate(time, scheduledDate: startDate); - if (alarmDate.day <= endDate.day) { - await _alarmRunner.schedule(alarmDate,description); - _isFinished = false; - } else { + DateTime alarmDate = getScheduleDateForTime(time, + scheduleStartDate: startDate, interval: intervalDays); + if (alarmDate.isAfter(endDate)) { _isFinished = true; + } else { + await _alarmRunner.schedule(alarmDate, description, alarmClock); + _isFinished = false; } } @override - Future cancel()async { + Future cancel() async { await _alarmRunner.cancel(); } diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart index e12c504a..b2d1e341 100644 --- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart +++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart @@ -84,10 +84,13 @@ class WeeklyAlarmSchedule extends AlarmSchedule { super(); @override - Future schedule(Time time,String description) async { - for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { - weekdaySchedule.alarmRunner.cancel(); - } + Future schedule(Time time,String description, [bool alarmClock = false]) async { + // for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { + // await weekdaySchedule.alarmRunner.cancel(); + // } + + // We schedule the next occurence for each weekday. + // Subsequent occurences will be scheduled after the first one passes. List weekdays = _weekdaySetting.selected.toList(); List existingWeekdays = @@ -99,8 +102,8 @@ class WeeklyAlarmSchedule extends AlarmSchedule { } for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { - DateTime alarmDate = getWeeklyAlarmDate(time, weekdaySchedule.weekday); - await weekdaySchedule.alarmRunner.schedule(alarmDate,description); + DateTime alarmDate = getWeeklyScheduleDateForTIme(time, weekdaySchedule.weekday); + await weekdaySchedule.alarmRunner.schedule(alarmDate,description, alarmClock); } } diff --git a/lib/alarm/utils/next_alarm.dart b/lib/alarm/utils/next_alarm.dart new file mode 100644 index 00000000..ee0658c6 --- /dev/null +++ b/lib/alarm/utils/next_alarm.dart @@ -0,0 +1,13 @@ +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; + +Alarm? getNextAlarm() { + List alarms = loadListSync('alarms'); + if (alarms.isEmpty) return null; + alarms.sort((a, b) { + if (a.currentScheduleDateTime == null) return 1; + if (b.currentScheduleDateTime == null) return -1; + return a.currentScheduleDateTime!.compareTo(b.currentScheduleDateTime!); + }); + return alarms.first; +} diff --git a/lib/alarm/widgets/alarm_card.dart b/lib/alarm/widgets/alarm_card.dart index 545c1081..212e2a71 100644 --- a/lib/alarm/widgets/alarm_card.dart +++ b/lib/alarm/widgets/alarm_card.dart @@ -7,7 +7,7 @@ import 'package:clock_app/clock/types/time.dart'; import 'package:clock_app/common/types/popup_action.dart'; import 'package:clock_app/common/utils/popup_action.dart'; import 'package:clock_app/common/widgets/card_edit_menu.dart'; -import 'package:clock_app/common/widgets/clock/clock_display.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock_display.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; @@ -161,7 +161,7 @@ class _AlarmCardState extends State { ), Row( children: [ - ClockDisplay( + DigitalClockDisplay( dateTime: widget.alarm.time.toDateTime(), scale: 0.6, color: widget.alarm.isEnabled diff --git a/lib/alarm/widgets/alarm_event_card.dart b/lib/alarm/widgets/alarm_event_card.dart index 64b6076d..864f4dcd 100644 --- a/lib/alarm/widgets/alarm_event_card.dart +++ b/lib/alarm/widgets/alarm_event_card.dart @@ -15,38 +15,35 @@ class AlarmEventCard extends StatelessWidget { Color textColor = colorScheme.onSurface.withOpacity(0.8); - return Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 16.0, right: 16.0, top: 8.0, bottom: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text(event.isActive ? "Active" : "Inactive", - style: textTheme.labelMedium?.copyWith( - color: event.isActive - ? colorScheme.primary - : colorScheme.onSurface)), - Text('Scheduled for: ${event.startDate}', - style: textTheme.labelMedium?.copyWith(color: textColor)), - Text( - 'Type: ${event.notificationType == ScheduledNotificationType.alarm ? "Alarm" : "Timer"}', - style: textTheme.labelMedium?.copyWith(color: textColor)), - Text('Created at: ${event.eventTime}', - style: textTheme.labelMedium?.copyWith(color: textColor)), - Text( - 'Description: ${event.description}', - style: textTheme.labelMedium?.copyWith(color: textColor), - maxLines: 5, - ), - Text('Schedule Id: ${event.scheduleId}', - style: textTheme.labelMedium?.copyWith(color: textColor)), - ], + return Padding( + padding: const EdgeInsets.only( + left: 16.0, right: 16.0, top: 8.0, bottom: 8.0), + child: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(event.isActive ? "Active" : "Inactive", + style: textTheme.labelMedium?.copyWith( + color: event.isActive + ? colorScheme.primary + : colorScheme.onSurface)), + Text('Scheduled for: ${event.startDate}', + style: textTheme.labelMedium?.copyWith(color: textColor)), + Text( + 'Type: ${event.notificationType == ScheduledNotificationType.alarm ? "Alarm" : "Timer"}', + style: textTheme.labelMedium?.copyWith(color: textColor)), + Text('Created at: ${event.eventTime}', + style: textTheme.labelMedium?.copyWith(color: textColor)), + Text( + 'Description: ${event.description}', + style: textTheme.labelMedium?.copyWith(color: textColor), + maxLines: 5, ), - ), - ], + Text('Schedule Id: ${event.scheduleId}', + style: textTheme.labelMedium?.copyWith(color: textColor)), + ], + ), ); } } diff --git a/lib/alarm/widgets/alarm_time_picker.dart b/lib/alarm/widgets/alarm_time_picker.dart index d8669509..0ed87b6a 100644 --- a/lib/alarm/widgets/alarm_time_picker.dart +++ b/lib/alarm/widgets/alarm_time_picker.dart @@ -1,12 +1,12 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/common/types/picker_result.dart'; -import 'package:clock_app/common/widgets/clock/clock_display.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock_display.dart'; import 'package:clock_app/common/widgets/time_picker.dart'; import 'package:clock_app/navigation/types/alignment.dart'; import 'package:flutter/material.dart'; class AlarmTimePicker extends StatefulWidget { - const AlarmTimePicker({Key? key, required this.alarm}) : super(key: key); + const AlarmTimePicker({super.key, required this.alarm}); final Alarm alarm; @@ -18,7 +18,7 @@ class _AlarmTimePickerState extends State { @override Widget build(BuildContext context) { return GestureDetector( - child: ClockDisplay( + child: DigitalClockDisplay( dateTime: widget.alarm.time.toDateTime(), horizontalAlignment: ElementAlignment.center, ), diff --git a/lib/alarm/widgets/try_alarm_task_button.dart b/lib/alarm/widgets/try_alarm_task_button.dart index 6b9d0e06..3e2f461f 100644 --- a/lib/alarm/widgets/try_alarm_task_button.dart +++ b/lib/alarm/widgets/try_alarm_task_button.dart @@ -4,8 +4,7 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:flutter/material.dart'; class TryAlarmTaskButton extends StatelessWidget { - const TryAlarmTaskButton({Key? key, required this.alarmTask}) - : super(key: key); + const TryAlarmTaskButton({super.key, required this.alarmTask}); final AlarmTask alarmTask; diff --git a/lib/app.dart b/lib/app.dart index c191902c..a15cd55b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,10 +5,9 @@ import 'package:clock_app/navigation/screens/nav_scaffold.dart'; import 'package:clock_app/navigation/types/routes.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; -import 'package:clock_app/notifications/types/notifications_controller.dart'; +import 'package:clock_app/notifications/logic/notifications_listeners.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/onboarding/screens/onboarding_screen.dart'; -import 'package:clock_app/settings/data/appearance_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_group.dart'; @@ -16,12 +15,14 @@ import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/theme/types/color_scheme.dart'; import 'package:clock_app/theme/theme.dart'; import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/theme/types/theme_brightness.dart'; import 'package:clock_app/theme/utils/color_scheme.dart'; import 'package:clock_app/timer/screens/timer_notification_screen.dart'; import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get_storage/get_storage.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -61,14 +62,14 @@ class _AppState extends State { setDigitalClockWidgetData(context); - NotificationController.setListeners(); + setNotificationListeners(); _appearanceSettings = appSettings.getGroup("Appearance"); _colorSettings = _appearanceSettings.getGroup("Colors"); _styleSettings = _appearanceSettings.getGroup("Style"); _generalSettings = appSettings.getGroup("General"); _animationSpeedSetting = - _generalSettings.getGroup("Animations").getSetting("Animation Speed"); + _appearanceSettings.getGroup("Animations").getSetting("Animation Speed"); _animationSpeedSetting.addListener(setAnimationSpeed); setAnimationSpeed(_animationSpeedSetting.value); @@ -161,12 +162,6 @@ class _AppState extends State { ThemeBrightness themeBrightness = _colorSettings.getSetting("Brightness").value; Locale locale = _generalSettings.getSetting("Language").value; - // if(!AppLocalizations.supportedLocales.contains(locale)){ - // - // } - // - // print("locaaaaaaaale $locale"); - // print(getLocaleOptions().map((e) => e.value).toList()); return MaterialApp( scaffoldMessengerKey: _messangerKey, diff --git a/lib/audio/logic/ringtones.dart b/lib/audio/logic/ringtones.dart new file mode 100644 index 00000000..18646ff0 --- /dev/null +++ b/lib/audio/logic/ringtones.dart @@ -0,0 +1,95 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/types/file_item.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_system_ringtones/flutter_system_ringtones.dart'; +import 'package:mime/mime.dart'; +import 'package:path/path.dart'; + +Future> getSystemRingtones() async { + final ringtones = (await FlutterSystemRingtones.getAlarmSounds()) + .map((ringtone) => FileItem( + ringtone.title, ringtone.uri, FileItemType.audio, + isDeletable: false)) + .toList(); + + // If no ringtones are found, add a default one + if (ringtones.isEmpty) { + ByteData data = await rootBundle.load("assets/ringtones/default.mp3"); + List bytes = + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + + String path = join(getRingtonesDirectoryPathSync(), "default.mp3"); + await File(path).writeAsBytes(bytes); + + ringtones + .add(FileItem("Default", path, FileItemType.audio, isDeletable: false)); + } + return ringtones; +} + +Future getDefaultRingtoneUri() async { + return (await loadList("ringtones")) + .firstWhere((ringtone) => ringtone.type == FileItemType.audio) + .uri; +} + +Future getRandomRingtoneIndex() async { + final ringtonesCount = (await loadList("ringtones")).length; + Random random = Random(); + return random.nextInt(ringtonesCount); +} + +Future> getNRandomRingtoneIndices(int n) async { + final ringtonesCount = (await loadList("ringtones")).length; + Random random = Random(); + List indices = []; + while (indices.length < n) { + int index = random.nextInt(ringtonesCount); + indices.add(index); + } + return indices; +} + +const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + +Future getRingtoneUri(FileItem fileItem) async { + switch (fileItem.type) { + case FileItemType.directory: + try { + List audioFiles = + (await Directory(fileItem.uri).list(recursive: true).toList()) + .whereType() + .where((item) => + lookupMimeType(item.path)?.startsWith('audio/') ?? false) + .toList(); + + if (audioFiles.isNotEmpty) { + logger.t("Audio files found in directory ${fileItem.uri}"); + Random random = Random(); + int index = random.nextInt(audioFiles.length); + FileSystemEntity documentFile = audioFiles[index]; + // logger.t("${documentFile.name} ${documentFile.uri}"); + return documentFile.uri.toString(); + } else { + logger.t( + "No audio files found in directory ${fileItem.uri}, using default"); + // Choose a default ringtone if directory doesn't have any audio + return await getDefaultRingtoneUri(); + } + } catch (e) { + logger.e("Error loading melody from directory: $e"); + return await getDefaultRingtoneUri(); + } + + case FileItemType.audio: + return fileItem.uri; + + default: + return await getDefaultRingtoneUri(); + } +} diff --git a/lib/audio/logic/system_ringtones.dart b/lib/audio/logic/system_ringtones.dart deleted file mode 100644 index 2c5d2367..00000000 --- a/lib/audio/logic/system_ringtones.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:io'; - -import 'package:clock_app/common/data/paths.dart'; -import 'package:clock_app/common/types/file_item.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_system_ringtones/flutter_system_ringtones.dart'; -import 'package:path/path.dart'; - -Future> getSystemRingtones() async { - final ringtones = (await FlutterSystemRingtones.getAlarmSounds()) - .map((ringtone) => - FileItem(ringtone.title, ringtone.uri, FileItemType.audio, isDeletable: false)) - .toList(); - - // If no ringtones are found, add a default one - if (ringtones.isEmpty) { - ByteData data = await rootBundle.load("assets/ringtones/default.mp3"); - List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - - String path = join(getRingtonesDirectoryPathSync(), "default.mp3"); - await File(path).writeAsBytes(bytes); - - ringtones.add(FileItem("Default", path, FileItemType.audio, isDeletable: false)); - } - return ringtones; -} diff --git a/lib/audio/screens/ringtones_screen.dart b/lib/audio/screens/ringtones_screen.dart new file mode 100644 index 00000000..2594114a --- /dev/null +++ b/lib/audio/screens/ringtones_screen.dart @@ -0,0 +1,160 @@ +import 'dart:io'; +import 'package:clock_app/audio/types/ringtone_player.dart'; +import 'package:clock_app/common/types/file_item.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/fab.dart'; +import 'package:clock_app/common/widgets/file_item_card.dart'; +import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:clock_app/system/data/device_info.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class RingtonesScreen extends StatefulWidget { + const RingtonesScreen({ + super.key, + }); + + @override + State createState() => _RingtonesScreenState(); +} + +class _RingtonesScreenState extends State { + final _listController = PersistentListController(); + List searchedItems = []; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + RingtonePlayer.stop(); + super.dispose(); + } + + void _onDeleteItem(FileItem fileItem) { + if (!fileItem.isDeletable) return; + if (fileItem.type != FileItemType.directory) { + final file = File(fileItem.uri); + file.deleteSync(); + } + RingtonePlayer.stop(); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + + return Scaffold( + appBar: AppTopBar( + title: AppLocalizations.of(context)!.melodiesSetting, + ), + body: Stack( + children: [ + Column( + children: [ + Expanded( + flex: 1, + child: PersistentListView( + saveTag: 'ringtones', + listController: _listController, + itemBuilder: (fileItem) => FileItemCard( + key: ValueKey(fileItem), + fileItem: fileItem, + onPressDelete: () => _listController.deleteItem(fileItem), + ), + onTapItem: (fileItem, index) { + // widget.setting.setValue(context, themeItem); + // _listController.reload(); + }, + onDeleteItem: _onDeleteItem, + isDuplicateEnabled: false, + placeholderText: "No melodies", + reloadOnPop: true, + isSelectable: true, + ), + ), + ], + ), + FAB( + icon: Icons.music_note_rounded, + bottomPadding: 8, + onPressed: () async { + RingtonePlayer.stop(); + try { + FilePickerResult? result = await FilePicker.platform + .pickFiles(type: FileType.audio, allowMultiple: true); + + // The result will be null, if the user aborted the dialog + if (result != null && result.files.isNotEmpty) { + for (PlatformFile file in result.files) { + logger.t("Saving melody ${file.name}, size ${file.size}"); + final bytes = await file.xFile.readAsBytes(); + final fileItem = + FileItem(file.name, "", FileItemType.audio); + fileItem.uri = + await saveRingtone(fileItem.id.toString(), bytes); + _listController.addItem(fileItem); + } + } + } catch (e) { + logger.e("Error loading melody from directory: $e"); + } + }), + FAB( + index: 1, + icon: Icons.create_new_folder_rounded, + bottomPadding: 8, + onPressed: () async { + Permission permission = androidInfo!.version.sdkInt >= 33 + ? Permission.audio + : Permission.storage; + if (!await permission.isGranted) { + final result = await permission.request(); + if (result != PermissionStatus.granted) { + if (context.mounted) { + showSnackBar(context, "You need to allow storage access"); + } + return; + } + } + RingtonePlayer.stop(); + try { + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath(); + + if (selectedDirectory != null && selectedDirectory.isNotEmpty) { + // logger.t("selectedDirectory: $selectedDirectory"); + + // final directory = Directory(selectedDirectory); + // final List entities = + // await directory.list().toList(); + + // logger.t(entities); + + String name = basename(selectedDirectory + .replaceAll("%3A", "/") + .replaceAll("%2F", "/")); + final fileItem = + FileItem(name, selectedDirectory, FileItemType.directory); + _listController.addItem(fileItem); + } + } catch (e) { + logger.e("Error loading directory: $e"); + } + }, + ) + ], + ), + ); + } +} diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index d5742ad2..126870bf 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -1,16 +1,23 @@ +import 'dart:math'; + import 'package:audio_session/audio_session.dart'; import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/audio/types/ringtone_manager.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:just_audio/just_audio.dart'; import 'package:vibration/vibration.dart'; +Random random = Random(); + class RingtonePlayer { static AudioPlayer? _alarmPlayer; static AudioPlayer? _timerPlayer; static AudioPlayer? _mediaPlayer; static AudioPlayer? activePlayer; static bool _vibratorIsAvailable = false; + static bool _stopRisingVolume = false; static Future initialize() async { _alarmPlayer ??= AudioPlayer(handleInterruptions: true); @@ -33,6 +40,8 @@ class RingtonePlayer { await _play(ringtoneUri, vibrate: vibrate, loopMode: LoopMode.one); } + + static Future playAlarm(Alarm alarm, {LoopMode loopMode = LoopMode.one}) async { await activePlayer?.stop(); @@ -42,43 +51,16 @@ class RingtonePlayer { contentType: AndroidAudioContentType.music, )); activePlayer = _alarmPlayer; - String uri = alarm.ringtone.uri; - // if (alarm.ringtone.type == FileItemType.directory) { - // print(alarm.ringtone.uri); - // List? persistentPermUris = - // await PickOrSave().urisWithPersistedPermission(); - // print(persistentPermUris); - // print(await Directory(alarm.ringtone.uri).list(recursive: true).toList()); - // List? documentFiles = - // await PickOrSave().directoryDocumentsPicker( - // params: DirectoryDocumentsPickerParams( - // directoryUri: alarm.ringtone.uri, - // // recurseDirectories: true, - // mimeTypesFilter: ["audio/*"], - // ), - // ); - // if (documentFiles != null && documentFiles.isNotEmpty) { - // Random random = Random(); - // int index = random.nextInt(documentFiles.length); - // DocumentFile documentFile = documentFiles[index]; - // print("${documentFile.name} ${documentFile.uri}"); - // uri = documentFile.uri; - // } else { - // // Choose a default ringtone if directory doesn't have any audio - // uri = (await loadList("ringtones")) - // .where((ringtone) => ringtone.type == FileItemType.audio) - // .toList() - // .first - // .uri; - // } - // } - await _play( - uri, - vibrate: alarm.vibrate, - loopMode: LoopMode.one, - volume: alarm.volume / 100, - secondsToMaxVolume: alarm.risingVolumeDuration.inSeconds, - ); + String uri = await getRingtoneUri(alarm.ringtone); + + logger.t("Playing alarm with uri: $uri"); + + await _play(uri, + vibrate: alarm.vibrate, + loopMode: LoopMode.one, + volume: alarm.volume / 100, + secondsToMaxVolume: alarm.risingVolumeDuration.inSeconds, + startAtRandomPos: alarm.shouldStartMelodyAtRandomPos); } static Future playTimer(ClockTimer timer, @@ -98,6 +80,8 @@ class RingtonePlayer { } static Future setVolume(double volume) async { + logger.t("Setting volume to $volume"); + _stopRisingVolume = true; await activePlayer?.setVolume(volume); } @@ -107,38 +91,55 @@ class RingtonePlayer { LoopMode loopMode = LoopMode.one, double volume = 1.0, int secondsToMaxVolume = 0, + bool startAtRandomPos = false, // double duration = double.infinity, }) async { - RingtoneManager.lastPlayedRingtoneUri = ringtoneUri; - if (_vibratorIsAvailable && vibrate) { - Vibration.vibrate(pattern: [500, 1000], repeat: 0); - } - // activePlayer?. - await activePlayer?.stop(); - await activePlayer?.setLoopMode(loopMode); - await activePlayer?.setAudioSource(AudioSource.uri(Uri.parse(ringtoneUri))); - await activePlayer?.setVolume(volume); - // activePlayer.setMode - - if (secondsToMaxVolume > 0) { - for (int i = 0; i <= 10; i++) { - Future.delayed( - Duration(milliseconds: i * (secondsToMaxVolume * 100)), - () { - activePlayer?.setVolume((i / 10) * volume); - }, - ); + try { + _stopRisingVolume = false; + + RingtoneManager.lastPlayedRingtoneUri = ringtoneUri; + if (_vibratorIsAvailable && vibrate) { + Vibration.vibrate(pattern: [500, 1000], repeat: 0); + } + // activePlayer?. + await activePlayer?.stop(); + await activePlayer?.setLoopMode(loopMode); + Duration? duration = await activePlayer + ?.setAudioSource(AudioSource.uri(Uri.parse(ringtoneUri))); + logger.t("Duration: $duration"); + + if (duration != null && startAtRandomPos) { + double randomNumber = random.nextInt(100) / 100.0; + logger.t("Starting at random position: $randomNumber"); + activePlayer?.seek(duration * randomNumber); + } + await setVolume(volume); + + // Gradually increase the volume + if (secondsToMaxVolume > 0) { + for (int i = 0; i <= 10; i++) { + Future.delayed( + Duration(milliseconds: i * (secondsToMaxVolume * 100)), + () { + if (!_stopRisingVolume) { + setVolume((i / 10) * volume); + } + }, + ); + } } + // Future.delayed( + // Duration(seconds: duration.toInt()), + // () async { + // await stop(); + // }, + // ); + + // Don't use await here as this will only return after the audio is done + activePlayer?.play(); + } catch (e) { + logger.e("Error playing $ringtoneUri: $e"); } - // Future.delayed( - // Duration(seconds: duration.toInt()), - // () async { - // await stop(); - // }, - // ); - - // Don't use await here as this will only return after the audio is done - activePlayer?.play(); } static Future pause() async { @@ -156,5 +157,6 @@ class RingtonePlayer { await Vibration.cancel(); } RingtoneManager.lastPlayedRingtoneUri = ""; + _stopRisingVolume = false; } } diff --git a/lib/clock/data/clock_settings_schema.dart b/lib/clock/data/clock_settings_schema.dart new file mode 100644 index 00000000..164c4609 --- /dev/null +++ b/lib/clock/data/clock_settings_schema.dart @@ -0,0 +1,104 @@ +import 'package:clock_app/common/types/clock_settings_types.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; +import 'package:clock_app/icons/flux_icons.dart'; +import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/settings/types/setting_enable_condition.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +SettingGroup clockSettingsSchema = SettingGroup( + "clock", + (context) => AppLocalizations.of(context)!.clockTitle, + [ + SettingGroup( + "clockStyle", + (context) => AppLocalizations.of(context)!.clockStyleSettingGroup, + [ + SelectSetting( + "clockType", + (context) => AppLocalizations.of(context)!.clockTypeSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.digitalClock, + ClockType.digital), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.analogClock, + ClockType.analog), + ], + searchTags: ["analog", "digital", "face"], + ), + SelectSetting( + "showNumbers", + (context) => AppLocalizations.of(context)!.showNumbersSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.allNumbers, + ClockNumbersType.all), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.quarterNumbers, + ClockNumbersType.quarter), + SelectSettingOption((context) => AppLocalizations.of(context)!.none, + ClockNumbersType.none), + ], + enableConditions: [ + ValueCondition(["clockType"], (value) => value == ClockType.analog), + ], + defaultValue: 1, + searchTags: ["analog", "digital", "face"], + ), + SelectSetting( + "numeralType", + (context) => AppLocalizations.of(context)!.numeralTypeSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.arabicNumeral, + ClockNumeralType.arabic), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.romanNumeral, + ClockNumeralType.roman), + ], + searchTags: ["roman", "arabic", "number", "numeral"], + enableConditions: [ + ValueCondition(["clockType"], (value) => value == ClockType.analog), + ValueCondition( + ["showNumbers"], (value) => value != ClockNumbersType.none) + ], + ), + SelectSetting( + "showTicks", + (context) => AppLocalizations.of(context)!.showClockTicksSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.allTicks, + ClockTicksType.all), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.majorTicks, + ClockTicksType.major), + SelectSettingOption((context) => AppLocalizations.of(context)!.none, + ClockTicksType.none), + ], + enableConditions: [ + ValueCondition(["clockType"], (value) => value == ClockType.analog), + ], + defaultValue: 1, + searchTags: ["ticks", "mark"], + ), + SwitchSetting( + 'showDigitalClock', + (context) => AppLocalizations.of(context)!.showDigitalClock, + false, + searchTags: ["digital", "time"], + enableConditions: [ + ValueCondition(["clockType"], (value) => value == ClockType.analog), + ], + ), + ], + // description: "Show comparison laps bars in stopwatch", + icon: Icons.palette_outlined, + searchTags: ["clock face", "ui"], + ), + ], + icon: FluxIcons.clock, +); diff --git a/lib/clock/logic/timezone_database.dart b/lib/clock/logic/timezone_database.dart index c6435da2..cf9ac33c 100644 --- a/lib/clock/logic/timezone_database.dart +++ b/lib/clock/logic/timezone_database.dart @@ -1,8 +1,12 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:clock_app/common/data/paths.dart'; +import 'package:path/path.dart'; // Database? database; Future initializeDatabases() async { @@ -12,10 +16,11 @@ Future initializeDatabases() async { if (FileSystemEntity.typeSync(timezonesDatabasePath) == FileSystemEntityType.notFound) { // Load database from asset and copy - ByteData data = await rootBundle.load('assets/timezones.db'); + ByteData data = await rootBundle.load(join('assets', 'timezones.db')); List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + logger.i('Copying timzones.db to $timezonesDatabasePath'); // Save copied asset to documents await File(timezonesDatabasePath).writeAsBytes(bytes); } diff --git a/lib/clock/screens/clock_screen.dart b/lib/clock/screens/clock_screen.dart index b6a28a26..7d14431e 100644 --- a/lib/clock/screens/clock_screen.dart +++ b/lib/clock/screens/clock_screen.dart @@ -1,12 +1,14 @@ import 'package:clock_app/clock/screens/search_city_screen.dart'; import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/clock/widgets/timezone_card.dart'; -import 'package:clock_app/common/widgets/clock/clock.dart'; +import 'package:clock_app/common/widgets/clock/analog_clock/analog_clock.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/navigation/types/alignment.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; class ClockScreen extends StatefulWidget { @@ -17,15 +19,17 @@ class ClockScreen extends StatefulWidget { } class _ClockScreenState extends State { - bool shouldShowSeconds = false; late Setting showSecondsSetting; + late Setting clockTypeSetting; + late Setting clockNumberTypeSetting; + late Setting clockNumeralTypeSetting; + late Setting clockTicksTypeSetting; + late Setting showDigitalClockSetting; final _listController = PersistentListController(); - void setShowSeconds(dynamic value) { - setState(() { - shouldShowSeconds = value; - }); + void update(dynamic) { + setState(() {}); } @override @@ -35,13 +39,32 @@ class _ClockScreenState extends State { .getGroup("General") .getGroup("Display") .getSetting("Show Seconds"); - setShowSeconds(showSecondsSetting.value); - showSecondsSetting.addListener(setShowSeconds); + SettingGroup clockStyleSettingGroup = + appSettings.getGroup("clock").getGroup("clockStyle"); + + clockTypeSetting = clockStyleSettingGroup.getSetting("clockType"); + clockNumberTypeSetting = clockStyleSettingGroup.getSetting("showNumbers"); + clockNumeralTypeSetting = clockStyleSettingGroup.getSetting("numeralType"); + clockTicksTypeSetting = clockStyleSettingGroup.getSetting("showTicks"); + showDigitalClockSetting = + clockStyleSettingGroup.getSetting("showDigitalClock"); + + showSecondsSetting.addListener(update); + clockTypeSetting.addListener(update); + clockNumberTypeSetting.addListener(update); + clockNumeralTypeSetting.addListener(update); + clockTicksTypeSetting.addListener(update); + showDigitalClockSetting.addListener(update); } @override void dispose() { - showSecondsSetting.removeListener(setShowSeconds); + showSecondsSetting.removeListener(update); + clockTypeSetting.removeListener(update); + clockNumberTypeSetting.removeListener(update); + clockNumeralTypeSetting.removeListener(update); + clockTicksTypeSetting.removeListener(update); + showDigitalClockSetting.removeListener(update); super.dispose(); } @@ -57,11 +80,19 @@ class _ClockScreenState extends State { Column(children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Clock( - shouldShowDate: true, - shouldShowSeconds: shouldShowSeconds, - horizontalAlignment: ElementAlignment.center, - ), + child: clockTypeSetting.value == ClockType.analog + ? AnalogClock( + showDigitalClock: showDigitalClockSetting.value, + showSeconds: showSecondsSetting.value, + numbersType: clockNumberTypeSetting.value, + numeralType: clockNumeralTypeSetting.value, + ticksType: clockTicksTypeSetting.value, + ) + : DigitalClock( + shouldShowDate: true, + shouldShowSeconds: showSecondsSetting.value, + horizontalAlignment: ElementAlignment.center, + ), ), // const SizedBox(height: 8), Expanded( @@ -69,10 +100,10 @@ class _ClockScreenState extends State { saveTag: 'favorite_cities', listController: _listController, itemBuilder: (city) => TimeZoneCard( - city: city, - onDelete: () => _listController.deleteItem(city)), + city: city, onDelete: () => _listController.deleteItem(city)), placeholderText: "No cities added", isDuplicateEnabled: false, + isSelectable: true, ), ), ]), diff --git a/lib/clock/screens/search_city_screen.dart b/lib/clock/screens/search_city_screen.dart index db172c7f..47989b2f 100644 --- a/lib/clock/screens/search_city_screen.dart +++ b/lib/clock/screens/search_city_screen.dart @@ -68,19 +68,21 @@ class _SearchCityScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: TextField( - autofocus: true, - controller: _filterController, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: - const OutlineInputBorder(borderSide: BorderSide.none), - fillColor: Colors.transparent, - hintText: AppLocalizations.of(context)!.searchCityPlaceholder, - hintStyle: Theme.of(context).textTheme.bodyLarge, + titleWidget: Expanded( + child: TextField( + autofocus: true, + controller: _filterController, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: + const OutlineInputBorder(borderSide: BorderSide.none), + fillColor: Colors.transparent, + hintText: AppLocalizations.of(context)!.searchCityPlaceholder, + hintStyle: Theme.of(context).textTheme.bodyLarge, + ), + textAlignVertical: TextAlignVertical.center, + style: Theme.of(context).textTheme.bodyLarge, ), - textAlignVertical: TextAlignVertical.center, - style: Theme.of(context).textTheme.bodyLarge, ), ), body: Padding( diff --git a/lib/clock/types/city.dart b/lib/clock/types/city.dart index 58898031..63d688a5 100644 --- a/lib/clock/types/city.dart +++ b/lib/clock/types/city.dart @@ -1,6 +1,6 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; -import 'package:flutter/material.dart'; +import 'package:clock_app/common/utils/id.dart'; class City extends ListItem { late String _name = "Unknown"; @@ -17,7 +17,7 @@ class City extends ListItem { @override bool get isDeletable => true; - City(this._name, this._country, this._timezone) : _id = UniqueKey().hashCode; + City(this._name, this._country, this._timezone) : _id = getId(); @override copy() { @@ -26,13 +26,13 @@ class City extends ListItem { City.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } _name = json['name'] ?? 'Unknown'; _country = json['country'] ?? 'Unknown'; _timezone = json['timezone'] ?? 'America/Detroit'; - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); } @override diff --git a/lib/clock/widgets/timezone_card.dart b/lib/clock/widgets/timezone_card.dart index f06be9d5..19113903 100644 --- a/lib/clock/widgets/timezone_card.dart +++ b/lib/clock/widgets/timezone_card.dart @@ -1,7 +1,7 @@ import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/common/utils/popup_action.dart'; import 'package:clock_app/common/widgets/card_edit_menu.dart'; -import 'package:clock_app/common/widgets/clock/clock.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; import 'package:flutter/material.dart'; import 'package:timer_builder/timer_builder.dart'; import 'package:timezone/timezone.dart' as timezone; @@ -64,7 +64,7 @@ class TimeZoneCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Clock( + DigitalClock( timezoneLocation: _timezoneLocation, scale: 0.4, ), diff --git a/lib/clock/widgets/timezone_card_content.dart b/lib/clock/widgets/timezone_card_content.dart index ab86302f..6b48e3c4 100644 --- a/lib/clock/widgets/timezone_card_content.dart +++ b/lib/clock/widgets/timezone_card_content.dart @@ -1,4 +1,4 @@ -import 'package:clock_app/common/widgets/clock/clock.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; import 'package:flutter/material.dart'; import 'package:timer_builder/timer_builder.dart'; import 'package:timezone/timezone.dart' as timezone; @@ -57,7 +57,7 @@ class TimezoneCardContent extends StatelessWidget { const SizedBox(width: 8), Column( children: [ - Clock( + DigitalClock( timezoneLocation: timezoneLocation, scale: 0.3, color: textColor, diff --git a/lib/clock/widgets/timezone_search_card.dart b/lib/clock/widgets/timezone_search_card.dart index 5cbe33f9..2582b3c5 100644 --- a/lib/clock/widgets/timezone_search_card.dart +++ b/lib/clock/widgets/timezone_search_card.dart @@ -1,8 +1,7 @@ import 'package:clock_app/clock/types/city.dart'; -import 'package:clock_app/clock/widgets/timezone_card_content.dart'; import 'package:clock_app/common/utils/snackbar.dart'; import 'package:clock_app/common/widgets/card_container.dart'; -import 'package:clock_app/common/widgets/clock/clock.dart'; +import 'package:clock_app/common/widgets/clock/digital_clock.dart'; import 'package:flutter/material.dart'; import 'package:timer_builder/timer_builder.dart'; import 'package:timezone/timezone.dart' as timezone; @@ -83,7 +82,7 @@ class TimeZoneSearchCard extends StatelessWidget { const SizedBox(width: 8), Column( children: [ - Clock( + DigitalClock( timezoneLocation: timezoneLocation, scale: 0.3, color: textColor, diff --git a/lib/common/data/animations.dart b/lib/common/data/animations.dart new file mode 100644 index 00000000..43484c46 --- /dev/null +++ b/lib/common/data/animations.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +extension AnimateWidgetExtensions on Widget { + Animate animateCard(dynamic key) => animate(delay: 50.ms, key: key) + .slideY(begin: 0.15, end: 0, duration: 150.ms, curve: Curves.easeOut) + .fade(duration: 150.ms, curve: Curves.easeOut); + +} + +extension AnimateListExtensions on List { + /// Wraps the target `List` in an [AnimateList] instance, and returns + /// the instance for chaining calls. + /// Ex. `[foo, bar].animate()` is equivalent to `AnimateList(children: [foo, bar])`. + AnimateList animateCardList() => animate(interval: 100.ms) + .slideY(begin: 0.15, end: 0, duration: 150.ms, curve: Curves.easeOut) + .fade(duration: 150.ms, curve: Curves.easeOut); +} diff --git a/lib/common/data/paths.dart b/lib/common/data/paths.dart index b908ad93..b03dd29a 100644 --- a/lib/common/data/paths.dart +++ b/lib/common/data/paths.dart @@ -41,3 +41,11 @@ String getRingtonesDirectoryPathSync() { Future getTimezonesDatabasePath() async { return path.join(await getAppDataDirectoryPath(), 'timezones.db'); } + +Future getLogsFilePath() async { + return path.join(await getAppDataDirectoryPath(), "logs.txt"); +} + +String getLogsFilePathSync(){ + return path.join(getAppDataDirectoryPathSync(), "logs.txt"); +} diff --git a/lib/common/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart index 28246179..e2fdfe93 100644 --- a/lib/common/logic/card_decoration.dart +++ b/lib/common/logic/card_decoration.dart @@ -4,15 +4,22 @@ import 'package:flutter/material.dart'; BoxDecoration getCardDecoration(BuildContext context, {Color? color, bool showLightBorder = false, + bool isSelected = false, showShadow = true, elevationMultiplier = 1, + boxShape = BoxShape.rectangle, blurStyle = BlurStyle.normal}) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; ThemeStyleExtension? themeStyle = theme.extension(); return BoxDecoration( - border: showLightBorder + border: isSelected ? Border.all( + color: colorScheme.primary, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside + + ) : showLightBorder ? Border.all( color: colorScheme.outline.withOpacity(0.2), width: 0.5, @@ -26,9 +33,11 @@ BoxDecoration getCardDecoration(BuildContext context, ) : null, color: color ?? colorScheme.surface, - borderRadius: theme.cardTheme.shape != null + borderRadius: boxShape == BoxShape.rectangle? theme.cardTheme.shape != null ? (theme.cardTheme.shape as RoundedRectangleBorder).borderRadius - : const BorderRadius.all(Radius.circular(8.0)), + : const BorderRadius.all(Radius.circular(8.0)) : null, + shape: boxShape, + boxShadow: [ if (showShadow && (themeStyle?.shadowOpacity ?? 0) > 0) BoxShadow( diff --git a/lib/common/logic/show_select.dart b/lib/common/logic/show_select.dart index b060d6c8..316895e3 100644 --- a/lib/common/logic/show_select.dart +++ b/lib/common/logic/show_select.dart @@ -1,6 +1,7 @@ import 'package:clock_app/common/types/popup_action.dart'; import 'package:clock_app/common/types/select_choice.dart'; import 'package:clock_app/common/widgets/fields/select_field/select_bottom_sheet.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/material.dart'; Future showSelectBottomSheet( @@ -40,7 +41,7 @@ Future showSelectBottomSheet( if (indices.length == 1) { currentSelectedIndices = [indices[0]]; } else { - debugPrint("Too many indices"); + logger.e("Too many indices in select bottom sheet"); } } }); diff --git a/lib/common/types/clock_settings_types.dart b/lib/common/types/clock_settings_types.dart new file mode 100644 index 00000000..4bd986ff --- /dev/null +++ b/lib/common/types/clock_settings_types.dart @@ -0,0 +1,13 @@ +enum ClockNumeralType{ + arabic, + roman, +} + +enum ClockTicksType{ + none, + major, + all, +} + +enum ClockNumbersType { none, quarter, all } + diff --git a/lib/common/types/file_item.dart b/lib/common/types/file_item.dart index a8bec429..d95a7e1d 100644 --- a/lib/common/types/file_item.dart +++ b/lib/common/types/file_item.dart @@ -2,8 +2,7 @@ import 'dart:convert'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; -import 'package:clock_app/common/types/timer_state.dart'; -import 'package:flutter/material.dart'; +import 'package:clock_app/common/utils/id.dart'; enum FileItemType { audio, @@ -33,14 +32,14 @@ class FileItem extends ListItem { bool get isDeletable => _isDeletable; FileItem(this.name, this._uri, this._type, {isDeletable = true}) - : _id = UniqueKey().hashCode, + : _id = getId(), _isDeletable = isDeletable; @override FileItem.fromJson(Json json) : _id = json != null - ? json['id'] ?? UniqueKey().hashCode - : UniqueKey().hashCode, + ? json['id'] ?? getId() + : getId(), _type = json != null ? json['type'] != null ? FileItemType.values diff --git a/lib/common/types/list_filter.dart b/lib/common/types/list_filter.dart index 7b7abe59..f454136d 100644 --- a/lib/common/types/list_filter.dart +++ b/lib/common/types/list_filter.dart @@ -1,20 +1,21 @@ +import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; -import 'package:clock_app/common/utils/debug.dart'; +import 'package:clock_app/common/utils/id.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ListSortOption { final String Function(BuildContext) getLocalizedName; - // final String abbreviation; final int Function(Item, Item) sortFunction; - String Function(BuildContext) get displayName => getLocalizedName; - - const ListSortOption( - this.getLocalizedName, this.sortFunction); + const ListSortOption(this.getLocalizedName, this.sortFunction); } abstract class ListFilterItem { + bool isEnabled = true; + bool Function(Item) get filterFunction; String Function(BuildContext) get displayName; bool get isActive; @@ -37,14 +38,13 @@ class ListFilter extends ListFilterItem { ListFilter(this.getLocalizedName, bool Function(Item) filterFunction, {int? id}) - : _id = id ?? UniqueKey().hashCode, + : _id = id ?? getId(), _filterFunction = filterFunction; int get id => _id; @override bool Function(Item) get filterFunction { - // print("Filtering $name $isSelected"); return isSelected ? _filterFunction : (Item item) => true; } @@ -63,6 +63,7 @@ class ListFilter extends ListFilterItem { class ListFilterSearch extends ListFilterItem { final String Function(BuildContext) getLocalizedName; String searchText = ''; + @override bool Function(Item) get filterFunction { // if (searchText.isEmpty) { @@ -198,7 +199,7 @@ abstract class FilterSelect try { return selectedFilter.filterFunction; } catch (e) { - printDebug("Error in getting filter function($displayName): $e"); + logger.d("Error in getting filter function($displayName): $e"); return (Item item) => true; } } diff --git a/lib/common/types/tag.dart b/lib/common/types/tag.dart index 9a921aeb..0e1fae0e 100644 --- a/lib/common/types/tag.dart +++ b/lib/common/types/tag.dart @@ -1,5 +1,6 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:flutter/material.dart'; class Tag extends ListItem { @@ -8,16 +9,16 @@ class Tag extends ListItem { String description; Color color; Tag(this.name, {this.description = "", this.color = Colors.blue}) - : _id = UniqueKey().hashCode; + : _id = getId(); Tag.fromJson(Json json) - : _id = json?['id'] ?? UniqueKey().hashCode, + : _id = json?['id'] ?? getId(), name = json?['name'] ?? "Unknown", description = json?['description'] ?? "", color = Color(json?['color'] ?? 0); Tag.from(Tag tag) - : _id = UniqueKey().hashCode, + : _id = getId(), name = tag.name, description = tag.description, color = tag.color; @@ -48,4 +49,10 @@ class Tag extends ListItem { description = other.description; color = other.color; } + + bool isEqualTo(Tag other) { + return name == other.name && + description == other.description && + color == other.color; + } } diff --git a/lib/common/types/time.dart b/lib/common/types/time.dart index 4cd27f5c..74f29bce 100644 --- a/lib/common/types/time.dart +++ b/lib/common/types/time.dart @@ -69,4 +69,18 @@ class Time extends JsonSerializable { return time >= startTime || time <= endTime; } } + + @override + bool operator ==(Object other) { + if (other is Time) { + return hour == other.hour && + minute == other.minute && + second == other.second; + } + return false; + } + + @override + int get hashCode => Object.hash(hour, minute, second); + } diff --git a/lib/common/utils/debug.dart b/lib/common/utils/debug.dart deleted file mode 100644 index f47f7237..00000000 --- a/lib/common/utils/debug.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:isolate'; - -import 'package:flutter/foundation.dart'; - -void printIsolateInfo() { - printDebug( - "Isolate: ${Isolate.current.debugName}, id: ${Isolate.current.hashCode}"); -} - -void printDebug(String message) { - if (kDebugMode) { - print(message); - } -} diff --git a/lib/common/utils/id.dart b/lib/common/utils/id.dart new file mode 100644 index 00000000..36c50288 --- /dev/null +++ b/lib/common/utils/id.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +int getId() { + return UniqueKey().hashCode; +} diff --git a/lib/common/utils/json_serialize.dart b/lib/common/utils/json_serialize.dart index 55c8016d..836b0269 100644 --- a/lib/common/utils/json_serialize.dart +++ b/lib/common/utils/json_serialize.dart @@ -9,6 +9,8 @@ import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/types/time.dart'; import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/stopwatch/types/stopwatch.dart'; import 'package:clock_app/theme/types/color_scheme.dart'; import 'package:clock_app/theme/types/style_theme.dart'; @@ -27,6 +29,7 @@ final fromJsonFactories = { StyleTheme: (Json json) => StyleTheme.fromJson(json), AlarmTask: (Json json) => AlarmTask.fromJson(json), Time: (Json json) => Time.fromJson(json), + Lap: (Json json) => Lap.fromJson(json), TimeOfDay: (Json json) => TimeOfDayUtils.fromJson(json), FileItem: (Json json) => FileItem.fromJson(json), AlarmEvent: (Json json) => AlarmEvent.fromJson(json), @@ -34,21 +37,22 @@ final fromJsonFactories = { Tag: (Json json) => Tag.fromJson(json), }; - String listToString(List items) => json.encode( items.map((item) => item.toJson()).toList(), ); List listFromString(String encodedItems) { if (!fromJsonFactories.containsKey(T)) { - throw Exception("No fromJson factory for type '$T'"); + throw Exception( + "No fromJson factory for type '$T'. Please add one in the file 'common/utils/json_serialize.dart'"); } try { - return (json.decode(encodedItems) as List) - .map((json) => fromJsonFactories[T]!(json)) - .toList(); + List rawList = json.decode(encodedItems) as List; + Function fromJson = fromJsonFactories[T]!; + List list = rawList.map((json) => fromJson(json)).toList(); + return list; } catch (e) { - debugPrint("Error decoding string: ${e.toString()}"); + logger.e("Error decoding string: ${e.toString()}"); rethrow; } } diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart index 437e38c4..d3584759 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; -import 'package:flutter/material.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:get_storage/get_storage.dart'; import 'package:path/path.dart' as path; -import 'package:path/path.dart'; import 'package:queue/queue.dart'; import 'package:watcher/watcher.dart'; @@ -47,10 +47,10 @@ void unwatchList(String key) { } List loadListSync(String key) { - try{ - return listFromString(loadTextFileSync(key)); - }catch(e){ - debugPrint("Error loading list ($key): $e"); + try { + return listFromString(loadTextFileSync(key)); + } catch (e) { + logger.e("Error loading list ($key): $e"); return []; } } @@ -65,15 +65,15 @@ Future saveList( } Future initList( - String key, List value) async { - await initTextFile(key, listToString(value)); + String key, List list) async { + await initTextFile(key, listToString(list)); } Future initTextFile(String key, String value) async { if (GetStorage().read('init_$key') == null) { GetStorage().write('init_$key', true); - if(!textFileExistsSync(key)){ - debugPrint("Initializing $key"); + if (!textFileExistsSync(key)) { + logger.i("Initializing $key"); await saveTextFile(key, value); } } @@ -90,13 +90,20 @@ Future saveTextFile(String key, String content) async { }); } -Future saveRingtone(String id, String sourceUri) async { +Future saveRingtone(String id, Uint8List data) async { String ringtonesDirectory = getRingtonesDirectoryPathSync(); - File source = File(sourceUri); String newPath = path.join(ringtonesDirectory, id); + + File file = File(newPath); + await queue.add(() async { - await source.copy(newPath); + if (!file.existsSync()) { + file.createSync(recursive: true); + } + + await file.writeAsBytes(data, mode: FileMode.writeOnly); }); + return newPath; } @@ -123,7 +130,6 @@ Future loadTextFile(String key) async { return file.readAsString(); } else { return '[]'; - } }); return content; diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index 267c2dfd..5cf3330f 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -1,14 +1,49 @@ import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/theme/types/theme_extension.dart'; import 'package:flutter/material.dart'; void showSnackBar(BuildContext context, String text, - {bool fab = false, bool navBar = false}) { + {bool fab = false, bool navBar = false, bool error = false}) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + Color? color = error ? colorScheme.error : null; + ThemeSettingExtension themeSettings = + theme.extension()!; + + Duration duration = + error ? const Duration(hours: 999) : const Duration(seconds: 4); ScaffoldMessenger.of(context).removeCurrentSnackBar(); - ScaffoldMessenger.of(context) - .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar)); + ScaffoldMessenger.of(context).showSnackBar(getSnackbar(text, + fab: fab, + navBar: navBar, + color: color, + useMaterialStyle: themeSettings.useMaterialStyle, + duration: duration)); } -SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) { +SnackBar getThemedSnackBar(BuildContext context, String text, + {bool fab = false, + bool navBar = false, + Color? color, + Duration duration = const Duration(seconds: 4)}) { + ThemeData theme = Theme.of(context); + ThemeSettingExtension themeSettings = + theme.extension()!; + + return getSnackbar(text, + fab: fab, + navBar: navBar, + color: color, + useMaterialStyle: themeSettings.useMaterialStyle, + duration: duration); +} + +SnackBar getSnackbar(String text, + {bool fab = false, + bool navBar = false, + bool useMaterialStyle = false, + Color? color, + Duration duration = const Duration(seconds: 4)}) { double left = 20; double right = 20; double bottom = 12; @@ -29,31 +64,27 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) { bottom = 4; } - final useMaterialStyle = appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Use Material Style") - .value; - if (useMaterialStyle) { bottom += 20; } return SnackBar( - content: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 28), - child: Container( - alignment: Alignment.centerLeft, - // height: 28, - child: Text(text), - ), + content: Container( + constraints: const BoxConstraints(minHeight: 56), + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + color: color, + // height: 28, + child: Text(text), ), margin: EdgeInsets.only( left: left, right: right, bottom: bottom, ), + padding: EdgeInsets.zero, elevation: 2, - dismissDirection: DismissDirection.none, + dismissDirection: DismissDirection.vertical, + duration: duration, ); } diff --git a/lib/common/widgets/animated_show_hide.dart b/lib/common/widgets/animated_show_hide.dart new file mode 100644 index 00000000..2a78adbe --- /dev/null +++ b/lib/common/widgets/animated_show_hide.dart @@ -0,0 +1,297 @@ +import 'package:flutter/material.dart'; + +/// A typedef for a custom animation transition builder used by +/// [AnimatedShowHide] and [AnimatedShowHideChild]. +/// +/// The [AnimatedShowHideTransitionBuilder] typedef represents a function that +/// takes the current build context, an animation object, and the child widget as +/// arguments and returns a widget. This function allows for custom animation +/// transitions when showing or hiding a child widget. +/// +/// The animation object provides information about the current state of the +/// animation, including the value, which ranges from 0.0 to 1.0. You can use +/// this information to control the appearance and behavior of the child widget +/// during the transition. +/// +/// {@tool snippet} +/// This example shows how to use a custom animation transition builder to +/// create a fade-in/fade-out animation. +/// +/// ```dart +/// AnimatedShowHide( +/// child: const Text('Hello World!'), +/// transitionBuilder: (context, animation, child) { +/// return FadeTransition( +/// opacity: animation, +/// child: child, +/// ); +/// }, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [AnimatedShowHide] +/// * [AnimatedShowHideChild] +/// * [FadeTransition] +typedef AnimatedShowHideTransitionBuilder = Widget Function( + BuildContext context, + Animation animation, + Widget? child, +); + +/// A widget that manages the showing and hiding of a child widget based on +/// animation. +/// +/// The [AnimatedShowHide] widget uses an [AnimationController] to animate the +/// showing and hiding of its child widget. The animation is controlled by the +/// [animate] property, which determines whether the child widget should be shown +/// or hidden. +/// +/// The animation can be customized using the [duration], [curve], [axis], and +/// [axisAlignment] properties. The [transitionBuilder] property can be used to +/// provide a custom animation transition. +/// +/// {@tool snippet} +/// This example shows how to use the [AnimatedShowHide] widget to animate the +/// showing and hiding of a child widget. +/// +/// ```dart +/// AnimatedShowHide( +/// child: const Text('Hello World!'), +/// animate: true, +/// duration: const Duration(seconds: 1), +/// curve: Curves.bounceInOut, +/// axis: Axis.horizontal, +/// axisAlignment: 0.5, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [AnimatedShowHideChild] +/// * [AnimatedShowHideTransitionBuilder] +/// * [SizeTransition] +/// * [FadeTransition] +class AnimatedShowHide extends StatelessWidget { + /// Creates a new [AnimatedShowHide] widget. + /// + /// The [child] property is the widget to be shown or hidden. The [animate] + /// property determines whether the child widget should be shown or hidden. The + /// [duration], [curve], [axis], and [axisAlignment] properties can be used to + /// customize the animation. The [transitionBuilder] property can be used to + /// provide a custom animation transition. + const AnimatedShowHide({ + this.child, + this.animate = true, + this.duration = const Duration(milliseconds: 180), + this.curve = Curves.ease, + this.axis = Axis.vertical, + this.axisAlignment = -1, + this.transitionBuilder, + super.key, + }); + + /// The widget to be shown or hidden. + final Widget? child; + + /// Whether to animate the showing and hiding of the child widget. + final bool animate; + + /// The duration of the animation. + final Duration duration; + + /// The curve of the animation. + final Curve curve; + + /// The axis of the animation. + final Axis axis; + + /// The axis alignment of the animation. + final double axisAlignment; + + /// A custom animation transition builder. + final AnimatedShowHideTransitionBuilder? transitionBuilder; + + Widget buildAnimationWidget(BuildContext context) { + return AnimatedShowHideChild( + transitionBuilder: transitionBuilder, + duration: duration, + curve: curve, + axis: axis, + axisAlignment: axisAlignment, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + if (animate) { + return buildAnimationWidget(context); + } + return child ?? const SizedBox(); + } +} + +/// A widget that manages the showing and hiding of a child widget based on animation. +/// +/// The [AnimatedShowHideChild] widget uses an [AnimationController] to animate the +/// showing and hiding of its child widget. +/// +/// The animation can be customized using the [duration], [curve], [axis], and +/// [axisAlignment] properties. The [transitionBuilder] property can be used to +/// provide a custom animation transition. +/// +/// {@tool snippet} +/// This example shows how to use the [AnimatedShowHideChild] widget to animate the +/// showing and hiding of a child widget. +/// +/// ```dart +/// AnimatedShowHideChild( +/// child: show ? const Text('Hello World!') : null, +/// animate: true, +/// duration: const Duration(seconds: 1), +/// curve: Curves.bounceInOut, +/// axis: Axis.horizontal, +/// axisAlignment: 0.5, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [AnimatedShowHide] +/// * [AnimatedShowHideTransitionBuilder] +/// * [SizeTransition] +/// * [FadeTransition] +class AnimatedShowHideChild extends StatefulWidget { + /// Creates a new [AnimatedShowHideChild] widget. + /// + /// The [child] property is the widget to be shown or hidden. The + /// [duration], [curve], [axis], and [axisAlignment] properties can be used to + /// customize the animation. The [transitionBuilder] property can be used to + /// provide a custom animation transition. + const AnimatedShowHideChild({ + this.child, + this.duration = const Duration(milliseconds: 180), + this.curve = Curves.ease, + this.axis = Axis.vertical, + this.axisAlignment = -1, + this.transitionBuilder, + super.key, + }); + + /// The widget to be shown or hidden. + final Widget? child; + + /// The duration of the animation. + final Duration duration; + + /// The curve of the animation. + final Curve curve; + + /// The axis of the animation. + final Axis axis; + + /// The axis alignment of the animation. + final double axisAlignment; + + /// A custom animation transition builder. + final AnimatedShowHideTransitionBuilder? transitionBuilder; + + @override + State createState() => _AnimatedShowHideChildState(); +} + +class _AnimatedShowHideChildState extends State + with SingleTickerProviderStateMixin { + AnimationController? controller; + late Animation animation; + + void _listener() { + if (controller?.isDismissed ?? false) { + setState(() { + outGoingChild = const SizedBox(); + }); + } + } + + Widget outGoingChild = const SizedBox(); + + @override + void initState() { + controller ??= AnimationController(vsync: this, duration: widget.duration); + controller!.addListener(_listener); + animation = CurvedAnimation( + parent: controller!.drive(Tween(begin: 0, end: 1)), + curve: widget.curve, + ); + controller!.forward(); + super.initState(); + } + + @override + void dispose() { + controller?.removeListener(_listener); + controller?.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant AnimatedShowHideChild oldWidget) { + super.didUpdateWidget(oldWidget); + animatedOnChanges(oldWidget); + } + + // This method manages the changes in the animated widget and ensures the appropriate actions are taken based on the properties of the current and previous widgets. + // + // If the `transitionBuilder` property of the current widget is null, it checks the `child` property of the previous widget. If the previous child is not null, it sets `_outGoingChild` to the previous child; otherwise, it sets it to `SizedBox()`. + // + // If the `child` property of the current widget is null, it calls `reverse()` on `_controller`; otherwise, it calls `forward()`. + // + // If the `transitionBuilder` property is not null, it checks and sets `_outGoingChild` based on the transition builder's call with the context, animation, and child properties. It then decides whether to call `reverse()` or `forward()` on `_controller` based on the transition builder's call result. + void animatedOnChanges(covariant AnimatedShowHideChild oldWidget) { + if (widget.transitionBuilder == null) { + if (oldWidget.child != null) { + outGoingChild = oldWidget.child ?? const SizedBox(); + } + if (widget.child == null) { + controller?.reverse(); + } else { + controller?.forward(); + } + } else { + if (oldWidget.transitionBuilder?.call(context, animation, widget.child) != + null) { + outGoingChild = oldWidget.transitionBuilder + ?.call(context, animation, widget.child) ?? + const SizedBox(); + } + if (widget.transitionBuilder?.call(context, animation, widget.child) == + null) { + controller?.reverse(); + } else { + controller?.forward(); + } + } + } + + @override + Widget build(BuildContext context) { + if (widget.transitionBuilder != null) { + return widget.transitionBuilder!( + context, + animation, + widget.child, + ); + } + return SizeTransition( + sizeFactor: animation, + axisAlignment: widget.axisAlignment, + axis: widget.axis, + child: widget.child ?? outGoingChild, + ); + } +} diff --git a/lib/common/widgets/card_container.dart b/lib/common/widgets/card_container.dart index b430f80c..eab7ca13 100644 --- a/lib/common/widgets/card_container.dart +++ b/lib/common/widgets/card_container.dart @@ -1,31 +1,28 @@ import 'package:clock_app/common/logic/card_decoration.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/theme/types/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:clock_app/common/utils/color.dart'; import 'package:material_color_utilities/hct/hct.dart'; import 'package:material_color_utilities/palettes/tonal_palette.dart'; - TonalPalette toTonalPalette(int value) { final color = Hct.fromInt(value); return TonalPalette.of(color.hue, color.chroma); } -Color getCardColor(BuildContext context, [Color? color]){ - ColorScheme colorScheme = Theme.of(context).colorScheme; - bool useMaterialYou = appSettings - .getGroup("Appearance") - .getGroup("Colors") - .getSetting("Use Material You") - .value; +Color getCardColor(BuildContext context, [Color? color]) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + ThemeSettingExtension themeStyle = theme.extension()!; TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); return color ?? - (useMaterialYou - ? Color(tonalPalette.get( - Theme.of(context).brightness == Brightness.light ? 96 : 15)) - : colorScheme.surface); + (themeStyle.useMaterialYou + ? Color(tonalPalette + .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) + : colorScheme.surface); } class CardContainer extends StatelessWidget { @@ -38,9 +35,10 @@ class CardContainer extends StatelessWidget { this.onTap, this.alignment, this.showShadow = true, - + this.isSelected = false, this.showLightBorder = false, this.blurStyle = BlurStyle.normal, + this.onLongPress, }); final Widget child; @@ -48,19 +46,12 @@ class CardContainer extends StatelessWidget { final Color? color; final EdgeInsetsGeometry? margin; final VoidCallback? onTap; + final VoidCallback? onLongPress; final Alignment? alignment; final bool showShadow; final BlurStyle blurStyle; final bool showLightBorder; - - // TonalPalette primaryTonalP = toTonalPalette(_primaryColor); - // primaryTonalP.get(50); // Getting the specific color - // - // - // TonalPalette toTonalPalette(int value) { - // final color = Hct.fromInt(value); - // return TonalPalette.of(color.hue, color.chroma); - // } + final bool isSelected; @override Widget build(BuildContext context) { @@ -68,12 +59,14 @@ class CardContainer extends StatelessWidget { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; return Container( + // duration: const Duration(milliseconds: 100), alignment: alignment, margin: margin ?? const EdgeInsets.all(4), clipBehavior: Clip.hardEdge, decoration: getCardDecoration( context, color: cardColor, + isSelected: isSelected, showLightBorder: showLightBorder, showShadow: showShadow, elevationMultiplier: elevationMultiplier, @@ -84,6 +77,7 @@ class CardContainer extends StatelessWidget { : Material( color: Colors.transparent, child: InkWell( + onLongPress: onLongPress, onTap: onTap, splashColor: cardColor.darken(0.075), borderRadius: Theme.of(context).toggleButtonsTheme.borderRadius, diff --git a/lib/common/widgets/clock/analog_clock/analog_clock.dart b/lib/common/widgets/clock/analog_clock/analog_clock.dart new file mode 100644 index 00000000..320d9a40 --- /dev/null +++ b/lib/common/widgets/clock/analog_clock/analog_clock.dart @@ -0,0 +1,68 @@ +import 'package:clock_app/common/logic/card_decoration.dart'; +import 'package:clock_app/common/types/clock_settings_types.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/common/widgets/clock/analog_clock/analog_clock_display.dart'; +import 'package:flutter/material.dart'; +import 'package:timer_builder/timer_builder.dart'; +import 'package:timezone/timezone.dart' as timezone; + +class AnalogClock extends StatelessWidget { + final bool showDigitalClock; + final ClockTicksType ticksType; + final ClockNumbersType numbersType; + final ClockNumeralType numeralType; + final timezone.Location? timezoneLocation; + final bool showSeconds; + + const AnalogClock({ + super.key, + this.showDigitalClock = false, + this.ticksType = ClockTicksType.none, + this.numbersType = ClockNumbersType.quarter, + this.numeralType = ClockNumeralType.arabic, + this.showSeconds = false, + this.timezoneLocation, + }); + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + + return TimerBuilder.periodic(const Duration(seconds: 1), + builder: (context) { + DateTime dateTime; + if (timezoneLocation != null) { + dateTime = timezone.TZDateTime.now(timezoneLocation!); + } else { + dateTime = DateTime.now(); + } + return Column( + children: [ + AnalogClockDisplay( + decoration: getCardDecoration(context, + color: getCardColor(context), boxShape: BoxShape.circle), + width: 220.0, + height: 220.0, + isLive: true, + hourHandColor: colorScheme.onSurface, + minuteHandColor: colorScheme.onSurface, + secondHandColor: colorScheme.primary, + showSecondHand: showSeconds, + numberColor: colorScheme.onSurface, + numbersType: numbersType, + ticksType: ticksType, + tickColor: colorScheme.onSurface.withOpacity(0.6), + numeralType: numeralType, + textScaleFactor: 1.4, + digitalClockColor: colorScheme.onSurface.withOpacity(0.6), + showDigitalClock: showDigitalClock, + dateTime: dateTime, + // showTicksInsteadOfMinorNumbers: true, + // dateTime: DateTime(2019, 1, 1, 9, 12, 15), + ), + ], + ); + }); + } +} diff --git a/lib/common/widgets/clock/analog_clock/analog_clock_display.dart b/lib/common/widgets/clock/analog_clock/analog_clock_display.dart new file mode 100644 index 00000000..42e8976f --- /dev/null +++ b/lib/common/widgets/clock/analog_clock/analog_clock_display.dart @@ -0,0 +1,84 @@ +library analog_clock; + +import 'dart:async'; +import 'package:clock_app/common/types/clock_settings_types.dart'; +import 'package:flutter/material.dart'; +import 'analog_clock_painter.dart'; + +class AnalogClockDisplay extends StatelessWidget { + final DateTime dateTime; + final bool showDigitalClock; + final ClockTicksType ticksType; + final ClockNumbersType numbersType; + final ClockNumeralType numeralType; + final bool showSecondHand; + final bool useMilitaryTime; + final Color hourHandColor; + final Color minuteHandColor; + final Color secondHandColor; + final Color tickColor; + final Color digitalClockColor; + final Color numberColor; + final bool showTicksInsteadOfMinorNumbers; + final double textScaleFactor; + final double width; + final double height; + final BoxDecoration decoration; + + const AnalogClockDisplay( + {required this.dateTime, + this.showDigitalClock = true, + this.ticksType = ClockTicksType.none, + this.numbersType = ClockNumbersType.quarter, + this.numeralType = ClockNumeralType.arabic, + this.showSecondHand = true, + this.useMilitaryTime = true, + this.hourHandColor = Colors.black, + this.minuteHandColor = Colors.black, + this.secondHandColor = Colors.redAccent, + this.tickColor = Colors.grey, + this.digitalClockColor = Colors.black, + this.numberColor = Colors.black, + this.textScaleFactor = 1.0, + this.showTicksInsteadOfMinorNumbers = false, + this.width = double.infinity, + this.height = double.infinity, + this.decoration = const BoxDecoration(), + isLive, + super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: decoration, + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: Container( + constraints: + const BoxConstraints(minWidth: 48.0, minHeight: 48.0), + width: double.infinity, + child: CustomPaint( + painter: AnalogClockPainter( + dateTime: dateTime, + numbersType: numbersType, + numeralType: numeralType, + ticksType: ticksType, + showDigitalClock: showDigitalClock, + showTicksInsteadOfMinorNumbers: + showTicksInsteadOfMinorNumbers, + showSecondHand: showSecondHand, + useMilitaryTime: useMilitaryTime, + hourHandColor: hourHandColor, + minuteHandColor: minuteHandColor, + secondHandColor: secondHandColor, + tickColor: tickColor, + digitalClockColor: digitalClockColor, + textScaleFactor: textScaleFactor, + numberColor: numberColor), + )))), + ); + } +} diff --git a/lib/common/widgets/clock/analog_clock/analog_clock_painter.dart b/lib/common/widgets/clock/analog_clock/analog_clock_painter.dart new file mode 100644 index 00000000..3aefe96d --- /dev/null +++ b/lib/common/widgets/clock/analog_clock/analog_clock_painter.dart @@ -0,0 +1,254 @@ +import 'package:clock_app/common/types/clock_settings_types.dart'; +import 'package:flutter/material.dart'; +import 'dart:math'; + +const List romanNumerals = [ + 'I', + 'II', + 'III', + 'IV', + 'V', + 'VI', + 'VII', + 'VIII', + 'IX', + 'X', + 'XI', + 'XII' +]; + +const List arabicNumerals = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12' +]; + +class AnalogClockPainter extends CustomPainter { + DateTime dateTime; + final bool showDigitalClock; + final ClockTicksType ticksType; + final ClockNumbersType numbersType; + final ClockNumeralType numeralType; + final bool showSecondHand; + final bool showTicksInsteadOfMinorNumbers; + final bool useMilitaryTime; + final Color hourHandColor; + final Color minuteHandColor; + final Color secondHandColor; + final Color tickColor; + final Color digitalClockColor; + final Color numberColor; + final double textScaleFactor; + + static const double baseSize = 320.0; + static const double minutesInHour = 60.0; + static const double secondsInMinute = 60.0; + static const double hoursInClock = 12.0; + static const double handPinHoleSize = 8.0; + static const double strokeWidth = 3.0; + + AnalogClockPainter( + {required this.dateTime, + this.numbersType = ClockNumbersType.quarter, + this.numeralType = ClockNumeralType.arabic, + this.showDigitalClock = true, + this.ticksType = ClockTicksType.none, + this.showSecondHand = true, + this.hourHandColor = Colors.black, + this.minuteHandColor = Colors.black, + this.secondHandColor = Colors.redAccent, + this.tickColor = Colors.grey, + this.showTicksInsteadOfMinorNumbers = false, + this.digitalClockColor = Colors.black, + this.numberColor = Colors.black, + this.textScaleFactor = 1.0, + this.useMilitaryTime = true}); + + @override + void paint(Canvas canvas, Size size) { + double scaleFactor = size.shortestSide / baseSize; + + if (ticksType != ClockTicksType.none) { + _paintTickMarks(canvas, size, scaleFactor); + } + if (numbersType != ClockNumbersType.none) { + _drawNumbers(canvas, size, scaleFactor); + } + + if (showDigitalClock) { + _paintDigitalClock(canvas, size, scaleFactor, useMilitaryTime); + } + + _paintClockHands(canvas, size, scaleFactor); + _paintPinHole(canvas, size, scaleFactor); + } + + @override + bool shouldRepaint(AnalogClockPainter oldDelegate) { + return oldDelegate.dateTime.isBefore(dateTime); + } + + _paintPinHole(canvas, size, scaleFactor) { + Paint midPointStrokePainter = Paint() + ..color = showSecondHand ? secondHandColor : minuteHandColor + ..strokeWidth = strokeWidth * scaleFactor + ..isAntiAlias = true + ..style = PaintingStyle.stroke; + + canvas.drawCircle(size.center(Offset.zero), handPinHoleSize * scaleFactor, + midPointStrokePainter); + } + + void _drawNumbers(Canvas canvas, Size size, double scaleFactor) { + TextStyle style = TextStyle( + color: numberColor, + fontWeight: FontWeight.bold, + fontSize: 18.0 * scaleFactor * textScaleFactor); + double p = 30.0; + if (ticksType != ClockTicksType.none) p += 12.0; + + double r = size.shortestSide / 2; + double longHandLength = r - (p * scaleFactor); + + List numerals = + numeralType == ClockNumeralType.roman ? romanNumerals : arabicNumerals; + + for (var h = 1; h <= 12; h++) { + if (numbersType != ClockNumbersType.all && h % 3 != 0) continue; + double angle = (h * pi / 6) - pi / 2; //+ pi / 2; + Offset offset = + Offset(longHandLength * cos(angle), longHandLength * sin(angle)); + TextSpan span = TextSpan(style: style, text: numerals[h - 1].toString()); + TextPainter tp = TextPainter( + text: span, + textAlign: TextAlign.center, + textDirection: TextDirection.ltr); + tp.layout(); + tp.paint(canvas, size.center(offset - tp.size.center(Offset.zero))); + } + } + + Offset _getHandOffset(double percentage, double length) { + final radians = 2 * pi * percentage; + final angle = -pi / 2.0 + radians; + + return Offset(length * cos(angle), length * sin(angle)); + } + + // ref: https://www.codenameone.com/blog/codename-one-graphics-part-2-drawing-an-analog-clock.html + void _paintTickMarks(Canvas canvas, Size size, double scaleFactor) { + double r = size.shortestSide / 2; + double tick = 5 * scaleFactor, + mediumTick = 2.0 * tick, + longTick = 3.0 * tick; + double p = longTick + 4 * scaleFactor; + Paint tickPaint = Paint() + ..color = tickColor + ..strokeWidth = 2.0 * scaleFactor; + + for (int i = 1; i <= 60; i++) { + // default tick length is short + double len = tick; + if (i % 15 == 0) { + // Longest tick on quarters (every 15 ticks) + len = longTick; + } else if (i % 5 == 0) { + // Medium ticks on the '5's (every 5 ticks) + len = mediumTick; + } else { + if (ticksType != ClockTicksType.all) { + continue; + } + } + // Get the angle from 12 O'Clock to this tick (radians) + double angleFrom12 = i / 60.0 * 2.0 * pi; + + // Get the angle from 3 O'Clock to this tick + // Note: 3 O'Clock corresponds with zero angle in unit circle + // Makes it easier to do the math. + double angleFrom3 = pi / 2.0 - angleFrom12; + + canvas.drawLine( + size.center(Offset(cos(angleFrom3) * (r + len - p), + sin(angleFrom3) * (r + len - p))), + size.center( + Offset(cos(angleFrom3) * (r - p), sin(angleFrom3) * (r - p))), + tickPaint); + } + } + + void _paintClockHands(Canvas canvas, Size size, double scaleFactor) { + double r = size.shortestSide / 2; + double p = 0.0; + if (ticksType != ClockTicksType.none) p += 28.0; + if (numbersType != ClockNumbersType.none) p += 24.0; + if (numbersType == ClockNumbersType.all) p += 24.0; + double longHandLength = r - (p * scaleFactor); + double shortHandLength = r - (p + 36.0) * scaleFactor; + + Paint handPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.bevel + ..strokeWidth = strokeWidth * scaleFactor; + double seconds = dateTime.second / secondsInMinute; + double minutes = (dateTime.minute + seconds) / minutesInHour; + double hour = (dateTime.hour + minutes) / hoursInClock; + + canvas.drawLine( + size.center(_getHandOffset(hour, handPinHoleSize * scaleFactor)), + size.center(_getHandOffset(hour, shortHandLength)), + handPaint..color = hourHandColor); + + canvas.drawLine( + size.center(_getHandOffset(minutes, handPinHoleSize * scaleFactor)), + size.center(_getHandOffset(minutes, longHandLength)), + handPaint..color = minuteHandColor); + if (showSecondHand) { + canvas.drawLine( + size.center(_getHandOffset(seconds, handPinHoleSize * scaleFactor)), + size.center(_getHandOffset(seconds, longHandLength)), + handPaint..color = secondHandColor); + } + } + + void _paintDigitalClock( + Canvas canvas, Size size, double scaleFactor, bool useMilitaryTime) { + int hourInt = dateTime.hour; + String meridiem = ''; + if (!useMilitaryTime) { + if (hourInt > 12) { + hourInt = hourInt - 12; + meridiem = ' PM'; + } else { + meridiem = ' AM'; + } + } + String hour = hourInt.toString().padLeft(2, "0"); + String minute = dateTime.minute.toString().padLeft(2, "0"); + String second = dateTime.second.toString().padLeft(2, "0"); + TextSpan digitalClockSpan = TextSpan( + style: TextStyle( + color: digitalClockColor, + fontSize: 18 * scaleFactor * textScaleFactor), + text: "$hour:$minute:$second$meridiem"); + TextPainter digitalClockTP = TextPainter( + text: digitalClockSpan, + textAlign: TextAlign.center, + textDirection: TextDirection.ltr); + digitalClockTP.layout(); + digitalClockTP.paint( + canvas, + size.center( + -digitalClockTP.size.center(Offset(0.0, -size.shortestSide / 6)))); + } +} diff --git a/lib/common/widgets/clock/clock.dart b/lib/common/widgets/clock/clock.dart deleted file mode 100644 index b415ab73..00000000 --- a/lib/common/widgets/clock/clock.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:clock_app/navigation/types/alignment.dart'; -import 'package:flutter/material.dart'; -import 'package:timer_builder/timer_builder.dart'; -import 'package:timezone/timezone.dart' as timezone; - -import 'clock_display.dart'; - -class Clock extends StatelessWidget { - const Clock({ - super.key, - this.scale = 1, - this.shouldShowDate = false, - this.shouldShowSeconds = false, - this.color, - this.timezoneLocation, - this.horizontalAlignment = ElementAlignment.start, - }); - - final ElementAlignment horizontalAlignment; - final double scale; - final bool shouldShowDate; - final bool shouldShowSeconds; - final Color? color; - final timezone.Location? timezoneLocation; - - @override - Widget build(BuildContext context) { - return TimerBuilder.periodic( - const Duration(seconds: 1), - builder: (context) { - DateTime dateTime; - if (timezoneLocation != null) { - dateTime = timezone.TZDateTime.now(timezoneLocation!); - } else { - dateTime = DateTime.now(); - } - return ClockDisplay( - scale: scale, - shouldShowDate: shouldShowDate, - color: color, - shouldShowSeconds: shouldShowSeconds, - dateTime: dateTime, - horizontalAlignment: horizontalAlignment, - ); - }, - ); - } -} diff --git a/lib/common/widgets/clock/clock_display.dart b/lib/common/widgets/clock/clock_display.dart deleted file mode 100644 index 9132f4e3..00000000 --- a/lib/common/widgets/clock/clock_display.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:ffi'; - -import 'package:clock_app/clock/types/time.dart'; -import 'package:clock_app/common/utils/time_format.dart'; -import 'package:clock_app/common/widgets/clock/time_display.dart'; -import 'package:clock_app/navigation/types/alignment.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/types/setting.dart'; -import 'package:flutter/material.dart'; - -class ClockDisplay extends StatefulWidget { - const ClockDisplay({ - Key? key, - this.scale = 1, - this.color, - this.shouldShowDate = false, - this.shouldShowSeconds = false, - required this.dateTime, - this.horizontalAlignment = ElementAlignment.start, - }) : super(key: key); - - final double scale; - final bool shouldShowDate; - final Color? color; - final DateTime dateTime; - final bool shouldShowSeconds; - final ElementAlignment horizontalAlignment; - - @override - State createState() => _ClockDisplayState(); -} - -class _ClockDisplayState extends State { - // late TimeFormat timeFormat; - late Setting timeFormatSetting = appSettings - .getGroup("General") - .getGroup("Display") - .getSetting("Time Format"); - - late Setting longDateFormatSetting = appSettings - .getGroup("General") - .getGroup("Display") - .getSetting("Long Date Format"); - - TimeFormat getTimeFormat() { - TimeFormat timeFormat = timeFormatSetting.value; - if (timeFormat == TimeFormat.device) { - if (MediaQuery.of(context).alwaysUse24HourFormat) { - timeFormat = TimeFormat.h24; - } else { - timeFormat = TimeFormat.h12; - } - } - return timeFormat; - } - - void update(dynamic value) { - setState(() {}); - } - - @override - void initState() { - super.initState(); - timeFormatSetting.addListener(update); - longDateFormatSetting.addListener(update); - } - - @override - void dispose() { - timeFormatSetting.removeListener(update); - longDateFormatSetting.removeListener(update); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - TimeFormat timeFormat = getTimeFormat(); - - return Column( - crossAxisAlignment: - CrossAxisAlignment.values[widget.horizontalAlignment.index], - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.values[widget.horizontalAlignment.index], - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - TimeDisplay( - format: getTimeFormatString(context, timeFormat, - showMeridiem: false), - fontSize: 72 * widget.scale, - height: widget.shouldShowDate ? 0.75 : null, - color: widget.color, - dateTime: widget.dateTime, - ), - SizedBox(width: 4 * widget.scale), - Column( - verticalDirection: VerticalDirection.up, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.shouldShowSeconds) - TimeDisplay( - format: 'ss', - fontSize: 36 * widget.scale, - height: 1, - color: widget.color, - dateTime: widget.dateTime, - ), - Row( - children: timeFormat == TimeFormat.h12 - ? [ - TimeDisplay( - format: 'a', - fontSize: (widget.shouldShowSeconds ? 24 : 32) * - widget.scale, - height: 1, - color: widget.color, - dateTime: widget.dateTime, - ), - if (widget.shouldShowSeconds) - SizedBox(width: 16 * widget.scale), - ] - : [ - if (widget.shouldShowSeconds) - SizedBox(width: 56 * widget.scale), - ], - ), - ], - ), - ]), - if (widget.shouldShowDate) SizedBox(height: 4 * widget.scale), - if (widget.shouldShowDate) - TimeDisplay( - format: longDateFormatSetting.value, - fontSize: 16 * widget.scale, - height: 1, - dateTime: widget.dateTime, - color: widget.color ?? - Theme.of(context).colorScheme.onBackground.withOpacity(0.8), - ), - ], - ); - } -} diff --git a/lib/common/widgets/clock/digital_clock.dart b/lib/common/widgets/clock/digital_clock.dart new file mode 100644 index 00000000..b10ab563 --- /dev/null +++ b/lib/common/widgets/clock/digital_clock.dart @@ -0,0 +1,50 @@ +import 'package:clock_app/common/widgets/clock/digital_clock_display.dart'; +import 'package:clock_app/navigation/types/alignment.dart'; +import 'package:flutter/material.dart'; +import 'package:timer_builder/timer_builder.dart'; +import 'package:timezone/timezone.dart' as timezone; + +enum ClockType { + digital, + analog, +} + +class DigitalClock extends StatelessWidget { + const DigitalClock({ + super.key, + this.scale = 1, + this.shouldShowDate = false, + this.shouldShowSeconds = false, + this.color, + this.timezoneLocation, + this.horizontalAlignment = ElementAlignment.start, + }); + + final ElementAlignment horizontalAlignment; + final double scale; + final bool shouldShowDate; + final bool shouldShowSeconds; + final Color? color; + final timezone.Location? timezoneLocation; + + @override + Widget build(BuildContext context) { + return TimerBuilder.periodic(const Duration(seconds: 1), + builder: (context) { + DateTime dateTime; + if (timezoneLocation != null) { + dateTime = timezone.TZDateTime.now(timezoneLocation!); + } else { + dateTime = DateTime.now(); + } + return DigitalClockDisplay( + scale: scale, + shouldShowDate: shouldShowDate, + color: color, + shouldShowSeconds: shouldShowSeconds, + dateTime: dateTime, + horizontalAlignment: horizontalAlignment, + ); + }); + } +} diff --git a/lib/common/widgets/clock/digital_clock_display.dart b/lib/common/widgets/clock/digital_clock_display.dart new file mode 100644 index 00000000..e968ff30 --- /dev/null +++ b/lib/common/widgets/clock/digital_clock_display.dart @@ -0,0 +1,145 @@ +import 'package:clock_app/clock/types/time.dart'; +import 'package:clock_app/common/utils/time_format.dart'; +import 'package:clock_app/common/widgets/clock/time_display.dart'; +import 'package:clock_app/navigation/types/alignment.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting.dart'; +import 'package:flutter/material.dart'; + +class DigitalClockDisplay extends StatefulWidget { + const DigitalClockDisplay({ + super.key, + this.scale = 1, + this.color, + this.shouldShowTime = true, + this.shouldShowDate = false, + this.shouldShowSeconds = false, + required this.dateTime, + this.horizontalAlignment = ElementAlignment.start, + }); + + final bool shouldShowTime; + final double scale; + final bool shouldShowDate; + final Color? color; + final DateTime dateTime; + final bool shouldShowSeconds; + final ElementAlignment horizontalAlignment; + + @override + State createState() => _DigitalClockDisplayState(); +} + +class _DigitalClockDisplayState extends State { + late Setting timeFormatSetting = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Time Format"); + + late Setting longDateFormatSetting = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Long Date Format"); + + TimeFormat getTimeFormat() { + TimeFormat timeFormat = timeFormatSetting.value; + if (timeFormat == TimeFormat.device) { + if (MediaQuery.of(context).alwaysUse24HourFormat) { + timeFormat = TimeFormat.h24; + } else { + timeFormat = TimeFormat.h12; + } + } + return timeFormat; + } + + void update(dynamic value) { + setState(() {}); + } + + @override + void initState() { + super.initState(); + timeFormatSetting.addListener(update); + longDateFormatSetting.addListener(update); + } + + @override + void dispose() { + timeFormatSetting.removeListener(update); + longDateFormatSetting.removeListener(update); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + TimeFormat timeFormat = getTimeFormat(); + + return Column( + crossAxisAlignment: + CrossAxisAlignment.values[widget.horizontalAlignment.index], + children: [ + if (widget.shouldShowTime) + Row( + mainAxisAlignment: + MainAxisAlignment.values[widget.horizontalAlignment.index], + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + TimeDisplay( + format: getTimeFormatString(context, timeFormat, + showMeridiem: false), + fontSize: 72 * widget.scale, + height: widget.shouldShowDate ? 0.75 : null, + color: widget.color, + dateTime: widget.dateTime, + ), + SizedBox(width: 4 * widget.scale), + Column( + verticalDirection: VerticalDirection.up, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.shouldShowSeconds) + TimeDisplay( + format: 'ss', + fontSize: 36 * widget.scale, + height: 1, + color: widget.color, + dateTime: widget.dateTime, + ), + Row( + children: timeFormat == TimeFormat.h12 + ? [ + TimeDisplay( + format: 'a', + fontSize: (widget.shouldShowSeconds ? 24 : 32) * + widget.scale, + height: 1, + color: widget.color, + dateTime: widget.dateTime, + ), + if (widget.shouldShowSeconds) + SizedBox(width: 16 * widget.scale), + ] + : [ + if (widget.shouldShowSeconds) + SizedBox(width: 56 * widget.scale), + ], + ), + ], + ), + ]), + if (widget.shouldShowDate) SizedBox(height: 4 * widget.scale), + if (widget.shouldShowDate) + TimeDisplay( + format: longDateFormatSetting.value, + fontSize: 16 * widget.scale, + height: 1, + dateTime: widget.dateTime, + color: widget.color ?? + Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + ), + ], + ); + } +} diff --git a/lib/common/widgets/fab.dart b/lib/common/widgets/fab.dart index a34b8fe5..f8a96317 100644 --- a/lib/common/widgets/fab.dart +++ b/lib/common/widgets/fab.dart @@ -2,6 +2,7 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/theme/types/theme_extension.dart'; import 'package:flutter/material.dart'; enum FabPosition { bottomLeft, bottomRight } @@ -30,7 +31,6 @@ class FAB extends StatefulWidget { class _FABState extends State { late Setting _leftHandedMode; - late Setting _useMaterialStyle; void update(value) { setState(() {}); @@ -42,27 +42,31 @@ class _FABState extends State { _leftHandedMode = appSettings.getGroup("Accessibility").getSetting("Left Handed Mode"); - _useMaterialStyle = appSettings.getGroup("Appearance").getGroup("Style").getSetting("Use Material Style"); _leftHandedMode.addListener(update); - _useMaterialStyle.addListener(update); } @override void dispose() { _leftHandedMode.removeListener(update); - _useMaterialStyle.removeListener(update); super.dispose(); } @override Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + ThemeSettingExtension themeSettings = + theme.extension()!; + final position = _leftHandedMode.value ? widget.position == FabPosition.bottomRight ? FabPosition.bottomLeft : FabPosition.bottomRight : widget.position; -double bottomPadding = _useMaterialStyle.value ? widget.bottomPadding + 20 : widget.bottomPadding; + double bottomPadding = themeSettings.useMaterialStyle + ? widget.bottomPadding + 20 + : widget.bottomPadding; return Positioned( bottom: bottomPadding, @@ -74,13 +78,13 @@ double bottomPadding = _useMaterialStyle.value ? widget.bottomPadding + 20 : wid : null, child: CardContainer( elevationMultiplier: 2, - color: Theme.of(context).colorScheme.primary, + color: colorScheme.primary, onTap: widget.onPressed, child: Padding( padding: const EdgeInsets.all(16.0), child: Icon( widget.icon, - color: Theme.of(context).colorScheme.onPrimary, + color: colorScheme.onPrimary, size: 24 * widget.size, ), ), diff --git a/lib/common/widgets/fields/date_picker_bottom_sheet.dart b/lib/common/widgets/fields/date_picker_bottom_sheet.dart index e1a094c2..3e67a5b9 100644 --- a/lib/common/widgets/fields/date_picker_bottom_sheet.dart +++ b/lib/common/widgets/fields/date_picker_bottom_sheet.dart @@ -29,11 +29,10 @@ class _DatePickerBottomSheetState extends State { DateTime? _rangeEndDate; DateTime _focusedDate = DateTime.now(); late Weekday firstWeekday = appSettings - .getGroup("General") - .getGroup("Display") - .getSetting("First Day of Week") - .value; - + .getGroup("General") + .getGroup("Display") + .getSetting("First Day of Week") + .value; bool get _isSaveEnabled => widget.rangeOnly ? _selectedDates.length == 2 : _selectedDates.isNotEmpty; @@ -46,8 +45,13 @@ class _DatePickerBottomSheetState extends State { ? DateTime.now() : widget.initialDates.first; if (widget.rangeOnly) { - _rangeStartDate = widget.initialDates.first; - _rangeEndDate = widget.initialDates.last; + if (widget.initialDates.isEmpty) { + _rangeStartDate = DateTime.now(); + _rangeEndDate = DateTime.now().add(const Duration(days: 2)); + } else { + _rangeStartDate = widget.initialDates.first; + _rangeEndDate = widget.initialDates.last; + } } } @@ -199,7 +203,8 @@ class _DatePickerBottomSheetState extends State { availableCalendarFormats: const { CalendarFormat.month: 'Month', }, - startingDayOfWeek: StartingDayOfWeek.values[firstWeekday.id - 1], + startingDayOfWeek: + StartingDayOfWeek.values[firstWeekday.id - 1], rowHeight: 48, headerStyle: HeaderStyle( // headerMargin: EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/common/widgets/fields/date_picker_field.dart b/lib/common/widgets/fields/date_picker_field.dart index 1d8a7681..04d89d94 100644 --- a/lib/common/widgets/fields/date_picker_field.dart +++ b/lib/common/widgets/fields/date_picker_field.dart @@ -7,13 +7,13 @@ import 'package:intl/intl.dart'; class DatePickerField extends StatefulWidget { const DatePickerField({ - Key? key, + super.key, required this.title, this.description, required this.onChanged, required this.value, this.rangeOnly = false, - }) : super(key: key); + }); final List value; final String title; diff --git a/lib/common/widgets/fields/select_field/select_bottom_sheet.dart b/lib/common/widgets/fields/select_field/select_bottom_sheet.dart index a19cdf19..c916c4e6 100644 --- a/lib/common/widgets/fields/select_field/select_bottom_sheet.dart +++ b/lib/common/widgets/fields/select_field/select_bottom_sheet.dart @@ -155,7 +155,7 @@ class SelectBottomSheet extends StatelessWidget { // // ], // ), - const SizedBox(height: 12.0), + // const SizedBox(height: 12.0), Flexible( child: _getOptionCard(), ), diff --git a/lib/common/widgets/fields/slider_field.dart b/lib/common/widgets/fields/slider_field.dart index ba242740..636d7170 100644 --- a/lib/common/widgets/fields/slider_field.dart +++ b/lib/common/widgets/fields/slider_field.dart @@ -11,9 +11,11 @@ class SliderField extends StatefulWidget { required this.max, required this.title, this.unit = '', - this.snapLength}); + this.snapLength, + this.description = ''}); final String title; + final String description; final double value; final double min; final double max; @@ -89,6 +91,10 @@ class _SliderFieldState extends State { widget.title, style: textTheme.headlineMedium, ), + if (widget.description.isNotEmpty) ...[ + const SizedBox(height: 2), + Text(widget.description, style: textTheme.bodyMedium) + ], const SizedBox(height: 8.0), Row( children: [ @@ -97,7 +103,7 @@ class _SliderFieldState extends State { // height: textSize.height, // width: 50, child: Row( - // crossAxisAlignment: CrossAxisAlignment.end, + // crossAxisAlignment: CrossAxisAlignment.end, children: [ IntrinsicWidth( child: TextField( diff --git a/lib/common/widgets/fields/switch_field.dart b/lib/common/widgets/fields/switch_field.dart index f3d76b2c..89729337 100644 --- a/lib/common/widgets/fields/switch_field.dart +++ b/lib/common/widgets/fields/switch_field.dart @@ -2,13 +2,14 @@ import 'package:flutter/material.dart'; class SwitchField extends StatefulWidget { const SwitchField( - {Key? key, + {super.key, required this.value, required this.onChanged, - required this.name}) - : super(key: key); + required this.name, + this.description = ""}); final String name; + final String description; final bool value; final void Function(bool value)? onChanged; @@ -19,6 +20,9 @@ class SwitchField extends StatefulWidget { class _SwitchFieldState extends State { @override Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + return Material( color: Colors.transparent, child: InkWell( @@ -29,9 +33,21 @@ class _SwitchFieldState extends State { children: [ Expanded( flex: 100, - child: Text( - widget.name, - style: Theme.of(context).textTheme.headlineMedium, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: textTheme.headlineMedium, + ), + if (widget.description.isNotEmpty) ...[ + const SizedBox(height: 4), + Text(widget.description, style: textTheme.bodyMedium) + ], + ], + ), ), ), const Spacer(), diff --git a/lib/common/widgets/file_item_card.dart b/lib/common/widgets/file_item_card.dart index 8a18027d..4d02146e 100644 --- a/lib/common/widgets/file_item_card.dart +++ b/lib/common/widgets/file_item_card.dart @@ -65,7 +65,7 @@ class _FileItemCardState extends State { child: Row( children: [ Icon(getFileItemIcon(widget.fileItem, isPlaying), color: colorScheme.primary), - const SizedBox(width: 4), + const SizedBox(width: 12), Expanded( flex: 999, child: Padding( diff --git a/lib/common/widgets/list/action_bottom_sheet.dart b/lib/common/widgets/list/action_bottom_sheet.dart index a905cfc7..3d37c191 100644 --- a/lib/common/widgets/list/action_bottom_sheet.dart +++ b/lib/common/widgets/list/action_bottom_sheet.dart @@ -92,8 +92,7 @@ class ActionBottomSheet extends StatelessWidget { children: [ Text( actions[index].name, - style: Theme.of(context) - .textTheme + style: textTheme .headlineMedium ?.copyWith( color: actions[index].color ?? colorScheme.onSurface), diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart new file mode 100644 index 00000000..9c4c1ac1 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart @@ -0,0 +1,207 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// A Flutter AnimatedGridView that animates insertion and removal of the item. +class AnimatedGridView extends StatelessWidget { + /// The current list of items that this[MotionGridViewBuilder] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + /// Controls the layout of tiles in a grid. + /// Given the current constraints on the grid, + /// a SliverGridDelegate computes the layout for the tiles in the grid. + /// The tiles can be placed arbitrarily, + /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children. + final SliverGridDelegate sliverGridDelegate; + + ///List of [AnimationEffect] used for the appearing animation when item is added in the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect] used for the disappearing animation when item is removed from list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedGridView( + {Key? key, + required this.items, + required this.itemBuilder, + required this.sliverGridDelegate, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.shrinkWrap = false, + this.isSameItem}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl.grid( + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + insertDuration: insertDuration, + removeDuration: removeDuration, + enterTransition: enterTransition, + exitTransition: exitTransition, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + isSameItem: isSameItem), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart new file mode 100644 index 00000000..d304220e --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart @@ -0,0 +1,200 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// A Flutter AnimatedGridView that animates insertion and removal of the item. +class AnimatedListView extends StatelessWidget { + /// The current list of items that this[MotionListViewBuilder] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The `context` argument is the build context where the widget will be + /// created, the `index` is the index of the item to be built, and the + /// `animation` is an [Animation] that should be used to animate an entry + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The `context` argument is the build context where the widget will be + /// created, the `index` is the index of the item to be built, and the + /// `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedListView({ + Key? key, + required this.items, + required this.itemBuilder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.scrollDirection = Axis.vertical, + this.padding, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.shrinkWrap = false, + this.isSameItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl( + items: items, + itemBuilder: itemBuilder, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart new file mode 100644 index 00000000..98ebc732 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart @@ -0,0 +1,257 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; + +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +///A GridView that enables users to interactively reorder items through dragging, with animated insertion and removal of items. +/// +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// The [onReorder] parameter is required and will be called when a child +/// widget is dragged to a new position. +/// +/// +/// All list items must have a key. +/// +/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator] +/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is +/// created with the original list item as its child. +class AnimatedReorderableGridView extends StatelessWidget { + /// The current list of items that this[AnimatedReorderableGridView] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + /// Controls the layout of tiles in a grid. + /// Given the current constraints on the grid, + /// a SliverGridDelegate computes the layout for the tiles in the grid. + /// The tiles can be placed arbitrarily, + /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children. + final SliverGridDelegate sliverGridDelegate; + + ///List of [AnimationEffect](s) used for the appearing animation when item is added in the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when item is removed from list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// A callback used by [ReorderableList] to report that a list item has moved + /// to a new position in the list. + /// + /// Implementations should remove the corresponding list item at [oldIndex] + /// and reinsert it at [newIndex]. + final ReorderCallback onReorder; + + /// A callback that is called when an item drag has started. + /// + /// The index parameter of the callback is the index of the selected item. + final void Function(int)? onReorderStart; + + /// A callback that is called when the dragged item is dropped. + /// + /// The index parameter of the callback is the index where the item is + /// dropped. Unlike [onReorder], this is called even when the list item is + /// dropped in the same location. + final void Function(int)? onReorderEnd; + + /// {@template flutter.widgets.reorderable_list.proxyDecorator} + /// A callback that allows the app to add an animated decoration around + /// an item when it is being dragged. + /// {@endtemplate} + final ReorderItemProxyDecorator? proxyDecorator; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the items can be dragged by long pressing on them. + final bool longPressDraggable; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedReorderableGridView( + {Key? key, + required this.items, + required this.itemBuilder, + required this.sliverGridDelegate, + required this.onReorder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.onReorderStart, + this.onReorderEnd, + this.proxyDecorator, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.longPressDraggable = true, + this.shrinkWrap = false, + this.insertItemBuilder, + this.removeItemBuilder, + this.isSameItem}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl.grid( + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + insertDuration: insertDuration, + removeDuration: removeDuration, + enterTransition: enterTransition, + exitTransition: exitTransition, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + longPressDraggable: longPressDraggable, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart new file mode 100644 index 00000000..483b5c82 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart @@ -0,0 +1,273 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +///A [ListView] that enables users to interactively reorder items through dragging, with animated insertion and removal of items. +/// +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// The [onReorder] parameter is required and will be called when a child +/// widget is dragged to a new position. +/// +/// By default, on [TargetPlatformVariant.desktop] platforms each item will +/// have a drag handle added on top of it that will allow the user to grab it +/// to move the item. On [TargetPlatformVariant.mobile], no drag handle will be +/// added, but when the user long presses anywhere on the item it will start +/// moving the item.Displaying drag handles can be controlled with [AnimatedReorderableListView.buildDefaultDragHandles]. +/// +/// All list items must have a key. +/// +/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator] +/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is +/// created with the original list item as its child. + +class AnimatedReorderableListView extends StatelessWidget { + /// The current list of items that this[AnimatedReorderableListView] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// A callback used by [ReorderableList] to report that a list item has moved + /// to a new position in the list. + /// + /// Implementations should remove the corresponding list item at [oldIndex] + /// and reinsert it at [newIndex]. + final ReorderCallback onReorder; + + /// A callback that is called when an item drag has started. + /// + /// The index parameter of the callback is the index of the selected item. + final void Function(int)? onReorderStart; + + /// A callback that is called when the dragged item is dropped. + /// + /// The index parameter of the callback is the index where the item is + /// dropped. Unlike [onReorder], this is called even when the list item is + /// dropped in the same location. + final void Function(int)? onReorderEnd; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.proxyDecorator} + /// A callback that allows the app to add an animated decoration around + /// an item when it is being dragged. + /// {@endtemplate} + final ReorderItemProxyDecorator? proxyDecorator; + + /// If true, on desktop platforms, a drag handle is stacked over the center of each item's trailing edge; + /// on mobile platforms, a long press anywhere on the item starts a drag. + /// + /// The default desktop drag handle is just an [Icons.drag_handle] wrapped by [ReorderableDragStartListener]. + /// On mobile platforms, the entire item is wrapped with a [ReorderableDragStartListener]. + /// + /// To change the appearance or the layout of the drag handles, make this parameter false + /// and wrap each list item, or a widget within each list item, with [ReorderableDragStartListener]or + /// a subclass of [ReorderableDragStartListener]. + /// + /// To get the idea [Flutter Example](https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html) + + final bool buildDefaultDragHandles; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the items can be dragged by long pressing on them. + final bool longPressDraggable; + + final bool useDefaultDragListeners; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedReorderableListView({ + Key? key, + required this.items, + required this.itemBuilder, + required this.onReorder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.onReorderStart, + this.onReorderEnd, + this.proxyDecorator, + this.scrollDirection = Axis.vertical, + this.padding, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.buildDefaultDragHandles = true, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.longPressDraggable = true, + this.useDefaultDragListeners = true, + this.shrinkWrap = false, + this.isSameItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl( + items: items, + itemBuilder: itemBuilder, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + buildDefaultDragHandles: buildDefaultDragHandles, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart new file mode 100644 index 00000000..5c50b12f --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart @@ -0,0 +1,14 @@ +export 'fade_in.dart'; +export 'flipin_x.dart'; +export 'flipin_y.dart'; +export 'landing.dart'; +export 'scale_in.dart'; +export 'scale_in_bottom.dart'; +export 'scale_in_left.dart'; +export 'scale_in_right.dart'; +export 'scale_in_top.dart'; +export 'slide_in_down.dart'; +export 'slide_in_left.dart'; +export 'slide_in_right.dart'; +export 'slide_in_up.dart'; +export 'size_animation.dart'; diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart new file mode 100644 index 00000000..d295db44 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FadeIn extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + FadeIn({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation opacity = buildAnimation( + entry, + begin: begin ?? beginValue, + end: end ?? endValue, + totalDuration) + .animate(animation); + return FadeTransition(opacity: opacity, child: child); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart new file mode 100644 index 00000000..51361c08 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart @@ -0,0 +1,31 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FlipInX extends AnimationEffect { + static const double beginValue = pi / 2; + static const double endValue = 0.0; + final double? begin; + final double? end; + + FlipInX({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation rotation = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Transform( + transform: Matrix4.rotationX(rotation.value), + alignment: Alignment.center, + child: child, + ); + }, + child: child); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart new file mode 100644 index 00000000..42844956 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart @@ -0,0 +1,32 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FlipInY extends AnimationEffect { + static const double beginValue = pi / 2; + static const double endValue = 0.0; + final double? begin; + final double? end; + + FlipInY({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation rotation = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return AnimatedBuilder( + animation: rotation, + builder: (BuildContext context, Widget? child) { + return Transform( + transform: Matrix4.rotationY(rotation.value), + alignment: Alignment.center, + child: child, + ); + }, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart new file mode 100644 index 00000000..d2f2c777 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart @@ -0,0 +1,26 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class Landing extends AnimationEffect { + static const double beginValue = 1.5; + static const double endValue = 1.0; + final double? begin; + final double? end; + + Landing({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: scale, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart new file mode 100644 index 00000000..0e177938 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart @@ -0,0 +1,75 @@ +import 'package:flutter/cupertino.dart'; + +abstract class AnimationEffect { + /// The delay for this specific [AnimationEffect]. + final Duration? delay; + + /// The duration for the specific [AnimationEffect]. + final Duration? duration; + + /// The curve for the specific [AnimationEffect]. + final Curve? curve; + + AnimationEffect({ + this.delay = Duration.zero, + this.duration = const Duration(milliseconds: 300), + this.curve, + }); + + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + return child; + } + + Animatable buildAnimation(EffectEntry entry, Duration totalDuration, + {required T begin, required T end}) { + return Tween(begin: begin, end: end) + .chain(entry.buildAnimation(totalDuration: totalDuration)); + } +} + +@immutable +class EffectEntry { + const EffectEntry({ + required this.animationEffect, + required this.delay, + required this.duration, + required this.curve, + }); + + /// The delay for this entry. + final Duration delay; + + /// The duration for this entry. + final Duration duration; + + /// The curve used by this entry. + final Curve curve; + + /// The effect associated with this entry. + final AnimationEffect animationEffect; + + /// The begin time for this entry. + Duration get begin => delay; + + /// The end time for this entry. + Duration get end => begin + duration; + + /// Builds a sub-animation based on the properties of this entry. + CurveTween buildAnimation({ + required Duration totalDuration, + Curve? curve, + }) { + int beginT = begin.inMicroseconds, endT = end.inMicroseconds; + return CurveTween( + curve: Interval(beginT / totalDuration.inMicroseconds, + endT / totalDuration.inMicroseconds, + curve: curve ?? this.curve), + ); + } + + @override + String toString() { + return "delay: $delay, Duration: $duration, curve: $curve, begin: $begin, end: $end, Effect: $animationEffect"; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart new file mode 100644 index 00000000..339f93cb --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart @@ -0,0 +1,16 @@ +enum AnimationType { + fadeIn, + flipInY, + flipInX, + landing, + size, + scaleIn, + scaleInTop, + scaleInBottom, + scaleInLeft, + scaleInRight, + slideInLeft, + slideInRight, + slideInDown, + slideInUp, +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart new file mode 100644 index 00000000..7a158ac2 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleIn extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleIn({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart new file mode 100644 index 00000000..844a2bcf --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInBottom extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInBottom( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.bottomCenter, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart new file mode 100644 index 00000000..ed8c35d1 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart @@ -0,0 +1,24 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInLeft extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInLeft({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.centerLeft, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart new file mode 100644 index 00000000..ea322822 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInRight extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInRight( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.centerRight, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart new file mode 100644 index 00000000..77588d9a --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + + +class ScaleInTop extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInTop({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.topCenter, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart new file mode 100644 index 00000000..62a3df0e --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart @@ -0,0 +1,37 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SizeAnimation extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + static const double alignmentValue = 0.0; + final double? begin; + final double? end; + final Axis? axis; + final double? axisAlignment; + + SizeAnimation( + {super.delay, + super.duration, + super.curve, + this.begin, + this.end, + this.axis, + this.axisAlignment}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation sizeFactor = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return Align( + child: SizeTransition( + sizeFactor: sizeFactor, + axis: axis ?? Axis.horizontal, + axisAlignment: axisAlignment ?? alignmentValue, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart new file mode 100644 index 00000000..3ebe8636 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart @@ -0,0 +1,26 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInDown extends AnimationEffect { + static const Offset beginValue = Offset(0, 1); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInDown({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition( + position: position, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart new file mode 100644 index 00000000..7f21192a --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart @@ -0,0 +1,22 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInLeft extends AnimationEffect { + static const Offset beginValue = Offset(-1, 0); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInLeft({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart new file mode 100644 index 00000000..a6af2f25 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInRight extends AnimationEffect { + static const Offset beginValue = Offset(1, 0); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInRight( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart new file mode 100644 index 00000000..cafb2d93 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart @@ -0,0 +1,22 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInUp extends AnimationEffect { + static const Offset beginValue = Offset(0, -1); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInUp({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart new file mode 100644 index 00000000..8a6e0fb5 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart @@ -0,0 +1,905 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import '../component/drag_listener.dart'; +import '../model/motion_data.dart'; +import 'motion_list_base.dart'; + +part '../component/drag_item.dart'; + +part '../component/motion_animated_content.dart'; + +typedef CustomAnimatedWidgetBuilder = Widget Function( + BuildContext context, Widget child, Animation animation); + +class MotionBuilder extends StatefulWidget { + final CustomAnimatedWidgetBuilder insertAnimationBuilder; + final CustomAnimatedWidgetBuilder removeAnimationBuilder; + final ReorderCallback? onReorder; + final void Function(int index)? onReorderStart; + final void Function(int index)? onReorderEnd; + + final ReorderItemProxyDecorator? proxyDecorator; + final ItemBuilder itemBuilder; + final int initialCount; + final Axis scrollDirection; + final SliverGridDelegate? delegateBuilder; + final bool buildDefaultDragHandles; + final bool longPressDraggable; + final bool useDefaultDragListeners; + + const MotionBuilder( + {Key? key, + required this.itemBuilder, + required this.insertAnimationBuilder, + required this.removeAnimationBuilder, + this.onReorder, + this.onReorderEnd, + this.onReorderStart, + this.proxyDecorator, + this.initialCount = 0, + this.delegateBuilder, + this.scrollDirection = Axis.vertical, + required this.buildDefaultDragHandles, + required this.useDefaultDragListeners, + this.longPressDraggable = false}) + : assert(initialCount >= 0), + super(key: key); + + @override + State createState() => MotionBuilderState(); + + static MotionBuilderState of(BuildContext context) { + final MotionBuilderState? result = + context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError( + 'MotionBuilderState.of() called with a context that does not contain a MotionBuilderState.\n' + 'No MotionBuilderState ancestor could be found starting from the ' + 'context that was passed to MotionBuilderState.of(). This can ' + 'happen when the context provided is from the same StatefulWidget that ' + 'built the AnimatedList.' + 'The context used was:\n' + ' $context', + ); + } + return true; + }()); + return result!; + } + + static MotionBuilderState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } +} + +class MotionBuilderState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; + final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; + int _itemsCount = 0; + + Map childrenMap = {}; + final Map _items = + {}; + + OverlayEntry? _overlayEntry; + int? _dragIndex; + _DragInfo? _dragInfo; + int? _insertIndex; + Offset? _finalDropPosition; + MultiDragGestureRecognizer? _recognizer; + int? _recognizerPointer; + EdgeDraggingAutoScroller? _autoScroller; + late ScrollableState _scrollable; + + bool autoScrolling = false; + + Axis get scrollDirection => axisDirectionToAxis(_scrollable.axisDirection); + + bool get _reverse => + _scrollable.axisDirection == AxisDirection.up || + _scrollable.axisDirection == AxisDirection.left; + + bool get isGrid => widget.delegateBuilder != null; + + @override + bool get wantKeepAlive => false; + + @override + void initState() { + _itemsCount = widget.initialCount; + for (int i = 0; i < widget.initialCount; i++) { + childrenMap[i] = MotionData(); + } + + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scrollable = Scrollable.of(context); + } + + @override + void didUpdateWidget(covariant MotionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialCount != oldWidget.initialCount) { + cancelReorder(); + } + } + + void startItemDragReorder( + {required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer}) { + assert(0 <= index && index < _itemsCount); + setState(() { + if (_dragInfo != null) { + cancelReorder(); + } else if (_recognizer != null && _recognizerPointer != event.pointer) { + _recognizer!.dispose(); + _recognizer = null; + _recognizerPointer = null; + } + if (_items.containsKey(index)) { + _dragIndex = index; + _recognizer = recognizer + ..onStart = _dragStart + ..addPointer(event); + _recognizerPointer = event.pointer; + } else { + throw Exception("Attempting ro start drag on a non-visible item"); + } + }); + } + + Drag? _dragStart(Offset position) { + assert(_dragInfo == null); + final MotionAnimatedContentState item = _items[_dragIndex]!; + item.dragging = true; + widget.onReorderStart?.call(_dragIndex!); + item.rebuild(); + _insertIndex = item.index; + _dragInfo = _DragInfo( + item: item, + initialPosition: position, + scrollDirection: scrollDirection, + gridView: isGrid, + onUpdate: _dragUpdate, + onCancel: _dragCancel, + onEnd: _dragEnd, + onDragCompleted: _dropCompleted, + proxyDecorator: widget.proxyDecorator, + tickerProvider: this); + + _dragInfo!.startDrag(); + item.dragSize = _dragInfo!.itemSize; + + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + assert(_overlayEntry == null); + _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); + overlay.insert(_overlayEntry!); + + for (final MotionAnimatedContentState childItem in _items.values) { + if (childItem == item || !childItem.mounted) { + continue; + } + item.updateForGap(false); + } + return _dragInfo; + } + + void _dragUpdate(_DragInfo item, Offset position, Offset delta) { + setState(() { + _overlayEntry?.markNeedsBuild(); + _dragUpdateItems(); + _autoScrollIfNecessary(); + }); + } + + void _dragCancel(_DragInfo item) { + setState(() { + _dragReset(); + }); + } + + Future _autoScrollIfNecessary() async { + if (autoScrolling || _dragInfo == null || _dragInfo!.scrollable == null) { + return; + } + + final position = _dragInfo!.scrollable!.position; + double? newOffset; + + const duration = Duration(milliseconds: 14); + const step = 1.0; + const overDragMax = 20.0; + const overDragCoef = 10; + + final isVertical = widget.scrollDirection == Axis.vertical; + + /// get the scroll window position on the screen + final scrollRenderBox = + _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox; + final Offset scrollPosition = scrollRenderBox.localToGlobal(Offset.zero); + + /// calculate the start and end position for the scroll window + double scrollWindowStart = + isVertical ? scrollPosition.dy : scrollPosition.dx; + double scrollWindowEnd = scrollWindowStart + + (isVertical ? scrollRenderBox.size.height : scrollRenderBox.size.width); + + /// get the proxy (dragged) object's position on the screen + final proxyObjectPosition = _dragInfo!.dragPosition - _dragInfo!.dragOffset; + + /// calculate the start and end position for the proxy object + double proxyObjectStart = + isVertical ? proxyObjectPosition.dy : proxyObjectPosition.dx; + double proxyObjectEnd = proxyObjectStart + + (isVertical ? _dragInfo!.itemSize.height : _dragInfo!.itemSize.width); + + if (!_reverse) { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels > position.minScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels < position.maxScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + } else { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels < position.maxScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels > position.minScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + } + + if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) { + autoScrolling = true; + await position.animateTo( + newOffset, + duration: duration, + curve: Curves.linear, + ); + autoScrolling = false; + if (_dragInfo != null) { + _dragUpdateItems(); + _autoScrollIfNecessary(); + } + } + } + + void _dragEnd(_DragInfo item) { + widget.onReorderEnd?.call(_insertIndex!); + setState(() => _finalDropPosition = _itemOffsetAt(_insertIndex!)); + } + + void _dropCompleted() { + final int fromIndex = _dragIndex!; + final int toIndex = _insertIndex!; + if (fromIndex != toIndex) { + widget.onReorder?.call(fromIndex, toIndex); + } + setState(() { + _dragReset(); + }); + } + + void cancelReorder() { + setState(() { + _dragReset(); + }); + } + + void _dragReset() { + if (_dragInfo != null) { + if (_dragIndex != null && _items.containsKey(_dragIndex)) { + final MotionAnimatedContentState dragItem = _items[_dragIndex]!; + dragItem.dragging = false; + dragItem.dragSize = Size.zero; + dragItem.rebuild(); + _dragIndex = null; + } + _dragInfo?.dispose(); + _dragInfo = null; + _autoScroller?.stopAutoScroll(); + _resetItemGap(); + _recognizer?.dispose(); + _recognizer = null; + _overlayEntry?.remove(); + _overlayEntry?.dispose(); + _overlayEntry = null; + _finalDropPosition = null; + } + } + + void _resetItemGap() { + for (final MotionAnimatedContentState item in _items.values) { + item.resetGap(); + } + } + + void _dragUpdateItems() { + assert(_dragInfo != null); + + int newIndex = _insertIndex!; + + final dragCenter = _dragInfo!.itemSize + .center(_dragInfo!.dragPosition - _dragInfo!.dragOffset); + + for (final MotionAnimatedContentState item in _items.values) { + if (!item.mounted) continue; + final Rect geometry = item.targetGeometryNonOffset(); + + if (geometry.contains(dragCenter)) { + newIndex = item.index; + break; + } + } + + if (newIndex == _insertIndex) return; + _insertIndex = newIndex; + + for (final MotionAnimatedContentState item in _items.values) { + if (item.index == _dragIndex) continue; + item.updateForGap(true); + } + } + + Offset calculateNextDragOffset(int index) { + int minPos = min(_dragIndex!, _insertIndex!); + int maxPos = max(_dragIndex!, _insertIndex!); + if (index < minPos || index > maxPos) return Offset.zero; + + final int direction = _insertIndex! > _dragIndex! ? -1 : 1; + if (isGrid) { + return _itemOffsetAt(index + direction) - _itemOffsetAt(index); + } else { + final Offset offset = + _extentOffset(_dragInfo!.itemExtent, scrollDirection); + return _insertIndex! > _dragIndex! ? -offset : offset; + } + } + + void registerItem(MotionAnimatedContentState item) { + _items[item.index] = item; + if (item.index == _dragInfo?.index) { + item.dragging = true; + item.dragSize = _dragInfo!.itemSize; + item.rebuild(); + } + } + + void unregisterItem(int index, MotionAnimatedContentState item) { + final MotionAnimatedContentState? currentItem = _items[index]; + if (currentItem == item) { + _items.remove(index); + } + } + + @override + void dispose() { + for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) { + item.controller?.dispose(); + } + _dragReset(); + super.dispose(); + } + + _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { + final int i = binarySearch(items, _ActiveItem.index(itemIndex)); + return i == -1 ? null : items.removeAt(i); + } + + _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) { + final int i = binarySearch(items, _ActiveItem.index(itemIndex)); + return i == -1 ? null : items[i]; + } + + int _indexToItemIndex(int index) { + int itemIndex = index; + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex <= itemIndex) { + itemIndex += 1; + } else { + break; + } + } + return itemIndex; + } + + int _itemIndexToIndex(int itemIndex) { + int index = itemIndex; + for (final _ActiveItem item in _outgoingItems) { + // assert(item.itemIndex != itemIndex); + if (item.itemIndex < itemIndex) { + index -= 1; + } else { + break; + } + } + return index; + } + + void insertItem(int index, {required Duration insertDuration}) { + assert(index >= 0); + final int itemIndex = _indexToItemIndex(index); + assert(itemIndex >= 0 && itemIndex <= _itemsCount); + + for (final _ActiveItem item in _incomingItems) { + if (item.itemIndex >= itemIndex) item.itemIndex += 1; + } + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex >= itemIndex) item.itemIndex += 1; + } + + final AnimationController controller = AnimationController( + duration: insertDuration, + vsync: this, + ); + final AnimationController sizeController = AnimationController( + duration: kAnimationDuration, + vsync: this, + ); + + final _ActiveItem incomingItem = _ActiveItem.animation( + controller, + itemIndex, + sizeController, + ); + + _incomingItems + ..add(incomingItem) + ..sort(); + + final motionData = + MotionData(endOffset: Offset.zero, startOffset: Offset.zero); + + final updatedChildrenMap = {}; + + if (childrenMap.containsKey(itemIndex)) { + for (final entry in childrenMap.entries) { + if (entry.key == itemIndex) { + updatedChildrenMap[itemIndex] = motionData.copyWith(visible: false); + updatedChildrenMap[entry.key + 1] = entry.value.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: getChildOffset(entry.key)); + } else if (entry.key > itemIndex) { + updatedChildrenMap[entry.key + 1] = entry.value.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: getChildOffset(entry.key)); + } else { + updatedChildrenMap[entry.key] = entry.value; + } + } + childrenMap.clear(); + childrenMap.addAll(updatedChildrenMap); + sizeController.forward().then((value) { + controller.forward().then((_) { + _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! + .controller! + .dispose(); + }); + }); + } else { + childrenMap[itemIndex] = motionData; + sizeController.value = kAlwaysCompleteAnimation.value; + controller.forward().then((_) { + _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! + .controller! + .dispose(); + }); + } + setState(() { + _itemsCount += 1; + }); + } + + void removeItem(int index, {required Duration removeItemDuration}) { + assert(index >= 0); + final int itemIndex = _indexToItemIndex(index); + if (itemIndex < 0 || itemIndex >= _itemsCount) { + return; + } + assert(itemIndex >= 0 && itemIndex < _itemsCount); + + assert(_activeItemAt(_outgoingItems, itemIndex) == null); + + if (childrenMap.containsKey(itemIndex)) { + final _ActiveItem? incomingItem = + _removeActiveItemAt(_incomingItems, itemIndex); + + final AnimationController sizeController = incomingItem?.sizeAnimation ?? + AnimationController( + vsync: this, duration: kAnimationDuration, value: 1.0); + final AnimationController controller = incomingItem?.controller ?? + AnimationController( + duration: removeItemDuration, value: 1.0, vsync: this) + ..addStatusListener((status) => ()); + final _ActiveItem outgoingItem = + _ActiveItem.animation(controller, itemIndex, sizeController); + _outgoingItems + ..add(outgoingItem) + ..sort(); + + controller.reverse().then((void value) { + if (controller.status == AnimationStatus.dismissed) { + if (childrenMap.containsKey(itemIndex)) { + childrenMap.update( + itemIndex, (value) => value.copyWith(visible: false)); + } + sizeController.reverse(from: 1.0).then((value) { + final removedItem = + _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!; + removedItem.controller!.dispose(); + removedItem.sizeAnimation!.dispose(); + + // Decrement the incoming and outgoing item indices to account + // for the removal. + for (final _ActiveItem item in _incomingItems) { + if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; + } + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; + } + _onItemRemoved(itemIndex, removeItemDuration); + }); + } + }); + } + } + + void _onItemRemoved(int itemIndex, Duration removeDuration) { + final updatedChildrenMap = {}; + if (childrenMap.containsKey(itemIndex)) { + for (final entry in childrenMap.entries) { + if (entry.key < itemIndex) { + updatedChildrenMap[entry.key] = childrenMap[entry.key]!; + } else if (entry.key == itemIndex) { + continue; + } else { + updatedChildrenMap[entry.key - 1] = childrenMap[entry.key]!.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: _itemOffsetAt(entry.key - 1)); + } + } + } + childrenMap.clear(); + childrenMap.addAll(updatedChildrenMap); + + setState(() => _itemsCount -= 1); + } + + Offset getChildOffset(int index) { + final currentOffset = _itemOffsetAt(index); + if (!isGrid) { + return currentOffset; + } + + if (widget.delegateBuilder + is SliverReorderableGridDelegateWithFixedCrossAxisCount) { + final delegateBuilder = widget.delegateBuilder + as SliverReorderableGridDelegateWithFixedCrossAxisCount; + return delegateBuilder.getOffset(index, currentOffset); + } else if (widget.delegateBuilder + is SliverReorderableGridWithMaxCrossAxisExtent) { + final delegateBuilder = + widget.delegateBuilder as SliverReorderableGridWithMaxCrossAxisExtent; + final offset = delegateBuilder.getOffset(index, currentOffset); + return offset; + } + return Offset.zero; + } + + Offset _itemOffsetAt(int index) { + final itemRenderBox = + _items[index]?.context.findRenderObject() as RenderBox?; + if (itemRenderBox == null) return Offset.zero; + return itemRenderBox.localToGlobal(Offset.zero); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.delegateBuilder != null + ? SliverGrid( + gridDelegate: widget.delegateBuilder!, delegate: _createDelegate()) + : SliverList(delegate: _createDelegate()); + } + + Widget _itemBuilder(BuildContext context, int index) { + final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, index); + final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, index); + + if (outgoingItem != null) { + // logger.d("Outgoing item in $_items at index $index"); + final child = _items[index]?.widget ?? Container(); + return _removeItemBuilder(outgoingItem, child); + } + if (_dragInfo != null && index >= _itemsCount) { + return SizedBox.fromSize(size: _dragInfo!.itemSize); + } + + final Widget child = widget.onReorder != null + ? reorderableItemBuilder(context, _itemIndexToIndex(index)) + : widget.itemBuilder(context, _itemIndexToIndex(index)); + + assert(() { + if (child.key == null) { + throw FlutterError( + 'Every item of AnimatedReorderableList must have a unique key.', + ); + } + return true; + }()); + + final Key itemGlobalKey = _MotionBuilderItemGlobalKey(child.key!, this); + final Widget builder = _insertItemBuilder(incomingItem, child); + + final motionData = childrenMap[index]; + if (motionData == null) return builder; + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + + return MotionAnimatedContent( + index: index, + key: itemGlobalKey, + motionData: motionData, + isGrid: isGrid, + updateMotionData: (MotionData motionData) { + final itemOffset = _itemOffsetAt(index); + childrenMap[index] = motionData.copyWith( + startOffset: itemOffset, endOffset: itemOffset, visible: true); + }, + capturedThemes: + InheritedTheme.capture(from: context, to: overlay.context), + child: builder, + ); + } + + SliverChildDelegate _createDelegate() { + return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); + } + + Widget reorderableItemBuilder(BuildContext context, int index) { + final Widget item = widget.itemBuilder(context, index); + + assert(() { + if (item.key == null) { + throw FlutterError( + 'Every item of AnimatedReorderableList must have a key.', + ); + } + return true; + }()); + final Key itemGlobalKey = _MotionBuilderItemGlobalKey(item.key!, this); + final Widget itemWithSemantics = _wrapWithSemantics(item, index); + + // if (widget.useDefaultDragListeners) { + if (!widget.longPressDraggable) { + return _wrapWithSemantics(item, index, itemGlobalKey); + } + if (widget.buildDefaultDragHandles) { + switch (Theme.of(context).platform) { + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + switch (widget.scrollDirection) { + case Axis.horizontal: + return Stack( + key: itemGlobalKey, + children: [ + itemWithSemantics, + Positioned.directional( + textDirection: Directionality.of(context), + start: 0, + end: 0, + bottom: 8, + child: Align( + alignment: Alignment.bottomCenter, + child: ReorderableDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ), + )) + ], + ); + case Axis.vertical: + return Stack( + key: itemGlobalKey, + children: [ + itemWithSemantics, + Positioned.directional( + textDirection: Directionality.of(context), + top: 0, + bottom: 0, + end: 8, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: ReorderableGridDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ), + )) + ], + ); + } + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + return ReorderableGridDelayedDragStartListener( + key: itemGlobalKey, index: index, child: item); + } + } + + const bool enable = true; + + return ReorderableGridDelayedDragStartListener( + key: itemGlobalKey, + index: index, + enabled: enable, + child: itemWithSemantics, + ); + } + + Widget _wrapWithSemantics(Widget child, int index, [dynamic key]) { + void reorder(int startIndex, int endIndex) { + if (startIndex != endIndex) { + widget.onReorder?.call(startIndex, endIndex); + } + } + + // First, determine which semantics actions apply. + final Map semanticsActions = + {}; + + // Create the appropriate semantics actions. + void moveToStart() => reorder(index, 0); + void moveToEnd() => reorder(index, _itemsCount); + void moveBefore() => reorder(index, index - 1); + // To move after, we go to index+2 because we are moving it to the space + // before index+2, which is after the space at index+1. + void moveAfter() => reorder(index, index + 2); + + final WidgetsLocalizations localizations = WidgetsLocalizations.of(context); + + // If the item can move to before its current position in the grid. + if (index > 0) { + semanticsActions[ + CustomSemanticsAction(label: localizations.reorderItemToStart)] = + moveToStart; + String reorderItemBefore = localizations.reorderItemUp; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemBefore = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemLeft + : localizations.reorderItemRight; + } + semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = + moveBefore; + } + + // If the item can move to after its current position in the grid. + if (index < _itemsCount - 1) { + String reorderItemAfter = localizations.reorderItemDown; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemAfter = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemRight + : localizations.reorderItemLeft; + } + semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = + moveAfter; + semanticsActions[ + CustomSemanticsAction(label: localizations.reorderItemToEnd)] = + moveToEnd; + } + + // We pass toWrap with a GlobalKey into the item so that when it + // gets dragged, the accessibility framework can preserve the selected + // state of the dragging item. + // + // We also apply the relevant custom accessibility actions for moving the item + // up, down, to the start, and to the end of the grid. + return MergeSemantics( + key: key, + child: Semantics( + customSemanticsActions: semanticsActions, + child: child, + ), + ); + } + + Widget _removeItemBuilder(_ActiveItem outgoingItem, Widget child) { + final Animation animation = + outgoingItem.controller ?? kAlwaysCompleteAnimation; + final Animation sizeAnimation = + outgoingItem.sizeAnimation ?? kAlwaysCompleteAnimation; + return SizeTransition( + sizeFactor: sizeAnimation, + child: widget.removeAnimationBuilder(context, child, animation)); + } + + Widget _insertItemBuilder(_ActiveItem? incomingItem, Widget child) { + final Animation animation = + incomingItem?.controller ?? kAlwaysCompleteAnimation; + final Animation sizeAnimation = + incomingItem?.sizeAnimation ?? kAlwaysCompleteAnimation; + return SizeTransition( + axis: widget.scrollDirection, + sizeFactor: sizeAnimation, + child: widget.insertAnimationBuilder(context, child, animation)); + } +} + +Offset _extentOffset(double extent, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return Offset(extent, 0.0); + case Axis.vertical: + return Offset(0.0, extent); + } +} + +@optionalTypeArgs +class _MotionBuilderItemGlobalKey extends GlobalObjectKey { + const _MotionBuilderItemGlobalKey(this.subKey, this.state) : super(subKey); + + final Key subKey; + final State state; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _MotionBuilderItemGlobalKey && + other.subKey == subKey && + other.state == state; + } + + @override + int get hashCode => Object.hash( + subKey, + state, + ); +} + +class _ActiveItem implements Comparable<_ActiveItem> { + _ActiveItem.animation(this.controller, this.itemIndex, this.sizeAnimation); + + _ActiveItem.index(this.itemIndex) + : controller = null, + sizeAnimation = null; + + final AnimationController? controller; + final AnimationController? sizeAnimation; + int itemIndex; + + @override + int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart new file mode 100644 index 00000000..843a9e75 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart @@ -0,0 +1,253 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'motion_animated_builder.dart'; + +typedef ItemBuilder = Widget Function( + BuildContext context, int index); + +typedef AnimatedWidgetBuilder = Widget Function( + Widget child, Animation animation); + +typedef EqualityChecker = bool Function(E, E); + +const Duration kAnimationDuration = Duration(milliseconds: 300); + +abstract class MotionListBase + extends StatefulWidget { + final ItemBuilder itemBuilder; + final List items; + final ReorderCallback? onReorder; + final void Function(int)? onReorderStart; + final void Function(int)? onReorderEnd; + final ReorderItemProxyDecorator? proxyDecorator; + final List? enterTransition; + final List? exitTransition; + final Duration? insertDuration; + final Duration? removeDuration; + final Axis scrollDirection; + final SliverGridDelegate? sliverGridDelegate; + final AnimatedWidgetBuilder? insertItemBuilder; + final AnimatedWidgetBuilder? removeItemBuilder; + final bool? buildDefaultDragHandles; + final bool useDefaultDragListeners; + final bool? longPressDraggable; + final bool Function(E a, E b)? isSameItem; + + const MotionListBase( + {super.key, + required this.items, + required this.itemBuilder, + this.onReorder, + this.onReorderEnd, + this.onReorderStart, + this.proxyDecorator, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + required this.scrollDirection, + this.sliverGridDelegate, + this.insertItemBuilder, + this.removeItemBuilder, + this.buildDefaultDragHandles, + this.longPressDraggable, + required this.useDefaultDragListeners, + this.isSameItem}); +} + +abstract class MotionListBaseState< + W extends Widget, + B extends MotionListBase, + E extends Object> extends State with TickerProviderStateMixin { + late List oldList; + + Duration _enterDuration = kAnimationDuration; + Duration _exitDuration = kAnimationDuration; + + List _enterAnimations = []; + List _exitAnimations = []; + + Duration get enterDuration => _enterDuration; + + Duration get exitDuration => _exitDuration; + + @protected + GlobalKey listKey = GlobalKey(); + + @nonVirtual + @protected + MotionBuilderState get list => listKey.currentState!; + + @nonVirtual + @protected + ItemBuilder get itemBuilder => widget.itemBuilder; + + @nonVirtual + @protected + SliverGridDelegate? get sliverGridDelegate => widget.sliverGridDelegate; + + @nonVirtual + @protected + ReorderCallback? get onReorder => widget.onReorder; + + @nonVirtual + @protected + void Function(int)? get onReorderStart => widget.onReorderStart; + + @nonVirtual + @protected + void Function(int)? get onReorderEnd => widget.onReorderEnd; + + @nonVirtual + @protected + ReorderItemProxyDecorator? get proxyDecorator => widget.proxyDecorator; + + @nonVirtual + @protected + Duration get insertDuration => widget.insertDuration ?? enterDuration; + + @nonVirtual + @protected + Duration get removeDuration => widget.removeDuration ?? exitDuration; + + @protected + @nonVirtual + Axis get scrollDirection => widget.scrollDirection; + + @nonVirtual + @protected + List get enterTransition => widget.enterTransition ?? []; + + @nonVirtual + @protected + List get exitTransition => widget.exitTransition ?? []; + + @nonVirtual + @protected + bool get buildDefaultDragHandles => widget.buildDefaultDragHandles ?? false; + + @nonVirtual + @protected + bool get longPressDraggable => widget.longPressDraggable ?? false; + + @nonVirtual + @protected + bool get useDefaultDragListeners => widget.useDefaultDragListeners ?? true; + + @nonVirtual + @protected + bool Function(E a, E b) get isSameItem => + widget.isSameItem ?? (a, b) => a == b; + + @override + void initState() { + super.initState(); + oldList = List.from(widget.items); + + addEffects(enterTransition, _enterAnimations, enter: true); + addEffects(exitTransition, _exitAnimations, enter: false); + } + + @override + void didUpdateWidget(covariant B oldWidget) { + final newList = widget.items; + if (!listEquals(oldWidget.enterTransition, enterTransition)) { + _enterAnimations = []; + addEffects(enterTransition, _enterAnimations, enter: true); + } + if (!listEquals(oldWidget.exitTransition, exitTransition)) { + _exitAnimations = []; + addEffects(exitTransition, _exitAnimations, enter: false); + } + calculateDiff(oldList, newList); + oldList = List.from(newList); + super.didUpdateWidget(oldWidget); + } + + void addEffects(List effects, List enteries, + {required bool enter}) { + if (effects.isNotEmpty) { + for (AnimationEffect effect in effects) { + addEffect(effect, enteries, enter: enter); + } + } else { + addEffect(FadeIn(), enteries, enter: enter); + } + } + + void addEffect(AnimationEffect effect, List enteries, + {required bool enter}) { + Duration zero = Duration.zero; + final timeForAnimation = + (effect.delay ?? zero) + (effect.duration ?? kAnimationDuration); + if (enter) { + _enterDuration = + timeForAnimation > _enterDuration ? timeForAnimation : _enterDuration; + assert(_enterDuration >= zero, "Duration can not be negative"); + } else { + _exitDuration = + timeForAnimation > _exitDuration ? timeForAnimation : _exitDuration; + assert(_exitDuration >= zero, "Duration can not be negative"); + } + + EffectEntry entry = EffectEntry( + animationEffect: effect, + delay: effect.delay ?? zero, + duration: effect.duration ?? kAnimationDuration, + curve: effect.curve ?? Curves.linear); + + enteries.add(entry); + } + + void calculateDiff(List oldList, List newList) { + // Detect removed and updated items + for (int i = oldList.length - 1; i >= 0; i--) { + if (newList.indexWhere((element) => isSameItem(oldList[i], element)) == + -1) { + listKey.currentState!.removeItem(i, removeItemDuration: removeDuration); + } + } + // Detect added items + for (int i = 0; i < newList.length; i++) { + if (oldList.indexWhere((element) => isSameItem(newList[i], element)) == + -1) { + listKey.currentState!.insertItem(i, insertDuration: insertDuration); + } + } + } + + @nonVirtual + @protected + Widget insertAnimationBuilder( + BuildContext context, Widget child, Animation animation) { + if (widget.insertItemBuilder != null) { + return widget.insertItemBuilder!(child, animation); + } else { + Widget animatedChild = child; + for (EffectEntry entry in _enterAnimations) { + animatedChild = entry.animationEffect + .build(context, animatedChild, animation, entry, insertDuration); + } + return animatedChild; + } + } + + @nonVirtual + @protected + Widget removeAnimationBuilder( + BuildContext context, Widget child, Animation animation) { + if (widget.removeItemBuilder != null) { + return widget.removeItemBuilder!(child, animation); + } else { + Widget animatedChild = child; + for (EffectEntry entry in _exitAnimations) { + animatedChild = entry.animationEffect + .build(context, animatedChild, animation, entry, removeDuration); + } + return animatedChild; + } + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart new file mode 100644 index 00000000..903e9d9d --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart @@ -0,0 +1,97 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/material.dart'; + +import 'motion_animated_builder.dart'; +import 'motion_list_base.dart'; + +class MotionListImpl extends MotionListBase { + const MotionListImpl({ + super.key, + required super.items, + required super.itemBuilder, + super.enterTransition, + super.exitTransition, + super.insertDuration, + super.removeDuration, + super.onReorder, + super.onReorderStart, + super.onReorderEnd, + super.proxyDecorator, + required super.scrollDirection, + super.insertItemBuilder, + super.removeItemBuilder, + super.buildDefaultDragHandles, + super.useDefaultDragListeners = true, + super.longPressDraggable, + super.isSameItem, + }); + + const MotionListImpl.grid({ + Key? key, + required List items, + required ItemBuilder itemBuilder, + required SliverGridDelegate sliverGridDelegate, + List? enterTransition, + List? exitTransition, + ReorderCallback? onReorder, + void Function(int)? onReorderStart, + void Function(int)? onReorderEnd, + ReorderItemProxyDecorator? proxyDecorator, + Duration? insertDuration, + Duration? removeDuration, + required Axis scrollDirection, + AnimatedWidgetBuilder? insertItemBuilder, + AnimatedWidgetBuilder? removeItemBuilder, + bool? buildDefaultDragHandles, + bool useDefaultDragListeners = true, + bool? longPressDraggable, + bool Function(E a, E b)? isSameItem, + }) : super( + key: key, + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + buildDefaultDragHandles: buildDefaultDragHandles, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + isSameItem: isSameItem); + + @override + MotionListImplState createState() => MotionListImplState(); +} + +class MotionListImplState + extends MotionListBaseState, E> { + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + assert(debugCheckHasOverlay(context)); + return MotionBuilder( + key: listKey, + initialCount: oldList.length, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + insertAnimationBuilder: insertAnimationBuilder, + removeAnimationBuilder: removeAnimationBuilder, + itemBuilder: itemBuilder, + scrollDirection: scrollDirection, + delegateBuilder: sliverGridDelegate, + buildDefaultDragHandles: buildDefaultDragHandles, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart new file mode 100644 index 00000000..7c731edf --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart @@ -0,0 +1,181 @@ +part of '../builder/motion_animated_builder.dart'; + +typedef _DragItemUpdate = void Function( + _DragInfo item, Offset position, Offset delta); +typedef _DragItemCallback = void Function(_DragInfo item); + +class _DragInfo extends Drag { + final bool gridView; + final Axis scrollDirection; + final _DragItemUpdate? onUpdate; + final _DragItemCallback? onEnd; + final _DragItemCallback? onCancel; + final VoidCallback? onDragCompleted; + final ReorderItemProxyDecorator? proxyDecorator; + final TickerProvider tickerProvider; + + late MotionBuilderState listState; + late int index; + late Widget child; + late Offset dragPosition; + late Offset dragOffset; + late Size itemSize; + late double itemExtent; + late CapturedThemes capturedThemes; + ScrollableState? scrollable; + AnimationController? _proxyAnimation; + + _DragInfo({ + required MotionAnimatedContentState item, + Offset initialPosition = Offset.zero, + required this.gridView, + this.scrollDirection = Axis.vertical, + this.onUpdate, + this.onEnd, + this.onCancel, + this.onDragCompleted, + this.proxyDecorator, + required this.tickerProvider, + }) { + final RenderBox itemRenderBox = + item.context.findRenderObject()! as RenderBox; + listState = item.listState; + index = item.index; + child = item.widget.child; + capturedThemes = item.widget.capturedThemes!; + dragPosition = initialPosition; + dragOffset = itemRenderBox.globalToLocal(initialPosition); + itemSize = item.context.size!; + itemExtent = _sizeExtent(itemSize, scrollDirection); + scrollable = Scrollable.of(item.context); + } + + void dispose() { + _proxyAnimation?.dispose(); + } + + void startDrag() { + _proxyAnimation = AnimationController( + vsync: tickerProvider, duration: const Duration(milliseconds: 250)) + ..addStatusListener((status) { + if (status == AnimationStatus.dismissed) { + _dropCompleted(); + } + }) + ..forward(); + } + + @override + void update(DragUpdateDetails details) { + final Offset delta = !gridView + ? _restrictAxis(details.delta, scrollDirection) + : details.delta; + dragPosition += delta; + onUpdate?.call(this, dragPosition, details.delta); + } + + @override + void end(DragEndDetails details) { + _proxyAnimation!.reverse(); + onEnd?.call(this); + } + + @override + void cancel() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onCancel?.call(this); + } + + void _dropCompleted() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onDragCompleted?.call(); + } + + Widget createProxy(BuildContext context) { + return capturedThemes.wrap(_DragItemProxy( + listState: listState, + index: index, + position: dragPosition - dragOffset - _overlayOrigin(context), + size: itemSize, + animation: _proxyAnimation!, + proxyDecorator: proxyDecorator, + child: child)); + } +} + +double _sizeExtent(Size size, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return size.width; + case Axis.vertical: + return size.height; + } +} + +Offset _restrictAxis(Offset offset, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return Offset(offset.dx, 0.0); + case Axis.vertical: + return Offset(0.0, offset.dy); + } +} + +Offset _overlayOrigin(BuildContext context) { + final OverlayState overlay = + Overlay.of(context, debugRequiredFor: context.widget); + final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; + return overlayBox.localToGlobal(Offset.zero); +} + +class _DragItemProxy extends StatelessWidget { + final MotionBuilderState listState; + final int index; + final Widget child; + final Offset position; + final Size size; + final AnimationController animation; + final ReorderItemProxyDecorator? proxyDecorator; + + const _DragItemProxy( + {required this.listState, + required this.index, + required this.child, + required this.position, + required this.size, + required this.animation, + required this.proxyDecorator}); + + @override + Widget build(BuildContext context) { + final Widget proxyChild = + proxyDecorator?.call(child, index, animation.view) ?? child; + final Offset overlayOrigin = _overlayOrigin(context); + return MediaQuery( + data: MediaQuery.of(context).removePadding(removeTop: true), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + Offset effectivePosition = position; + final Offset? dropPosition = listState._finalDropPosition; + if (dropPosition != null) { + effectivePosition = Offset.lerp( + dropPosition - overlayOrigin, + effectivePosition, + Curves.easeOut.transform(animation.value))!; + } + return Positioned( + left: effectivePosition.dx, + top: effectivePosition.dy, + child: SizedBox( + width: size.width, + height: size.height, + child: child, + )); + }, + child: proxyChild, + )); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart new file mode 100644 index 00000000..8b6c7516 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart @@ -0,0 +1,95 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; + +import '../builder/motion_animated_builder.dart'; + +class ReorderableGridDragStartListener extends StatelessWidget { + /// Creates a listener for a drag immediately following a pointer down + /// event over the given child widget. + /// + /// This is most commonly used to wrap part of a grid item like a drag + /// handle. + const ReorderableGridDragStartListener({ + super.key, + required this.child, + required this.index, + this.enabled = true, + }); + + /// The widget for which the application would like to respond to a tap and + /// drag gesture by starting a reordering drag on a reorderable grid. + final Widget child; + + /// The index of the associated item that will be dragged in the grid. + final int index; + + /// Whether the [child] item can be dragged and moved in the grid. + /// + /// If true, the item can be moved to another location in the grid when the + /// user taps on the child. If false, tapping on the child will be ignored. + final bool enabled; + + @override + Widget build(BuildContext context) { + return Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: enabled + ? (PointerDownEvent event) => _startDragging(context, event) + : null, + child: child, + ); + } + + /// Provides the gesture recognizer used to indicate the start of a reordering + /// drag operation. + /// + /// By default this returns an [ImmediateMultiDragGestureRecognizer] but + /// subclasses can use this to customize the drag start gesture. + @protected + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(debugOwner: this,delay: const Duration(milliseconds: 1)); + } + + void _startDragging(BuildContext context, PointerDownEvent event) { + final MotionBuilderState? list = MotionBuilder.maybeOf(context); + list?.startItemDragReorder( + index: index, + event: event, + recognizer: createRecognizer(), + ); + } +} + +/// A wrapper widget that will recognize the start of a drag operation by +/// looking for a long press event. Once it is recognized, it will start +/// a drag operation on the wrapped item in the reorderable grid. +/// +/// See also: +/// +/// * [ReorderableGridDragStartListener], a similar wrapper that will +/// recognize the start of the drag immediately after a pointer down event. +/// * [ReorderableGrid], a widget grid that allows the user to reorder +/// its items. +/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder +/// its items. +/// * [ReorderableGridView], a material design grid that allows the user to +/// reorder its items. +class ReorderableGridDelayedDragStartListener + extends ReorderableGridDragStartListener { + /// Creates a listener for an drag following a long press event over the + /// given child widget. + /// + /// This is most commonly used to wrap an entire grid item in a reorderable + /// grid. + const ReorderableGridDelayedDragStartListener({ + super.key, + required super.child, + required super.index, + super.enabled, + }); + + @override + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(debugOwner: this); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart new file mode 100644 index 00000000..882aef28 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart @@ -0,0 +1,221 @@ +part of '../builder/motion_animated_builder.dart'; + +class MotionAnimatedContent extends StatefulWidget { + final int index; + final MotionData motionData; + final Widget child; + final Function(MotionData)? updateMotionData; + final CapturedThemes? capturedThemes; + final bool isGrid; + + const MotionAnimatedContent({ + Key? key, + required this.index, + required this.motionData, + required this.child, + this.updateMotionData, + required this.capturedThemes, + required this.isGrid, + }) : super(key: key); + + @override + State createState() => MotionAnimatedContentState(); +} + +class MotionAnimatedContentState extends State + with SingleTickerProviderStateMixin { + late MotionBuilderState listState; + + Offset _targetOffset = Offset.zero; + Offset _startOffset = Offset.zero; + AnimationController? _offsetAnimation; + + bool _dragging = false; + + bool get dragging => _dragging; + + set dragging(bool dragging) { + if (mounted) { + setState(() { + _dragging = dragging; + }); + } + } + + Size _dragSize = Size.zero; + + set dragSize(Size itemSize) { + if (mounted) { + setState(() { + _dragSize = itemSize; + }); + } + } + + int get index => widget.index; + bool visible = true; + + @override + void initState() { + listState = MotionBuilder.of(context); + listState.registerItem(this); + visible = widget.motionData.visible; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + widget.updateMotionData?.call(widget.motionData); + }); + Future.delayed(kAnimationDuration).then((value) { + visible = true; + rebuild(); + }); + super.initState(); + } + + @override + void didUpdateWidget(covariant MotionAnimatedContent oldWidget) { + if (oldWidget.index != widget.index) { + listState.unregisterItem(oldWidget.index, this); + listState.registerItem(this); + } + if (oldWidget.index != widget.index && !_dragging && widget.isGrid) { + _updateAnimationTranslation(); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) { + setState(() { + visible = true; + }); + widget.updateMotionData?.call(widget.motionData); + } + }); + super.didUpdateWidget(oldWidget); + } + + void _updateAnimationTranslation() { + Offset offsetDiff = + (widget.motionData.startOffset + offset) - widget.motionData.endOffset; + _startOffset = offsetDiff; + if (offsetDiff.dx != 0 || offsetDiff.dy != 0) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: listState, + duration: kAnimationDuration, + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + widget.updateMotionData?.call(widget.motionData); + + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offsetDiff; + _offsetAnimation!.forward(from: 0.0); + } + } + } + + Offset get offset { + if (_offsetAnimation != null) { + final Offset offset = + Offset.lerp(_startOffset, _targetOffset, _offsetAnimation!.value)!; + return offset; + } + return _targetOffset; + } + + void updateForGap(bool animate) { + if (!mounted) return; + final Offset newTargetOffset = listState.calculateNextDragOffset(index); + if (newTargetOffset == _targetOffset) return; + _targetOffset = newTargetOffset; + + if (animate) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: listState, + duration: const Duration(milliseconds: 250), + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offset; + _offsetAnimation!.forward(from: 0.0); + } + } else { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = _targetOffset; + } + rebuild(); + } + + @override + Widget build(BuildContext context) { + listState.registerItem(this); + return Visibility( + maintainSize: true, + maintainAnimation: true, + maintainState: true, + visible: visible && !_dragging, + child: Transform.translate( + offset: offset, + child: + !_dragging ? widget.child : SizedBox.fromSize(size: _dragSize)), + ); + } + + Offset itemOffset() { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return Offset.zero; + return box.localToGlobal(Offset.zero); + } + + void resetGap() { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = Offset.zero; + _targetOffset = Offset.zero; + rebuild(); + } + + Rect targetGeometryNonOffset() { + final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; + final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero); + return itemPosition & itemRenderBox.size; + } + + void rebuild() { + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + listState.unregisterItem(widget.index, this); + _offsetAnimation?.dispose(); + super.dispose(); + } + + @override + void deactivate() { + listState.unregisterItem(index, this); + super.deactivate(); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart new file mode 100644 index 00000000..188035ed --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart @@ -0,0 +1,157 @@ +import 'dart:math'; +import 'package:flutter/rendering.dart'; + +class SliverGridWithCustomGeometryLayout extends SliverGridRegularTileLayout { + final SliverGridGeometry Function( + int index, SliverGridRegularTileLayout layout) geometryBuilder; + + const SliverGridWithCustomGeometryLayout({ + required this.geometryBuilder, + required int crossAxisCount, + required double mainAxisStride, + required double crossAxisStride, + required double childMainAxisExtent, + required double childCrossAxisExtent, + required bool reverseCrossAxis, + }) : assert(crossAxisCount > 0), + assert(mainAxisStride >= 0), + assert(crossAxisStride >= 0), + assert(childMainAxisExtent >= 0), + assert(childCrossAxisExtent >= 0), + super( + crossAxisCount: crossAxisCount, + mainAxisStride: mainAxisStride, + crossAxisStride: crossAxisStride, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: reverseCrossAxis); + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + return geometryBuilder(index, this); + } +} + +/// Creates grid layouts with a fixed number of tiles in the cross axis. +/// +/// For example, if the grid is vertical, this delegate will create a layout +/// with a fixed number of columns. If the grid is horizontal, this delegate +/// will create a layout with a fixed number of rows. +/// +/// This delegate creates grids with equally sized and spaced tiles. + +class SliverReorderableGridDelegateWithFixedCrossAxisCount + extends SliverGridDelegateWithFixedCrossAxisCount { + /// The number of children in the cross axis. + final int crossAxisCount; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// The ratio of the cross-axis to the main-axis extent of each child. + final double childAspectRatio; + + /// The extent of each tile in the main axis. If provided it would define the + /// logical pixels taken by each tile in the main-axis. + /// + /// If null, [childAspectRatio] is used instead. + final double? mainAxisExtent; + + double childCrossAxisExtent = 0.0; + double childMainAxisExtent = 0.0; + + /// Creates a delegate that makes grid layouts with a fixed number of tiles in + /// the cross axis. + /// + /// The `mainAxisSpacing`, `mainAxisExtent` and `crossAxisSpacing` arguments + /// must not be negative. The `crossAxisCount` and `childAspectRatio` + /// arguments must be greater than zero. + + SliverReorderableGridDelegateWithFixedCrossAxisCount({ + required this.crossAxisCount, + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.childAspectRatio = 1.0, + this.mainAxisExtent, + }) : assert(crossAxisCount > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(childAspectRatio > 0), + super( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + ); + + bool _debugAssertIsValid() { + assert(crossAxisCount > 0); + assert(mainAxisSpacing >= 0); + assert(crossAxisSpacing >= 0); + assert(childAspectRatio > 0); + return true; + } + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + assert(_debugAssertIsValid()); + final usableCrossAxisCount = max(0.0, + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1)); + + childCrossAxisExtent = usableCrossAxisCount / crossAxisCount; + childMainAxisExtent = childCrossAxisExtent / childAspectRatio; + return SliverGridWithCustomGeometryLayout( + geometryBuilder: (index, layout) { + return SliverGridGeometry( + scrollOffset: (index ~/ crossAxisCount) * layout.mainAxisStride, + crossAxisOffset: _getOffsetFromStartInCrossAxis(index, layout), + mainAxisExtent: childMainAxisExtent, + crossAxisExtent: childCrossAxisExtent); + }, + crossAxisCount: crossAxisCount, + mainAxisStride: childMainAxisExtent + mainAxisSpacing, + crossAxisStride: childCrossAxisExtent + crossAxisSpacing, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: + axisDirectionIsReversed(constraints.crossAxisDirection)); + } + + Offset getOffset(int index, Offset currentOffset) { + final int col = index % crossAxisCount; + final crossAxisStart = crossAxisSpacing; + + if (col == crossAxisCount - 1) { + return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent); + } else { + return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy); + } + } + + double _getOffsetFromStartInCrossAxis( + int index, + SliverGridRegularTileLayout layout, + ) { + final crossAxisStart = (index % crossAxisCount) * layout.crossAxisStride; + + if (layout.reverseCrossAxis) { + return crossAxisCount * layout.crossAxisStride - + crossAxisStart - + layout.childCrossAxisExtent - + (layout.crossAxisStride - layout.childCrossAxisExtent); + } + return crossAxisStart; + } + + @override + bool shouldRelayout(SliverGridDelegateWithFixedCrossAxisCount oldDelegate) { + return oldDelegate.crossAxisCount != crossAxisCount || + oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing || + oldDelegate.childAspectRatio != childAspectRatio || + oldDelegate.mainAxisExtent != mainAxisExtent; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart new file mode 100644 index 00000000..0c06521b --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart @@ -0,0 +1,128 @@ +import 'package:flutter/rendering.dart'; + +import 'dart:math' as math; + +/// Creates grid layouts with tiles that each have a maximum cross-axis extent. +/// +/// This delegate will select a cross-axis extent for the tiles that is as +/// large as possible subject to the following conditions: +/// +/// - The extent evenly divides the cross-axis extent of the grid. +/// - The extent is at most [maxCrossAxisExtent]. +/// +/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and +/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 +/// columns that are 125.0 pixels wide. +/// +/// This delegate creates grids with equally sized and spaced tiles. + +class SliverReorderableGridWithMaxCrossAxisExtent + extends SliverGridDelegateWithMaxCrossAxisExtent { + /// The maximum extent of tiles in the cross axis. + /// + /// This delegate will select a cross-axis extent for the tiles that is as + /// large as possible subject to the following conditions: + /// + /// - The extent evenly divides the cross-axis extent of the grid. + /// - The extent is at most [maxCrossAxisExtent]. + /// + /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and + /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 + /// columns that are 125.0 pixels wide. + final double maxCrossAxisExtent; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// The ratio of the cross-axis to the main-axis extent of each child. + final double childAspectRatio; + + /// The extent of each tile in the main axis. If provided it would define the + /// logical pixels taken by each tile in the main-axis. + /// + /// If null, [childAspectRatio] is used instead. + final double? mainAxisExtent; + int crossAxisCount = 0; + double childCrossAxisExtent = 0.0; + double childMainAxisExtent = 0.0; + + /// Creates a delegate that makes grid layouts with tiles that have a maximum + /// cross-axis extent. + /// + /// The [maxCrossAxisExtent], [mainAxisExtent], [mainAxisSpacing], + /// and [crossAxisSpacing] arguments must not be negative. + /// The [childAspectRatio] argument must be greater than zero. + SliverReorderableGridWithMaxCrossAxisExtent({ + required this.maxCrossAxisExtent, + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.childAspectRatio = 1.0, + this.mainAxisExtent, + }) : assert(maxCrossAxisExtent > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(childAspectRatio > 0), + super( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + mainAxisExtent: mainAxisExtent); + + bool _debugAssertIsValid(double crossAxisExtent) { + assert(maxCrossAxisExtent > 0); + assert(mainAxisSpacing >= 0); + assert(crossAxisSpacing >= 0); + assert(childAspectRatio > 0); + return true; + } + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + assert(_debugAssertIsValid(constraints.crossAxisExtent)); + int childCrossAxisCount = + (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) + .ceil(); + // Ensure a minimum count of 1, can be zero and result in an infinite extent + // below when the window size is 0. + crossAxisCount = math.max(1, childCrossAxisCount); + final double usableCrossAxisExtent = math.max( + 0.0, + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), + ); + childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; + childMainAxisExtent = + mainAxisExtent ?? childCrossAxisExtent / childAspectRatio; + return SliverGridRegularTileLayout( + crossAxisCount: crossAxisCount, + mainAxisStride: childMainAxisExtent + mainAxisSpacing, + crossAxisStride: childCrossAxisExtent + crossAxisSpacing, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + Offset getOffset(int index, Offset currentOffset) { + final int col = index % crossAxisCount; + final crossAxisStart = crossAxisSpacing; + + if (col == crossAxisCount - 1) { + return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent); + } else { + return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy); + } + } + + @override + bool shouldRelayout(SliverGridDelegateWithMaxCrossAxisExtent oldDelegate) { + return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || + oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing || + oldDelegate.childAspectRatio != childAspectRatio || + oldDelegate.mainAxisExtent != mainAxisExtent; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart new file mode 100644 index 00000000..06de5a9f --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart @@ -0,0 +1,30 @@ +import 'package:flutter/cupertino.dart'; + +class MotionData { + final Offset startOffset; + final Offset endOffset; + final bool visible; + + MotionData( + {this.startOffset = Offset.zero, + this.endOffset = Offset.zero, + this.visible = true}); + + MotionData copyWith({Offset? startOffset, Offset? endOffset, bool? visible}) { + return MotionData( + startOffset: startOffset ?? this.startOffset, + endOffset: endOffset ?? this.endOffset, + visible: visible ?? this.visible); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MotionData && + runtimeType == other.runtimeType && + startOffset == other.startOffset && + endOffset == other.endOffset; + + @override + int get hashCode => startOffset.hashCode ^ endOffset.hashCode; +} diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 9e4f3fe2..21faa7c2 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -1,21 +1,17 @@ -import 'package:clock_app/common/logic/get_list_filter_chips.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/reorderable_list_decorator.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart'; import 'package:clock_app/common/widgets/list/delete_alert_dialogue.dart'; -import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; +import 'package:clock_app/common/widgets/list/list_filter_bar.dart'; import 'package:clock_app/common/widgets/list/list_item_card.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:great_list_view/great_list_view.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -typedef ItemCardBuilder = Widget Function( - BuildContext context, - int index, - AnimatedWidgetBuilderData data, -); class CustomListView extends StatefulWidget { const CustomListView({ @@ -34,11 +30,13 @@ class CustomListView extends StatefulWidget { this.isDeleteEnabled = true, this.isDuplicateEnabled = true, this.shouldInsertOnTop = true, + this.isSelectable = false, this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], this.initialSortIndex = 0, this.onChangeSortIndex, + this.header, }); final List items; @@ -60,6 +58,8 @@ class CustomListView extends StatefulWidget { final List> customActions; final List> sortOptions; final Function(int index)? onChangeSortIndex; + final Widget? header; + final bool isSelectable; @override State createState() => _CustomListViewState(); @@ -68,47 +68,64 @@ class CustomListView extends StatefulWidget { class _CustomListViewState extends State> { late List currentList = List.from(widget.items); - double _itemCardHeight = 0; final _scrollController = ScrollController(); - final _controller = AnimatedListController(); - late int selectedSortIndex = widget.initialSortIndex; + // final _controller = AnimatedListController(); + late int _selectedSortIndex = widget.initialSortIndex; + late Setting _longPressActionSetting; + List _selectedIds = []; + bool _isSelecting = false; + // bool _isReordering = false; @override void initState() { super.initState(); + + _longPressActionSetting = appSettings + .getGroup("General") + .getGroup("Interactions") + .getSetting("Long Press Action"); + + _longPressActionSetting.addListener(_handleUpdateSettings); + widget.listController.setChangeItems(_handleChangeItems); widget.listController.setAddItem(_handleAddItem); widget.listController.setDeleteItem(_handleDeleteItem); widget.listController.setGetItemIndex(_getItemIndex); widget.listController.setDuplicateItem(_handleDuplicateItem); widget.listController.setReloadItems(_handleReloadItems); - widget.listController.setClearItems(_handleClear); + widget.listController.setClearItems(_handleClearItems); widget.listController.setGetItems(() => widget.items); - updateCurrentList(); + _updateCurrentList(); // widget.listController.setChangeItemWithId(_handleChangeItemWithId); } + @override + void dispose() { + _longPressActionSetting.removeListener(_handleUpdateSettings); + super.dispose(); + } + + void _handleUpdateSettings(dynamic value) { + _endSelection(); + } + void _handleReloadItems(List items) { setState(() { widget.items.clear(); widget.items.addAll(items); - updateCurrentList(); + _updateCurrentList(); }); - // TODO: MAN THIS SUCKS, WHY YOU GOTTA DO THIS - _controller.notifyRemovedRange( - 0, widget.items.length - 1, _getChangeListBuilder()); - _controller.notifyInsertedRange(0, widget.items.length); } - void updateCurrentList() { - if (selectedSortIndex > widget.sortOptions.length) { - selectedSortIndex = 0; + void _updateCurrentList() { + if (_selectedSortIndex > widget.sortOptions.length) { + _selectedSortIndex = 0; } currentList.clear(); - if (selectedSortIndex != 0) { + if (_selectedSortIndex != 0) { final temp = [...widget.items]; - temp.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction); + temp.sort(widget.sortOptions[_selectedSortIndex - 1].sortFunction); currentList.addAll(temp); } else { currentList.addAll(widget.items); @@ -118,41 +135,19 @@ class _CustomListViewState int _getItemIndex(Item item) => currentList.indexWhere((element) => element.id == item.id); - void _updateItemHeight() { - if (_itemCardHeight == 0) { - _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; - } - } - - void _notifyChangeList() { - _controller.notifyChangedRange( - 0, - currentList.length, - _getChangeListBuilder(), - ); - } - - ItemCardBuilder _getChangeWidgetBuilder(Item item) { - _updateItemHeight(); - return (context, index, data) => data.measuring - ? SizedBox(height: _itemCardHeight) - : ListItemCard( - key: ValueKey(item), - onTap: () {}, - onDelete: () {}, - onDuplicate: () {}, - child: widget.itemBuilder(item), - ); - } - - ItemCardBuilder _getChangeListBuilder() => (context, index, data) => - _getChangeWidgetBuilder(widget.items[index])(context, index, data); + // void _updateItemHeight() { + // if (_itemCardHeight == 0) { + // // _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; + // } + // } - bool _handleReorderItems(int oldIndex, int newIndex, Object? slot) { - if (newIndex >= widget.items.length || selectedSortIndex != 0) return false; + bool _handleReorderItems(int oldIndex, int newIndex) { + if (newIndex >= widget.items.length || _selectedSortIndex != 0) { + return false; + } widget.onReorderItem?.call(widget.items[oldIndex]); widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); - updateCurrentList(); + _updateCurrentList(); widget.onModifyList?.call(); return true; @@ -160,78 +155,34 @@ class _CustomListViewState void _handleChangeItems( ItemChangerCallback callback, bool callOnModifyList) { - final initialList = List.from(currentList); - callback(widget.items); setState(() { - updateCurrentList(); + _updateCurrentList(); }); - final deletedItems = List.from(initialList - .where( - (element) => currentList.where((e) => e.id == element.id).isEmpty) - .toList()); - final addedItems = List.from(currentList - .where( - (element) => initialList.where((e) => e.id == element.id).isEmpty) - .toList()); - - for (var deletedItem in deletedItems) { - _controller.notifyRemovedRange( - initialList.indexWhere((element) => element.id == deletedItem.id), - 1, - _getChangeWidgetBuilder(deletedItem), - ); - } - - for (var addedItem in addedItems) { - _controller.notifyInsertedRange( - currentList.indexWhere((element) => element.id == addedItem.id), - 1, - ); - } - - _notifyChangeList(); - if (callOnModifyList) widget.onModifyList?.call(); } Future _handleDeleteItem(Item deletedItem, [bool callOnModifyList = true]) async { - int index = _getItemIndex(deletedItem); - - // print(listToString(widget.items)); - + widget.items.removeWhere((element) => element.id == deletedItem.id); setState(() { - widget.items.removeWhere((element) => element.id == deletedItem.id); - updateCurrentList(); + _updateCurrentList(); }); - _controller.notifyRemovedRange( - index, - 1, - _getChangeWidgetBuilder(deletedItem), - ); await widget.onDeleteItem?.call(deletedItem); if (callOnModifyList) widget.onModifyList?.call(); } Future _handleDeleteItemList(List deletedItems) async { for (var item in deletedItems) { - int index = _getItemIndex(item); - - setState(() { - widget.items.removeWhere((element) => element.id == item.id); - updateCurrentList(); - }); - - _controller.notifyRemovedRange( - index, - 1, - _getChangeWidgetBuilder(deletedItems.first), - ); + widget.items.removeWhere((element) => element.id == item.id); } + setState(() { + _updateCurrentList(); + }); + for (var item in deletedItems) { await widget.onDeleteItem?.call(item); } @@ -239,8 +190,8 @@ class _CustomListViewState widget.onModifyList?.call(); } - void _handleClear() { - _handleDeleteItemList(List.from(widget.items)); + void _handleClearItems() async { + await _handleDeleteItemList(List.from(widget.items)); } Future _handleAddItem(Item item, {int index = -1}) async { @@ -250,17 +201,12 @@ class _CustomListViewState widget.items.insert(index, item); await widget.onAddItem?.call(item); setState(() { - updateCurrentList(); + _updateCurrentList(); }); int currentListIndex = _getItemIndex(item); - _controller.notifyInsertedRange(currentListIndex, 1); - // _scrollToIndex(index); - // TODO: Remove this delay - Future.delayed(const Duration(milliseconds: 100), () { - _scrollToIndex(currentListIndex); - }); - _updateItemHeight(); + _scrollToIndex(currentListIndex); + // _updateItemHeight(); widget.onModifyList?.call(); } @@ -269,151 +215,154 @@ class _CustomListViewState } void _scrollToIndex(int index) { - // if (_scrollController.offset == 0) { - // _scrollController.jumpTo(1); - // } - if (_itemCardHeight == 0 && index != 0) return; - _scrollController.animateTo(index * _itemCardHeight, + if (index != 0) return; + _scrollController.animateTo(index.toDouble(), duration: const Duration(milliseconds: 250), curve: Curves.easeIn); } - _getItemBuilder() { - return (BuildContext context, Item item, data) { - for (var filter in widget.listFilters) { - // print("${filter.displayName} ${filter.filterFunction}"); - if (!filter.filterFunction(item)) { - return Container(); - } + void _endSelection() { + setState(() { + _isSelecting = false; + // _isReordering = false; + _selectedIds.clear(); + }); + } + + void _startSelection(Item item) { + setState(() { + _isSelecting = true; + _selectedIds = [item.id]; + }); + } + + void _handleSelect(Item item) { + setState(() { + if (_selectedIds.contains(item.id)) { + _selectedIds.remove(item.id); + } else { + _selectedIds.add(item.id); } - return data.measuring - ? SizedBox(height: _itemCardHeight) - : ListItemCard( - key: ValueKey(item), - onTap: () { - return widget.onTapItem?.call(item, _getItemIndex(item)); - }, - onDelete: - widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null, - onDuplicate: () => _handleDuplicateItem(item), - isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, - isDuplicateEnabled: widget.isDuplicateEnabled, - child: widget.itemBuilder(item), - ); - }; + }); + if (_selectedIds.isEmpty) { + _endSelection(); + } } - void onFilterChange() { + void _handleSortChange(int index) { setState(() { - _notifyChangeList(); + _selectedSortIndex = index; + widget.onChangeSortIndex?.call(index); + _updateCurrentList(); }); } - List getCurrentList() { - final List items = List.from(widget.items); + void _handleFilterChange() { + setState(() {}); + } - if (selectedSortIndex != 0) { - items.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction); - } + void _handleSelectAll() { + setState(() { + _selectedIds = widget.items.map((e) => e.id).toList(); + }); + } + + void _handleCustomAction(ListFilterCustomAction action) { + final items = _getActionableItems(); + action.action(items); + _endSelection(); + } + + void _handleDeleteAction() async { + Navigator.pop(context); + final result = await showDeleteAlertDialogue(context); + if (result == null || result == false) return; - return items; + final list = _getActionableItems(); + final itemsToRemove = + List.from(list.where((item) => item.isDeletable)); + _endSelection(); + await _handleDeleteItemList(itemsToRemove); + } + + List _getActionableItems() { + return _isSelecting + ? widget.items.where((item) => _selectedIds.contains(item.id)).toList() + : widget.items + .where((item) => widget.listFilters + .every((filter) => filter.filterFunction(item))) + .toList(); + } + + _getItemBuilder() { + return (BuildContext context, int index) { + Item item = currentList[index]; + for (var filter in widget.listFilters) { + if (!filter.filterFunction(item)) { + return Container(key: ValueKey(item)); + } + } + Widget itemWidget = ListItemCard( + key: ValueKey(item.id), + onTap: () { + if (_isSelecting) { + _handleSelect(item); + } else { + return widget.onTapItem?.call(item, index); + } + }, + onLongPress: () { + if (widget.isSelectable && + _longPressActionSetting.value == LongPressAction.multiSelect) { + if (!_isSelecting) { + _startSelection(item); + } else { + _handleSelect(item); + } + } + }, + onDelete: widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null, + onDuplicate: () => _handleDuplicateItem(item), + isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, + isDuplicateEnabled: widget.isDuplicateEnabled, + isSelected: _selectedIds.contains(item.id), + showReorderHandle: + _isSelecting && widget.isReorderable && _selectedSortIndex == 0, + index: index, + child: widget.itemBuilder(item), + ); + return itemWidget; + }; } @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; - if (selectedSortIndex > widget.sortOptions.length) { - updateCurrentList(); + if (_selectedSortIndex > widget.sortOptions.length) { + _updateCurrentList(); } - List getFilterChips() { - List widgets = []; - int activeFilterCount = - widget.listFilters.where((filter) => filter.isActive).length; - if (activeFilterCount > 0) { - widgets.add(ListFilterActionChip( - actions: [ - ListFilterAction( - name: AppLocalizations.of(context)!.clearFiltersAction, - icon: Icons.clear_rounded, - action: () { - for (var filter in widget.listFilters) { - filter.reset(); - } - onFilterChange(); - }, - ), - ...widget.customActions.map((action) => ListFilterAction( - name: action.name, - icon: action.icon, - action: () => action.action(widget.items - .where((item) => widget.listFilters - .every((filter) => filter.filterFunction(item))) - .toList()), - )), - ListFilterAction( - name: AppLocalizations.of(context)!.deleteAllFilteredAction, - icon: Icons.delete_rounded, - color: colorScheme.error, - action: () async { - Navigator.pop(context); - final result = await showDeleteAlertDialogue(context); - if (result == null || result == false) return; - - final toRemove = List.from(widget.items.where((item) => - widget.listFilters - .every((filter) => filter.filterFunction(item)))); - await _handleDeleteItemList(toRemove); - - widget.onModifyList?.call(); - }, - ) - ], - activeFilterCount: activeFilterCount, - )); - } - widgets.addAll(widget.listFilters - .map((filter) => getListFilterChip(filter, onFilterChange))); - if (widget.sortOptions.isNotEmpty) { - widgets.add( - ListSortChip( - selectedIndex: selectedSortIndex, - sortOptions: [ - ListSortOption( - (context) => AppLocalizations.of(context)!.defaultLabel, - (a, b) => 0), - ...widget.sortOptions, - ], - onChange: (index) => setState(() { - selectedSortIndex = index; - widget.onChangeSortIndex?.call(index); - updateCurrentList(); - _notifyChangeList(); - }), - ), - ); - } - return widgets; - } - - // timeDilation = 1; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: getFilterChips(), - ), - ), - ), + ListFilterBar( + listFilters: widget.listFilters, + customActions: widget.customActions, + sortOptions: widget.sortOptions, + isSelecting: _isSelecting, + handleCustomAction: _handleCustomAction, + handleEndSelection: _endSelection, + handleDeleteAction: _handleDeleteAction, + handleSelectAll: _handleSelectAll, + selectedIds: _selectedIds, + handleFilterChange: _handleFilterChange, + selectedSortIndex: _selectedSortIndex, + handleSortChange: _handleSortChange, + isDeleteEnabled: widget.isDeleteEnabled, ), + if (widget.header != null) widget.header!, Expanded( flex: 1, child: Stack(children: [ @@ -424,47 +373,33 @@ class _CustomListViewState child: Center( child: Text( widget.placeholderText, - style: - Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), + style: textTheme.displaySmall?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + ), ), ), ) : Container(), SlidableAutoCloseBehavior( - child: AutomaticAnimatedListView( - list: currentList, + child: AnimatedReorderableListView( + longPressDraggable: false, + buildDefaultDragHandles: false, + proxyDecorator: (widget, index, animation) => + reorderableListDecorator(context, widget), + items: currentList, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - comparator: AnimatedListDiffListComparator( - sameItem: (a, b) => a.id == b.id, - sameContent: (a, b) => a.id == b.id, - ), + isSameItem: (a, b) => a.id == b.id, + scrollDirection: Axis.vertical, itemBuilder: _getItemBuilder(), - // animator: DefaultAnimatedListAnimator, - listController: _controller, - scrollController: _scrollController, - addLongPressReorderable: widget.isReorderable, - reorderModel: widget.isReorderable && selectedSortIndex == 0 - ? AnimatedListReorderModel( - onReorderStart: (index, dx, dy) => true, - onReorderFeedback: (int index, int dropIndex, - double offset, double dx, double dy) => - null, - onReorderMove: (int index, int dropIndex) => true, - onReorderComplete: _handleReorderItems, - ) - : null, - reorderDecorationBuilder: - widget.isReorderable ? reorderableListDecorator : null, - footer: const SizedBox(height: 64 + 80), - // cacheExtent: double.infinity, + enterTransition: [FadeIn()], + exitTransition: [FadeIn()], + controller: _scrollController, + insertDuration: const Duration(milliseconds: 300), + removeDuration: const Duration(milliseconds: 300), + onReorder: _handleReorderItems, ), - ), + ) ]), ), ], diff --git a/lib/common/widgets/list/list_filter_bar.dart b/lib/common/widgets/list/list_filter_bar.dart new file mode 100644 index 00000000..9f1a9ded --- /dev/null +++ b/lib/common/widgets/list/list_filter_bar.dart @@ -0,0 +1,139 @@ +import 'package:clock_app/common/logic/get_list_filter_chips.dart'; +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ListFilterBar extends StatelessWidget { + const ListFilterBar( + {super.key, + required this.listFilters, + required this.customActions, + required this.sortOptions, + required this.isSelecting, + required this.handleCustomAction, + required this.handleEndSelection, + required this.handleDeleteAction, + required this.handleSelectAll, + required this.selectedIds, + required this.handleFilterChange, + required this.selectedSortIndex, + required this.handleSortChange, + required this.isDeleteEnabled}); + + final List> listFilters; + final List> customActions; + final List> sortOptions; + final bool isSelecting; + final bool isDeleteEnabled; + final Function(ListFilterCustomAction) handleCustomAction; + final Function handleEndSelection; + final void Function() handleFilterChange; + final Function handleSelectAll; + final List selectedIds; + final int selectedSortIndex; + final void Function() handleDeleteAction; + final void Function(int) handleSortChange; + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + List getFilterChips() { + List widgets = []; + int activeFilterCount = + listFilters.where((filter) => filter.isActive).length; + if (activeFilterCount > 0 || isSelecting) { + widgets.add( + ListFilterActionChip( + actions: [ + ListFilterAction( + name: AppLocalizations.of(context)!.clearFiltersAction, + icon: Icons.clear_rounded, + action: () { + for (var filter in listFilters) { + filter.reset(); + } + handleEndSelection(); + }, + ), + ...customActions.map( + (action) => ListFilterAction( + name: action.name, + icon: action.icon, + action: () => handleCustomAction(action), + ), + ), + if (isDeleteEnabled) + ListFilterAction( + name: AppLocalizations.of(context)!.deleteAllFilteredAction, + icon: Icons.delete_rounded, + color: colorScheme.error, + action: handleDeleteAction, + ) + ], + activeFilterCount: activeFilterCount + (isSelecting ? 1 : 0), + ), + ); + } + + if (isSelecting) { + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)! + .selectionStatus(selectedIds.length), + icon: Icons.clear_rounded, + onTap: () => handleEndSelection(), + ), + ); + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)!.selectAll, + icon: Icons.select_all_rounded, + onTap: () => handleSelectAll(), + ), + ); + } + widgets.addAll(listFilters + .map((filter) => getListFilterChip(filter, handleFilterChange))); + if (sortOptions.isNotEmpty) { + widgets.add( + ListSortChip( + selectedIndex: selectedSortIndex, + sortOptions: [ + ListSortOption( + (context) => AppLocalizations.of(context)!.defaultLabel, + (a, b) => 0), + ...sortOptions, + ], + onChange: handleSortChange, + ), + ); + } + return widgets; + } + + return Expanded( + flex: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: AnimatedContainer( + duration: 150.ms, + height: getFilterChips().isEmpty ? 0 : 40, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: getFilterChips(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 38bb2890..73b19f64 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -2,9 +2,11 @@ import 'package:clock_app/common/logic/show_select.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/types/select_choice.dart'; +import 'package:clock_app/common/widgets/animated_show_hide.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/list/action_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ListFilterChip extends StatelessWidget { @@ -12,10 +14,13 @@ class ListFilterChip extends StatelessWidget { super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, + }); final ListFilter listFilter; final VoidCallback onChange; + final bool isEnabled; @override Widget build(BuildContext context) { @@ -23,20 +28,24 @@ class ListFilterChip extends StatelessWidget { ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - return CardContainer( - color: listFilter.isSelected ? colorScheme.primary : null, - onTap: () { - listFilter.isSelected = !listFilter.isSelected; - onChange(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Text( - listFilter.displayName(context), - style: textTheme.headlineSmall?.copyWith( - color: listFilter.isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface, + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: listFilter.isSelected ? colorScheme.primary : null, + onTap: () { + listFilter.isSelected = !listFilter.isSelected; + onChange(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + listFilter.displayName(context), + style: textTheme.headlineSmall?.copyWith( + color: listFilter.isSelected + ? colorScheme.onPrimary + : colorScheme.onSurface, + ), ), ), ), @@ -44,15 +53,79 @@ class ListFilterChip extends StatelessWidget { } } +class ListButtonChip extends StatelessWidget { + const ListButtonChip({ + super.key, + required this.label, + this.onTap, + required this.icon, + this.isActive = false, + this.isEnabled = true, + + }); + + final String? label; + final IconData? icon; + final Function()? onTap; + final bool isActive; + final bool isEnabled; + + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + onTap: onTap, + color: isActive ? colorScheme.primary : null, + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only( + left: 10.0, right: 6.0, top: 6.0, bottom: 6.0), + child: Icon( + icon, + color: + isActive ? colorScheme.onPrimary : colorScheme.onSurface, + size: 20, + ), + ), + if (label != null) + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + label!, + style: textTheme.headlineSmall?.copyWith( + color: isActive + ? colorScheme.onPrimary + : colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ); + } +} + class ListFilterActionChip extends StatelessWidget { const ListFilterActionChip({ super.key, required this.actions, required this.activeFilterCount, + this.isEnabled = true, }); final List actions; final int activeFilterCount; + final bool isEnabled; void _showPopupMenu(BuildContext context) async { await showModalBottomSheet>( @@ -75,34 +148,38 @@ class ListFilterActionChip extends StatelessWidget { ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - return CardContainer( - color: colorScheme.primary, - onTap: () { - _showPopupMenu(context); - // listFilter.isSelected = !listFilter.isSelected; - // onChange(); - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 8.0, right: 6.0, top: 6.0, bottom: 6.0), - child: Icon( - Icons.filter_list_rounded, - color: colorScheme.onPrimary, - size: 20, + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: colorScheme.primary, + onTap: () { + _showPopupMenu(context); + // listFilter.isSelected = !listFilter.isSelected; + // onChange(); + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 8.0, right: 6.0, top: 6.0, bottom: 6.0), + child: Icon( + Icons.filter_list_rounded, + color: colorScheme.onPrimary, + size: 20, + ), ), - ), - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Text( - activeFilterCount.toString(), - style: textTheme.headlineSmall?.copyWith( - color: colorScheme.onPrimary.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + activeFilterCount.toString(), + style: textTheme.headlineSmall?.copyWith( + color: colorScheme.onPrimary.withOpacity(0.6), + ), ), ), - ), - ], + ], + ), ), ); } @@ -111,11 +188,15 @@ class ListFilterActionChip extends StatelessWidget { class ListFilterSelectChip extends StatelessWidget { final FilterSelect listFilter; final VoidCallback onChange; + final bool isEnabled; + const ListFilterSelectChip({ super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, + }); @override @@ -141,34 +222,38 @@ class ListFilterSelectChip extends StatelessWidget { multiSelect: false); } - return CardContainer( - color: isFirstSelected ? null : colorScheme.primary, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - isFirstSelected - ? listFilter.displayName(context) - : listFilter.selectedFilter.displayName(context), - style: textTheme.headlineSmall?.copyWith( - color: isFirstSelected - ? colorScheme.onSurface - : colorScheme.onPrimary), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: isFirstSelected ? null : colorScheme.primary, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + isFirstSelected + ? listFilter.displayName(context) + : listFilter.selectedFilter.displayName(context), + style: textTheme.headlineSmall?.copyWith( + color: isFirstSelected + ? colorScheme.onSurface + : colorScheme.onPrimary), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon( - Icons.keyboard_arrow_down_rounded, - color: isFirstSelected - ? colorScheme.onSurface.withOpacity(0.6) - : colorScheme.onPrimary.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon( + Icons.keyboard_arrow_down_rounded, + color: isFirstSelected + ? colorScheme.onSurface.withOpacity(0.6) + : colorScheme.onPrimary.withOpacity(0.6), + ), ), - ), - ], + ], + ), ), ); } @@ -177,11 +262,14 @@ class ListFilterSelectChip extends StatelessWidget { class ListFilterMultiSelectChip extends StatelessWidget { final FilterMultiSelect listFilter; final VoidCallback onChange; + final bool isEnabled; + const ListFilterMultiSelectChip({ super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, }); @override @@ -208,36 +296,40 @@ class ListFilterMultiSelectChip extends StatelessWidget { multiSelect: true); } - return CardContainer( - color: isSelected ? colorScheme.primary : null, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - !isSelected - ? listFilter.displayName(context) - : listFilter.selectedIndices.length == 1 - ? listFilter.selectedFilters[0].displayName(context) - : "${listFilter.selectedIndices.length} selected", - style: textTheme.headlineSmall?.copyWith( - color: isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: isSelected ? colorScheme.primary : null, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + !isSelected + ? listFilter.displayName(context) + : listFilter.selectedIndices.length == 1 + ? listFilter.selectedFilters[0].displayName(context) + : "${listFilter.selectedIndices.length} selected", + style: textTheme.headlineSmall?.copyWith( + color: isSelected + ? colorScheme.onPrimary + : colorScheme.onSurface), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon( - Icons.keyboard_arrow_down_rounded, - color: isSelected - ? colorScheme.onPrimary.withOpacity(0.6) - : colorScheme.onSurface.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon( + Icons.keyboard_arrow_down_rounded, + color: isSelected + ? colorScheme.onPrimary.withOpacity(0.6) + : colorScheme.onSurface.withOpacity(0.6), + ), ), - ), - ], + ], + ), ), ); } @@ -247,12 +339,15 @@ class ListSortChip extends StatelessWidget { final List sortOptions; final Function(int) onChange; final int selectedIndex; + final bool isEnabled; const ListSortChip({ super.key, required this.sortOptions, required this.onChange, required this.selectedIndex, + this.isEnabled = true, + }); @override @@ -276,26 +371,30 @@ class ListSortChip extends StatelessWidget { multiSelect: false); } - return CardContainer( - // color: isFirstSelected ? null : colorScheme.primary, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}", - style: textTheme.headlineSmall - ?.copyWith(color: colorScheme.onSurface), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + // color: isFirstSelected ? null : colorScheme.primary, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}", + style: textTheme.headlineSmall + ?.copyWith(color: colorScheme.onSurface), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon(Icons.keyboard_arrow_down_rounded, - color: colorScheme.onSurface.withOpacity(0.6)), - ), - ], + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon(Icons.keyboard_arrow_down_rounded, + color: colorScheme.onSurface.withOpacity(0.6)), + ), + ], + ), ), ); } diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index 69a87597..eedab955 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -1,9 +1,12 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/action_pane.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/drag_listener.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; class ListItemCard extends StatefulWidget { @@ -16,15 +19,23 @@ class ListItemCard extends StatefulWidget { this.onInit, this.isDeleteEnabled = true, this.isDuplicateEnabled = true, + this.isSelected = false, + this.showReorderHandle = false, + required this.index, + this.onLongPress, }); final VoidCallback? onDelete; final VoidCallback? onDuplicate; final VoidCallback? onTap; + final VoidCallback? onLongPress; final Widget child; final VoidCallback? onInit; final bool isDeleteEnabled; final bool isDuplicateEnabled; + final bool isSelected; + final bool showReorderHandle; + final int index; @override State createState() => _ListItemCardState(); @@ -32,6 +43,7 @@ class ListItemCard extends StatefulWidget { class _ListItemCardState extends State> { late Setting swipeActionSetting; + late Setting longPressActionSetting; void update(dynamic value) { setState(() {}); @@ -41,8 +53,11 @@ class _ListItemCardState extends State> { void initState() { super.initState(); widget.onInit?.call(); - swipeActionSetting = - appSettings.getGroup("General").getSetting("Swipe Action"); + final interactionSettingsGroup = + appSettings.getGroup("General").getGroup("Interactions"); + swipeActionSetting = interactionSettingsGroup.getSetting("Swipe Action"); + longPressActionSetting = + interactionSettingsGroup.getSetting("Long Press Action"); swipeActionSetting.addListener(update); } @@ -55,8 +70,11 @@ class _ListItemCardState extends State> { @override Widget build(BuildContext context) { Widget innerWidget = widget.child; + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; - if ((widget.isDeleteEnabled || widget.isDuplicateEnabled) && swipeActionSetting.value == SwipeAction.cardActions) { + if ((widget.isDeleteEnabled || widget.isDuplicateEnabled) && + swipeActionSetting.value == SwipeAction.cardActions) { ActionPane startActionPane = widget.isDuplicateEnabled ? getDuplicateActionPane(widget.onDuplicate ?? () {}, context) : getDeleteActionPane(widget.onDelete ?? () {}, context); @@ -64,6 +82,7 @@ class _ListItemCardState extends State> { ? getDeleteActionPane(widget.onDelete ?? () {}, context) : getDuplicateActionPane(widget.onDuplicate ?? () {}, context); innerWidget = Slidable( + enabled: !widget.showReorderHandle, groupTag: 'list', key: widget.key, startActionPane: startActionPane, @@ -76,7 +95,34 @@ class _ListItemCardState extends State> { width: double.infinity, child: CardContainer( onTap: widget.onTap, - child: innerWidget, + onLongPress: widget.onLongPress, + isSelected: widget.isSelected, + child: Row( + children: [ + AnimatedContainer( + duration: 150.ms, + width: widget.showReorderHandle ? 28 : 0, + color: Colors.transparent, + // decoration: const BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: ReorderableGridDragStartListener( + + key: widget.key, + index: widget.index, + enabled: true, + child: Padding( + padding: + const EdgeInsets.only(left: 8.0, top: 16.0, bottom: 16.0), + child: Icon( + Icons.drag_indicator, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + ), + Expanded(child: innerWidget), + ], + ), ), ); } diff --git a/lib/common/widgets/list/list_item_measurer.dart b/lib/common/widgets/list/list_item_measurer.dart index beb6033b..9c3f0eda 100644 --- a/lib/common/widgets/list/list_item_measurer.dart +++ b/lib/common/widgets/list/list_item_measurer.dart @@ -1,31 +1,30 @@ -import 'package:flutter/material.dart'; -import 'package:great_list_view/great_list_view.dart'; +// import 'package:flutter/material.dart'; -class ListItemMeasurer extends StatefulWidget { - const ListItemMeasurer({ - super.key, - required this.controller, - required this.index, - }); - - final AnimatedListController controller; - final int index; - - @override - State createState() => _ListItemMeasurerState(); -} - -class _ListItemMeasurerState extends State { - late double? height; - - @override - void initState() { - super.initState(); - height = widget.controller.computeItemBox(0, true)?.height; - } - - @override - Widget build(BuildContext context) { - return SizedBox(height: height); - } -} +// class ListItemMeasurer extends StatefulWidget { +// const ListItemMeasurer({ +// super.key, +// required this.controller, +// required this.index, +// }); +// +// final AnimatedListController controller; +// final int index; +// +// @override +// State createState() => _ListItemMeasurerState(); +// } +// +// class _ListItemMeasurerState extends State { +// late double? height; +// +// @override +// void initState() { +// super.initState(); +// height = widget.controller.computeItemBox(0, true)?.height; +// } +// +// @override +// Widget build(BuildContext context) { +// return SizedBox(height: height); +// } +// } diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index ac453fc8..09636b20 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -3,6 +3,7 @@ import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:flutter/material.dart'; @@ -70,11 +71,14 @@ class PersistentListView extends StatefulWidget { this.isReorderable = true, this.isDeleteEnabled = true, this.isDuplicateEnabled = true, + this.isSelectable = false, this.reloadOnPop = false, this.shouldInsertOnTop = true, this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], + this.header, + this.onSaveItems , // this.initialSortIndex = 0, }); @@ -90,11 +94,14 @@ class PersistentListView extends StatefulWidget { final bool isDeleteEnabled; final bool isDuplicateEnabled; final bool reloadOnPop; + final bool isSelectable; final bool shouldInsertOnTop; + final Widget? header; // final int initialSortIndex; final List> listFilters; final List> customActions; final List> sortOptions; + final Function(List items)? onSaveItems; @override State createState() => _PersistentListViewState(); @@ -113,7 +120,6 @@ class _PersistentListViewState if (widget.saveTag.isNotEmpty) { _items = loadListSync(widget.saveTag); } - // watchList(widget.saveTag, (event) => reloadItems()); ListenerManager.addOnChangeListener(widget.saveTag, _loadItems); if (widget.sortOptions.isNotEmpty) { @@ -128,16 +134,12 @@ class _PersistentListViewState else { _initialSortIndex = 0; } - // ListenerManager.addOnChangeListener( - // "${widget.saveTag}-reload", reloadItems); } @override void dispose() { ListenerManager.removeOnChangeListener(widget.saveTag, _loadItems); - // ListenerManager.removeOnChangeListener( - // "${widget.saveTag}-reload", reloadItems); - // unwatchList(widget.saveTag); + super.dispose(); } @@ -159,10 +161,12 @@ class _PersistentListViewState } } - void _saveItems() { + void _saveItems () async { if (widget.saveTag.isNotEmpty) { - saveList(widget.saveTag, _items); + await saveList(widget.saveTag, _items); } + widget.onSaveItems?.call(_items); + } void _handleChangeSort(int index) { @@ -184,6 +188,7 @@ class _PersistentListViewState onModifyList: _saveItems, isReorderable: widget.isReorderable, isDeleteEnabled: widget.isDeleteEnabled, + isSelectable: widget.isSelectable, isDuplicateEnabled: widget.isDuplicateEnabled, shouldInsertOnTop: widget.shouldInsertOnTop, listFilters: widget.listFilters, @@ -191,6 +196,7 @@ class _PersistentListViewState sortOptions: widget.sortOptions, initialSortIndex: _initialSortIndex, onChangeSortIndex: _handleChangeSort, + header: widget.header, ); } } diff --git a/lib/common/widgets/list/static_list_view.dart b/lib/common/widgets/list/static_list_view.dart new file mode 100644 index 00000000..829c21cb --- /dev/null +++ b/lib/common/widgets/list/static_list_view.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class StaticListView extends StatelessWidget { + const StaticListView({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + ...children, + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/measure_size.dart b/lib/common/widgets/measure_size.dart index 0cc621aa..37d962a8 100644 --- a/lib/common/widgets/measure_size.dart +++ b/lib/common/widgets/measure_size.dart @@ -27,10 +27,10 @@ class MeasureSize extends SingleChildRenderObjectWidget { final OnWidgetSizeChange onChange; const MeasureSize({ - Key? key, + super.key, required this.onChange, - required Widget child, - }) : super(key: key, child: child); + required Widget super.child, + }); @override RenderObject createRenderObject(BuildContext context) { diff --git a/lib/common/widgets/time_picker.dart b/lib/common/widgets/time_picker.dart index c86a596a..8f651041 100644 --- a/lib/common/widgets/time_picker.dart +++ b/lib/common/widgets/time_picker.dart @@ -2820,8 +2820,15 @@ class _TimePickerDialogState extends State TextTheme textTheme = theme.textTheme; ColorScheme colorScheme = theme.colorScheme; - bool use24hMode = MediaQuery.of(context).alwaysUse24HourFormat || - appSettings.getSetting("Time Format").value == TimeFormat.h24; + TimeFormat timeFormat = appSettings.getSetting("Time Format").value; + + bool use24hMode = false; + if (timeFormat == TimeFormat.device) { + use24hMode = MediaQuery.of(context).alwaysUse24HourFormat; + } else { + use24hMode = + appSettings.getSetting("Time Format").value == TimeFormat.h24; + } switch (type) { case TimePickerType.spinner: diff --git a/lib/settings/data/developer_settings_schema.dart b/lib/developer/data/developer_settings_schema.dart similarity index 64% rename from lib/settings/data/developer_settings_schema.dart rename to lib/developer/data/developer_settings_schema.dart index e32d7770..6fb7e26c 100644 --- a/lib/settings/data/developer_settings_schema.dart +++ b/lib/developer/data/developer_settings_schema.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:clock_app/alarm/screens/alarm_events_screen.dart'; +import 'package:clock_app/developer/screens/logs_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; @@ -10,9 +13,8 @@ SettingGroup developerSettingsSchema = SettingGroup( "Developer Options", (context) => AppLocalizations.of(context)!.developerOptionsSettingGroup, [ - SettingGroup("Alarm", - (context) => AppLocalizations.of(context)!.alarmTitle, - [ + SettingGroup( + "Alarm", (context) => AppLocalizations.of(context)!.alarmTitle, [ SwitchSetting( "Show Instant Alarm Button", (context) => AppLocalizations.of(context)!.showIstantAlarmButtonSetting, @@ -21,9 +23,8 @@ SettingGroup developerSettingsSchema = SettingGroup( // "Show a button on the alarm screen that creates an alarm that rings one second in the future", ), ]), - SettingGroup("Logs", - (context) => AppLocalizations.of(context)!.logsSettingGroup, - [ + SettingGroup( + "Logs", (context) => AppLocalizations.of(context)!.logsSettingGroup, [ SliderSetting( "Max logs", (context) => AppLocalizations.of(context)!.maxLogsSetting, @@ -32,10 +33,16 @@ SettingGroup developerSettingsSchema = SettingGroup( 100, snapLength: 1, ), - SettingPageLink("Alarm Logs", - (context) => AppLocalizations.of(context)!.alarmLogSetting, - const AlarmEventsScreen()), - ]), + SettingPageLink( + "alarm_logs", + (context) => AppLocalizations.of(context)!.alarmLogSetting, + const AlarmEventsScreen()), + SettingPageLink( + "app_logs", + (context) => AppLocalizations.of(context)!.appLogs, + const LogsScreen()), + + ]), ], icon: Icons.code_rounded, ); diff --git a/lib/developer/data/log_list_filters.dart b/lib/developer/data/log_list_filters.dart new file mode 100644 index 00000000..0da6006d --- /dev/null +++ b/lib/developer/data/log_list_filters.dart @@ -0,0 +1,24 @@ +import 'package:clock_app/alarm/types/alarm_event.dart'; +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/utils/date_time.dart'; +import 'package:clock_app/developer/types/log.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logger/logger.dart'; + +final List> logListFilters = [ + ListFilterSelect((context) => AppLocalizations.of(context)!.dateFilterGroup, [ + ListFilter((context) => AppLocalizations.of(context)!.todayFilter, + (log) => log.dateTime.isToday()), + ListFilter((context) => AppLocalizations.of(context)!.tomorrowFilter, + (log) => log.dateTime.isTomorrow()), + ]), + ListFilterMultiSelect( + (context) => AppLocalizations.of(context)!.logTypeFilterGroup, [ + ListFilter((context) => "Debug", (log) => log.level == Level.debug), + ListFilter((context) => "Trace", (log) => log.level == Level.trace), + ListFilter((context) => "Info", (log) => log.level == Level.info), + ListFilter((context) => "Warning", (log) => log.level == Level.warning), + ListFilter((context) => "Error", (log) => log.level == Level.error), + ListFilter((context) => "Fatal", (log) => log.level == Level.fatal), + ]), +]; diff --git a/lib/developer/data/log_sort_options.dart b/lib/developer/data/log_sort_options.dart new file mode 100644 index 00000000..f398cadb --- /dev/null +++ b/lib/developer/data/log_sort_options.dart @@ -0,0 +1,17 @@ +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/developer/types/log.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +final List> logSortOptions = [ + ListSortOption((context) => "Earlies first", sortDateAscending), + ListSortOption((context) => "Latest first", sortDateDescending), +]; + +int sortDateAscending(Log a, Log b) { + return a.id.compareTo(b.id); +} + +int sortDateDescending(Log a, Log b) { + return b.id.compareTo(a.id); +} + diff --git a/lib/developer/logic/logger.dart b/lib/developer/logic/logger.dart new file mode 100644 index 00000000..e8a68b37 --- /dev/null +++ b/lib/developer/logic/logger.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/developer/types/file_logger_output.dart'; +import 'package:clock_app/developer/types/log_filter.dart'; +import 'package:logger/logger.dart'; +import 'dart:isolate'; + +var logger = Logger( + filter: FileLogFilter(), + output: FileLoggerOutput(), + printer: PrettyPrinter( + methodCount: 100, // Number of method calls to be displayed + errorMethodCount: 100, // Number of method calls if stacktrace is provided + lineLength: 80, // Width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + // Should each log print contain a timestamp + dateTimeFormat: DateTimeFormat.none, + ), +); + +void printIsolateInfo() { + logger.t( + "Isolate: ${Isolate.current.debugName}, id: ${Isolate.current.hashCode}"); +} diff --git a/lib/developer/screens/logs_screen.dart b/lib/developer/screens/logs_screen.dart new file mode 100644 index 00000000..70437974 --- /dev/null +++ b/lib/developer/screens/logs_screen.dart @@ -0,0 +1,181 @@ +import 'dart:io'; + +import 'package:clock_app/alarm/data/alarm_events_list_filters.dart'; +import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/types/list_controller.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/common/widgets/fab.dart'; +import 'package:clock_app/common/widgets/list/custom_list_view.dart'; +import 'package:clock_app/common/widgets/list/static_list_view.dart'; +import 'package:clock_app/developer/data/log_list_filters.dart'; +import 'package:clock_app/developer/data/log_sort_options.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/developer/types/log.dart'; +import 'package:clock_app/developer/widgets/log_card.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; +import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; + +class LogsScreen extends StatefulWidget { + const LogsScreen({ + super.key, + }); + + @override + State createState() => _LogsScreenState(); +} + +class _LogsScreenState extends State { + List _logs = []; + List _filteredLogs = []; + final _listController = ListController(); + + List _mergeMultilineLogs(List logLines) { + final mergedLogs = []; + final buffer = StringBuffer(); + + for (var line in logLines) { + if (line.startsWith('[')) { + if (buffer.isNotEmpty) { + mergedLogs.add(buffer.toString()); + buffer.clear(); + } + buffer.writeln(line.trim()); + } else { + buffer.writeln(line.trim()); + } + } + + if (buffer.isNotEmpty) { + mergedLogs.add(buffer.toString()); + } + + // remove new lines from the end of the logs + for (var i = 0; i < mergedLogs.length; i++) { + mergedLogs[i] = mergedLogs[i].trim(); + } + + return mergedLogs; + } + + @override + void initState() { + final File file = File(getLogsFilePathSync()); + final content = file.readAsLinesSync(); + final logLines = _mergeMultilineLogs(content); + + for (int i = 0; i < logLines.length; i++) { + _logs.add(Log.fromLine(logLines[i], i)); + } + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + + return Scaffold( + appBar: SearchTopBar( + title: "App Logs", + searchParams: SearchParams( + onSearch: (searchedItems) { + setState(() { + _filteredLogs = searchedItems; + }); + }, + placeholder: "Search logs", + choices: _logs, + searchTermGetter: (log) { + return log.message; + }, + ), + ), + body: Stack( + children: [ + Column( + children: [ + Expanded( + flex: 1, + child: _filteredLogs.isEmpty + ? CustomListView( + items: _logs, + listController: _listController, + itemBuilder: (log) => LogCard( + key: ValueKey(log), + log: log, + ), + isDuplicateEnabled: false, + isReorderable: false, + isDeleteEnabled: false, + placeholderText: "No logs", + listFilters: logListFilters, + sortOptions: logSortOptions, + ) + : StaticListView( + children: _filteredLogs + .map( + (log) => CardContainer( + child: LogCard( + key: ValueKey(log), + log: log, + ), + ), + ) + .toList()), + ), + ], + ), + FAB( + icon: Icons.delete_rounded, + bottomPadding: 8, + onPressed: () async { + final File file = File(await getLogsFilePath()); + + await file.writeAsString(""); + + if (context.mounted) showSnackBar(context, "Logs cleared"); + + _listController.clearItems(); + setState(() {}); + }, + ), + FAB( + index: 1, + icon: Icons.file_download, + bottomPadding: 8, + onPressed: () async { + try { + final File file = File(await getLogsFilePath()); + + if (!(await file.exists())) { + await file.create(recursive: true); + } + final result = await FilePicker.platform.saveFile( + bytes: await file.readAsBytes(), + fileName: + "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", + ); + if (result != null) { + if (context.mounted) { + showSnackBar(context, "Logs saved to device"); + } + } + } catch (e) { + logger.e("Error saving logs file: ${e.toString()}"); + } + }), + ], + ), + ); + } +} diff --git a/lib/developer/types/file_logger_output.dart b/lib/developer/types/file_logger_output.dart new file mode 100644 index 00000000..1fe95ef4 --- /dev/null +++ b/lib/developer/types/file_logger_output.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:clock_app/app.dart'; +import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:logger/logger.dart'; + +class FileLoggerOutput extends LogOutput { + FileLoggerOutput(); + + @override + void output(OutputEvent event) { + for (var line in event.lines) { + // ignore: avoid_print + print(line); + } + + String message = switch (event.origin.message.runtimeType) { + const (String) => event.origin.message as String, + const (Exception) => (event.origin.message as Exception).toString(), + _ => "Unknown error", + }; + + if (!Platform.environment.containsKey('FLUTTER_TEST')) { + _writeLog(message, event.level); + + if (event.level.value >= Level.error.value && + App.navigatorKey.currentContext != null) { + Future(() { + showSnackBar(App.navigatorKey.currentContext!, message, + error: true, navBar: false, fab: false); + }); + } + } + } + + Future _writeLog(String message, Level level) async { + final DateTime currentDate = DateTime.now(); + final String dateString = + "${currentDate.day}-${currentDate.month}-${currentDate.year}"; + + final File file = File(await getLogsFilePath()); + + if (!(await file.exists())) { + await file.create(recursive: true); + } + + file.writeAsStringSync( + "[$dateString | ${currentDate.hour}:${currentDate.minute}:${currentDate.second}] [${level.name}] $message\n", + mode: FileMode.append, + ); + } +} diff --git a/lib/developer/types/log.dart b/lib/developer/types/log.dart new file mode 100644 index 00000000..468aae1a --- /dev/null +++ b/lib/developer/types/log.dart @@ -0,0 +1,68 @@ +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; + +class Log extends ListItem { + @override + final int id; + late String message; + late DateTime dateTime; + late Level level; + + Log( + this.id, + this.message, + this.dateTime, + this.level, + ); + + Log.fromLine(String line, int index) : id = index { + final regex = RegExp( + r'\[(\d+-\d+-\d+)\s\|\s(\d+:\d+:\d+)\]\s\[(\w+)\]\s(.*)', + dotAll: true); + final match = regex.firstMatch(line); + + if (match != null) { + final datePart = match.group(1)!; + final timePart = match.group(2)!; + level = Level.values.byName(match.group(3)!); + message = match.group(4)!; + + final dateTimeStr = '$datePart $timePart'; + final dateFormat = DateFormat('d-M-yyyy HH:mm:ss'); + dateTime = dateFormat.parse(dateTimeStr); + } else { + message = "Cannot read log"; + level = Level.off; + dateTime = DateTime.now(); + throw const FormatException('Invalid log format'); + } + } + + @override + copy() { + return Log(id,message, dateTime, level); + } + + @override + void copyFrom(other) { + message = other.message; + dateTime = other.timestamp; + level = other.level; + } + + @override + bool get isDeletable => false; + + @override + Json? toJson() { + return { + "id": id, + "message": message, + "timestamp": dateTime.toIso8601String(), + "level": level.toString(), + }; + } +} diff --git a/lib/developer/types/log_filter.dart b/lib/developer/types/log_filter.dart new file mode 100644 index 00000000..508f8ed4 --- /dev/null +++ b/lib/developer/types/log_filter.dart @@ -0,0 +1,8 @@ +import 'package:logger/logger.dart'; + +class FileLogFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + return true; + } +} diff --git a/lib/developer/widgets/log_card.dart b/lib/developer/widgets/log_card.dart new file mode 100644 index 00000000..c0b201a8 --- /dev/null +++ b/lib/developer/widgets/log_card.dart @@ -0,0 +1,78 @@ +import 'package:clock_app/developer/types/log.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; + +class LogCard extends StatefulWidget { + const LogCard({ + super.key, + required this.log, + }); + + final Log log; + + @override + State createState() => _LogCardState(); +} + +class LevelColor { + Color backgroundColor; + Color textColor; + + LevelColor(this.backgroundColor, this.textColor); +} + +Map levelColors = { + Level.debug: LevelColor(Colors.brown, Colors.white), + Level.trace: LevelColor(Colors.grey, Colors.white), + Level.info: LevelColor(Colors.blue, Colors.white), + Level.warning: LevelColor(Colors.orange, Colors.white), + Level.error: LevelColor(Colors.red, Colors.white), + Level.fatal: LevelColor(Colors.purple, Colors.white), +}; + +class _LogCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: + const EdgeInsets.only(left: 16.0, right: 16.0, top: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: levelColors[widget.log.level]?.backgroundColor, + ), + child: Text(widget.log.level.name, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: levelColors[widget.log.level]?.textColor, + )), + ), + const SizedBox( + width: 8, + ), + Text( + DateFormat('yyyy-MM-dd | kk:mm:ss') + .format(widget.log.dateTime), + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), + ], + ), + Text( + widget.log.message, + style: Theme.of(context).textTheme.bodyMedium, + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // softWrap: false, + ), + ], + )); + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ff4d5477..341547e6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -91,7 +91,7 @@ "@cancelButton": {}, "customizeButton": "Anpassen", "@customizeButton": {}, - "saveButton": "Speichern Button", + "saveButton": "Speichern", "@saveButton": {}, "labelField": "Bezeichnung", "@labelField": {}, @@ -117,7 +117,7 @@ "@scheduleTypeRange": {}, "settingGroupMore": "Mehr", "@settingGroupMore": {}, - "soundSettingGroup": "Ton und Vibration", + "soundSettingGroup": "Ton", "@soundSettingGroup": {}, "vibrationSetting": "Vibration", "@vibrationSetting": {}, @@ -177,7 +177,7 @@ "@system": {}, "languageSetting": "Sprache", "@languageSetting": {}, - "timeFormat24": "24 Stunden Format", + "timeFormat24": "24 Stunden", "@timeFormat24": {}, "timeFormatDevice": "Geräte Einstellungen", "@timeFormatDevice": {}, @@ -187,7 +187,7 @@ "@pickerInput": {}, "pickerSpinner": "Wählrad", "@pickerSpinner": {}, - "durationPickerSetting": "Dauer Auswahl", + "durationPickerSetting": "Auswahl der Dauer", "@durationPickerSetting": {}, "pickerRings": "Ringe", "@pickerRings": {}, @@ -201,8 +201,516 @@ "@dateFormatSetting": {}, "timeFormatSetting": "Zeit Format", "@timeFormatSetting": {}, - "timeFormat12": "12 Stunden Format", + "timeFormat12": "12 Stunden", "@timeFormat12": {}, - "timePickerSetting": "Uhrzeit Auswähler", - "@timePickerSetting": {} + "timePickerSetting": "Zeitauswahl", + "@timePickerSetting": {}, + "translateDescription": "Helfe die App zu übersetzen", + "@translateDescription": {}, + "batteryOptimizationSetting": "Manuelles Deaktivieren der Akku-Optimierung", + "@batteryOptimizationSetting": {}, + "styleThemeShapeSettingGroup": "Form", + "@styleThemeShapeSettingGroup": {}, + "styleThemeRadiusSetting": "Rundheit der Ecken", + "@styleThemeRadiusSetting": {}, + "styleThemeElevationSetting": "Erhebung", + "@styleThemeElevationSetting": {}, + "errorLabel": "Fehler", + "@errorLabel": {}, + "styleThemeOpacitySetting": "Deckkraft", + "@styleThemeOpacitySetting": {}, + "previewLabel": "Vorschau", + "@previewLabel": {}, + "cardLabel": "Karte", + "@cardLabel": {}, + "accentLabel": "Akzent", + "@accentLabel": {}, + "materialBrightnessSystem": "System", + "@materialBrightnessSystem": {}, + "materialBrightnessDark": "Dunkel", + "@materialBrightnessDark": {}, + "accentColorSetting": "Akzent Farbe", + "@accentColorSetting": {}, + "alarmRangeSetting": "Datumsbereich", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Intervall", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Täglich", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Nach Beendigung löschen", + "@alarmDeleteAfterFinishingSetting": {}, + "audioChannelAlarm": "Alarm", + "@audioChannelAlarm": {}, + "audioChannelRingtone": "Klingelton", + "@audioChannelRingtone": {}, + "mathEasyDifficulty": "Einfach (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Mittel (X × Y)", + "@mathMediumDifficulty": {}, + "saveReminderAlert": "Wollen Sie schliesen, ohne zu speichern?", + "@saveReminderAlert": {}, + "noTagsMessage": "Keine Stichwörter erstellt", + "@noTagsMessage": {}, + "createdDateFilterGroup": "Erstellungsdatum", + "@createdDateFilterGroup": {}, + "todayFilter": "Heute", + "@todayFilter": {}, + "scheduleDateFilterGroup": "Zeitplan Datum", + "@scheduleDateFilterGroup": {}, + "durationAsc": "Kürzeste", + "@durationAsc": {}, + "enableAllFilteredAlarmsAction": "Alle gefilterten Alarme aktivieren", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Alle gefilterten Alarme deaktivieren", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Alle gefilterten Alarme überspringen", + "@skipAllFilteredAlarmsAction": {}, + "openSourceLicensesSetting": "Open Source Lizenzen", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Mitwirkende", + "@contributorsSetting": {}, + "donorsSetting": "Spendende", + "@donorsSetting": {}, + "editButton": "Bearbeiten", + "@editButton": {}, + "elapsedTime": "Verstrichene Zeit", + "@elapsedTime": {}, + "alarmDeleteAfterRingingSetting": "Löschen nach verpassten Weckruf", + "@alarmDeleteAfterRingingSetting": {}, + "noLapsMessage": "Noch keine Runden", + "@noLapsMessage": {}, + "wednesdayFull": "Mittwoch", + "@wednesdayFull": {}, + "mondayLetter": "M", + "@mondayLetter": {}, + "tuesdayLetter": "D", + "@tuesdayLetter": {}, + "thursdayLetter": "D", + "@thursdayLetter": {}, + "fridayLetter": "F", + "@fridayLetter": {}, + "sundayLetter": "S", + "@sundayLetter": {}, + "horizontalAlignmentSetting": "Horizontale Ausrichtung", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Vertikale Ausrichtung", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Oben", + "@alignmentTop": {}, + "alignmentBottom": "Unten", + "@alignmentBottom": {}, + "alignmentJustify": "Begründen", + "@alignmentJustify": {}, + "dateSettingGroup": "Datum", + "@dateSettingGroup": {}, + "timeSettingGroup": "Zeit", + "@timeSettingGroup": {}, + "sizeSetting": "Größe", + "@sizeSetting": {}, + "fontWeightSetting": "Schriftstärke", + "@fontWeightSetting": {}, + "showMeridiemSetting": "AM/PM anzeigen", + "@showMeridiemSetting": {}, + "firstDayOfWeekSetting": "Erster Tag der Woche", + "@firstDayOfWeekSetting": {}, + "separatorSetting": "Trennzeichen", + "@separatorSetting": {}, + "editTagLabel": "Stichwort bearbeiten", + "@editTagLabel": {}, + "tagNamePlaceholder": "Stichwort Name", + "@tagNamePlaceholder": {}, + "timePickerModeButton": "Modus", + "@timePickerModeButton": {}, + "maxSnoozesSetting": "Maximale Betätigung des Slummermoduses", + "@maxSnoozesSetting": {}, + "audioChannelMedia": "Medien", + "@audioChannelMedia": {}, + "timeToFullVolumeSetting": "Zeit bis zur vollen Lautstärke", + "@timeToFullVolumeSetting": {}, + "snoozePreventDeletionSetting": "Löschung verhindern", + "@snoozePreventDeletionSetting": {}, + "noButton": "Nein", + "@noButton": {}, + "noAlarmMessage": "Keine Alarme erstellt", + "@noAlarmMessage": {}, + "noTimerMessage": "Keine Timer erstellt", + "@noTimerMessage": {}, + "noStopwatchMessage": "Keine Stoppuhren erstellt", + "@noStopwatchMessage": {}, + "noTaskMessage": "Keine Aufgaben erstellt", + "@noTaskMessage": {}, + "noLogsMessage": "Keine Alarm Protokolle", + "@noLogsMessage": {}, + "noPresetsMessage": "Keine Voreinstellungen erstellt", + "@noPresetsMessage": {}, + "cancelSkipAlarmButton": "Überspringen abbrechen", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Ablehnen", + "@dismissAlarmButton": {}, + "allFilter": "Alle", + "@allFilter": {}, + "dateFilterGroup": "Datum", + "@dateFilterGroup": {}, + "logTypeFilterGroup": "Typ", + "@logTypeFilterGroup": {}, + "deleteButton": "Löschen", + "@deleteButton": {}, + "duplicateButton": "Duplizieren", + "@duplicateButton": {}, + "skipAlarmButton": "Nächsten Alarm überspringen", + "@skipAlarmButton": {}, + "tomorrowFilter": "Morgen", + "@tomorrowFilter": {}, + "stateFilterGroup": "Zustand", + "@stateFilterGroup": {}, + "activeFilter": "Aktiv", + "@activeFilter": {}, + "inactiveFilter": "Inaktiv", + "@inactiveFilter": {}, + "runningTimerFilter": "läuft", + "@runningTimerFilter": {}, + "pausedTimerFilter": "pausiert", + "@pausedTimerFilter": {}, + "sortGroup": "Sortieren", + "@sortGroup": {}, + "stoppedTimerFilter": "Gestoppt", + "@stoppedTimerFilter": {}, + "defaultLabel": "Standard", + "@defaultLabel": {}, + "filterActions": "Filter Aktionen", + "@filterActions": {}, + "clearFiltersAction": "Alle Filter löschen", + "@clearFiltersAction": {}, + "durationDesc": "Längste", + "@durationDesc": {}, + "nameAsc": "Bezeichnung A-Z", + "@nameAsc": {}, + "nameDesc": "Bezeichnung Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Die frühen Stunden zuerst", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Die späten Stunden zuerst", + "@timeOfDayDesc": {}, + "cancelSkipAllFilteredAlarmsAction": "Abbrechen der Überspringung aller gefilterten Alarme", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Alle gefilterten Elemente löschen", + "@deleteAllFilteredAction": {}, + "alarmDescriptionWeekly": "Jeden {days}", + "@alarmDescriptionWeekly": {}, + "defaultSettingGroup": "Standardeinstellungen", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Standardwerte für neue Alarme festlegen", + "@alarmsDefaultSettingGroupDescription": {}, + "notificationsSettingGroup": "Benachrichtigungen", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Anstehende Alarmbenachrichtigungen anzeigen", + "@showUpcomingAlarmNotificationSetting": {}, + "timerDefaultSettingGroupDescription": "Standardwerte für neue Timer festlegen", + "@timerDefaultSettingGroupDescription": {}, + "upcomingLeadTimeSetting": "Zeit vor dem Alarm", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Wenn Schlummernd, Benachrichtigungen anzeigen", + "@showSnoozeNotificationSetting": {}, + "filtersSettingGroup": "Filter", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Filter anzeigen", + "@showFiltersSetting": {}, + "showSortSetting": "Sortierung anzeigen", + "@showSortSetting": {}, + "showNotificationSetting": "Benachrichtigungen anzeigen", + "@showNotificationSetting": {}, + "stopwatchShowMillisecondsSetting": "Millisekunden anzeigen", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Vergleich Lap Bars", + "@comparisonLapBarsSettingGroup": {}, + "packageNameLabel": "Name des Pakets", + "@packageNameLabel": {}, + "licenseLabel": "Lizenz", + "@licenseLabel": {}, + "emailLabel": "E-mail", + "@emailLabel": {}, + "viewOnGithubLabel": "Auf Github anzeigen", + "@viewOnGithubLabel": {}, + "dismissActionSetting": "Aktionstyp, wenn der Alarm verpasst wird", + "@dismissActionSetting": {}, + "dismissActionSlide": "Schieben", + "@dismissActionSlide": {}, + "presetsSetting": "Voreinstellungen", + "@presetsSetting": {}, + "newPresetPlaceholder": "Neue Voreinstellung", + "@newPresetPlaceholder": {}, + "dismissActionButtons": "Buttons", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Bereich Buttons", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Zeit Format", + "@stopwatchTimeFormatSettingGroup": {}, + "showPreviousLapSetting": "Vorherige Runde anzeigen", + "@showPreviousLapSetting": {}, + "sameTime": "Gleiche Zeit", + "@sameTime": {}, + "searchCityPlaceholder": "Nach einer Stadt suchen", + "@searchCityPlaceholder": {}, + "relativeTime": "{hours}h {relative, select, ahead{voraus} behind{hinter} other{Sonstiges}}", + "@relativeTime": {}, + "tuesdayShort": "Die", + "@tuesdayShort": {}, + "wednesdayShort": "Mit", + "@wednesdayShort": {}, + "thursdayShort": "Don", + "@thursdayShort": {}, + "fridayShort": "Fre", + "@fridayShort": {}, + "saturdayFull": "Samstag", + "@saturdayFull": {}, + "sundayFull": "Sonntag", + "@sundayFull": {}, + "mondayShort": "Mon", + "@mondayShort": {}, + "donorsDescription": "Unsere großzügigen Patreons", + "@donorsDescription": {}, + "contributorsDescription": "Menschen, die diese App möglich machen", + "@contributorsDescription": {}, + "thursdayFull": "Donnerstag", + "@thursdayFull": {}, + "fridayFull": "Freitag", + "@fridayFull": {}, + "saturdayShort": "Sam", + "@saturdayShort": {}, + "donateDescription": "Spenden, um die Entwicklung der App zu unterstützen", + "@donateDescription": {}, + "sundayShort": "Son", + "@sundayShort": {}, + "longDateFormatSetting": "Langes Datumsformat", + "@longDateFormatSetting": {}, + "batteryOptimizationSettingDescription": "Deaktivieren Sie die Batterieoptimierung für diese Anwendung, um zu verhindern, dass Alarme verzögert werden", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Sperrbildschirm Benachrichtigungen für Alarme und Timer zulassen", + "@allowNotificationSettingDescription": {}, + "pickerDial": "Kreise", + "@pickerDial": {}, + "swipActionCardAction": "Kartenaktionen", + "@swipActionCardAction": {}, + "swipeActionSwitchTabsDescription": "Zwischen Registerkarten wischen", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Melodien", + "@melodiesSetting": {}, + "tagsSetting": "Stichworte", + "@tagsSetting": {}, + "vendorSetting": "Anbieter Einstellungen", + "@vendorSetting": {}, + "vendorSettingDescription": "Manuelle Deaktivierung herstellerspezifischer Optimierungen", + "@vendorSettingDescription": {}, + "colorSchemeBackgroundSettingGroup": "Hintergrund", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Akzent", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Fehler", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Karte", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Schatten", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsShadowSetting": "Aktzent als Schatten nutzen", + "@colorSchemeUseAccentAsShadowSetting": {}, + "colorSchemeUseAccentAsOutlineSetting": "Akzent als Außenlinie nutzen", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSetting": "Farbe", + "@colorSetting": {}, + "textColorSetting": "Text", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Farbschema", + "@colorSchemeNamePlaceholder": {}, + "styleThemeNamePlaceholder": "Stil-Thema", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Schatten", + "@styleThemeShadowSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Umriss", + "@colorSchemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Breite", + "@styleThemeOutlineWidthSetting": {}, + "showIstantAlarmButtonSetting": "Sofortalarm Button anzeigen", + "@showIstantAlarmButtonSetting": {}, + "styleThemeOutlineSettingGroup": "Umriss", + "@styleThemeOutlineSettingGroup": {}, + "showIstantTimerButtonSetting": "„Sofortigen Timer\" Button anzeigen", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Protokolle", + "@logsSettingGroup": {}, + "maxLogsSetting": "Maximale Protokolle", + "@maxLogsSetting": {}, + "alarmLogSetting": "Alarm Protokolle", + "@alarmLogSetting": {}, + "resetButton": "Zurücketzen", + "@resetButton": {}, + "alarmWeekdaysSetting": "Wochen Tage", + "@alarmWeekdaysSetting": {}, + "exportSettingsSetting": "Exportieren", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Einstellungen in eine lokale Datei exportieren", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Importieren", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importieren von Einstellungen aus einer lokalen Datei", + "@importSettingsSettingDescription": {}, + "showFastestLapSetting": "Schnellste Runde anzeigen", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Durchschnittliche Runde anzeigen", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Langsamste Runde anzeigen", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Linkshändermodus", + "@leftHandedSetting": {}, + "versionLabel": "Version", + "@versionLabel": {}, + "autoStartSettingDescription": "Bei einigen Geräten muss der automatische Start aktiviert sein, damit die Alarme klingeln, wenn die App geschlossen ist", + "@autoStartSettingDescription": {}, + "allowNotificationSetting": "Manuelles Zulassen aller Benachrichtigungen", + "@allowNotificationSetting": {}, + "autoStartSetting": "Auto Start", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Berechtigungen", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Batterie Optimierung ignorieren", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Benachrichtigung Erlaubnis", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Benachrichtigungen Erlaubnis bereits erteilt", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "gnore Batterieoptimierung Erlaubnis bereits erteilt", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "animationSettingGroup": "Animationen", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Animations Geschwindigkeit", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Extra Animationen", + "@extraAnimationSetting": {}, + "nameField": "Name", + "@nameField": {}, + "styleThemeBlurSetting": "Unschärfe", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Verbreitung", + "@styleThemeSpreadSetting": {}, + "materialBrightnessLight": "Hell", + "@materialBrightnessLight": {}, + "alarmDatesSetting": "Daten", + "@alarmDatesSetting": {}, + "alarmIntervalWeekly": "Wöchentlich", + "@alarmIntervalWeekly": {}, + "scheduleTypeDate": "Zu bestimmten Daten", + "@scheduleTypeDate": {}, + "mathHardDifficulty": "Schwer (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Sehr schwer (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeNumberChars": "Anzahl der Zeichen", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Zahlen einbeziehen", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Kleinbuchstaben einbeziehen", + "@retypeLowercaseSetting": {}, + "soundAndVibrationSettingGroup": "Ton und Vibration", + "@soundAndVibrationSettingGroup": {}, + "sequenceLengthSetting": "Länge der Sequenz", + "@sequenceLengthSetting": {}, + "audioChannelNotification": "Benachrichtigung", + "@audioChannelNotification": {}, + "sequenceGridSizeSetting": "Rastergröße", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Anzahl der Probleme", + "@numberOfProblemsSetting": {}, + "widgetsSettingGroup": "Widgets", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Digitale Uhr", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Layout", + "@layoutSettingGroup": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Alarm kann nicht deaktiviert werden, wenn er im Schlummerzustand ist", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "yesButton": "Ja", + "@yesButton": {}, + "completedFilter": "Abgeschlossen", + "@completedFilter": {}, + "snoozedFilter": "Schlummernd", + "@snoozedFilter": {}, + "remainingTimeDesc": "Wenig Zeit übrig", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Die meiste Zeit übrig", + "@remainingTimeAsc": {}, + "disabledFilter": "Deaktiviert", + "@disabledFilter": {}, + "searchSettingPlaceholder": "Nach einer Einstellung suchen", + "@searchSettingPlaceholder": {}, + "mondayFull": "Montag", + "@mondayFull": {}, + "tuesdayFull": "Dienstag", + "@tuesdayFull": {}, + "editPresetsTitle": "Voreinstellungen bearbeiten", + "@editPresetsTitle": {}, + "wednesdayLetter": "M", + "@wednesdayLetter": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "alignmentLeft": "Links", + "@alignmentLeft": {}, + "alignmentCenter": "Zentrum", + "@alignmentCenter": {}, + "alignmentRight": "Rechts", + "@alignmentRight": {}, + "defaultPageSetting": "Standard Registerkarte", + "@defaultPageSetting": {}, + "translateLink": "Übersetzen", + "@translateLink": {}, + "textSettingGroup": "Text", + "@textSettingGroup": {}, + "showDateSetting": "Datum anzeigen", + "@showDateSetting": {}, + "settingsTitle": "Einstellungen", + "@settingsTitle": {}, + "donateButton": "Spenden", + "@donateButton": {}, + "addLengthSetting": "Länge hinzufügen", + "@addLengthSetting": {}, + "cityAlreadyInFavorites": "Diese Stadt ist bereits in deinen Favoriten", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Dauer wählen", + "@durationPickerTitle": {}, + "hoursString": "{count, plural, =0{} =1{1 Stunde} other{{count} Stunden}}", + "@hoursString": {}, + "secondsString": "{count, plural, =0{} =1{1 Sekunde} other{{count} Sekunden}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 Tag} other{{count} Tage}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 Woche} other{{count} Wochen}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 Monat} other{{count} Monate}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 Jahr} other{{count} Jahre}}", + "@yearsString": {}, + "lessThanOneMinute": "weniger als 1 Minute", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Der Alarm ertönt in {duration}", + "@alarmRingInMessage": {}, + "minutesString": "{count, plural, =0{} =1{1 Minute} other{{count} Minuten}}", + "@minutesString": {}, + "nextAlarmIn": "Nächste: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} und {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showNextAlarm": "Nächsten Alarm anzeigen", + "@showNextAlarm": {}, + "showForegroundNotification": "Vordergrund-Benachrichtigung anzeigen", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Eine dauerhafte Benachrichtigung anzeigen, um die App am Leben zu erhalten", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Anzeigen von Benachrichtigungen zulassen", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Animationen anzeigen, die nicht optimiert sind und bei leistungsschwachen Geräten zu Bildaussetzern führen können", + "@extraAnimationSettingDescription": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a95d89ae..86275552 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -45,10 +45,14 @@ "@pickerInput": {}, "pickerSpinner": "Spinner", "@pickerSpinner": {}, + "pickerNumpad": "Numpad", + "@pickerNumpad": {}, "durationPickerSetting": "Duration Picker", "@durationPickerSetting": {}, "pickerRings": "Rings", "@pickerRings": {}, + "interactionsSettingGroup": "Interactions", + "@interactionsSettingGroup": {}, "swipeActionSetting": "Swipe Action", "@swipeActionSetting": {}, "swipActionCardAction": "Card Actions", @@ -59,6 +63,12 @@ "@swipActionSwitchTabs": {}, "swipeActionSwitchTabsDescription": "Swipe between tabs", "@swipeActionSwitchTabsDescription": {}, + "longPressActionSetting": "Long Press Action", + "@longPressActionSetting": {}, + "longPressReorderAction": "Reorder", + "@longPressReorderAction": {}, + "longPressSelectAction": "Multiselect", + "@longPressSelectAction": {}, "melodiesSetting": "Melodies", "@melodiesSetting": {}, "tagsSetting": "Tags", @@ -149,16 +159,24 @@ "@backupSettingGroup": {}, "developerOptionsSettingGroup": "Developer Options", "@developerOptionsSettingGroup": {}, - "showIstantAlarmButtonSetting": "Show Instant Alarm Button", + "showIstantAlarmButtonSetting": "Show instant alarm button", "@showIstantAlarmButtonSetting": {}, - "showIstantTimerButtonSetting": "Show Instant Timer Button", + "showIstantTimerButtonSetting": "Show instant timer button", "@showIstantTimerButtonSetting": {}, "logsSettingGroup": "Logs", "@logsSettingGroup": {}, - "maxLogsSetting": "Max Logs", + "maxLogsSetting": "Max alarm logs", "@maxLogsSetting": {}, - "alarmLogSetting": "Alarm Logs", + "alarmLogSetting": "Alarm logs", "@alarmLogSetting": {}, + "appLogs": "App logs", + "@appLogs": {}, + "saveLogs": "Save logs", + "@saveLogs": {}, + "showErrorSnackbars": "Show error snackbars", + "@showErrorSnackbars": {}, + "clearLogs": "Clear logs", + "@clearLogs": {}, "aboutSettingGroup": "About", "@aboutSettingGroup": {}, "restoreSettingGroup": "Restore default values", @@ -277,6 +295,10 @@ "@settingGroupMore": {}, "melodySetting": "Melody", "@melodySetting": {}, + "startMelodyAtRandomPos": "Random position", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "Melody will start at a random position", + "@startMelodyAtRandomPosDescription": {}, "vibrationSetting": "Vibration", "@vibrationSetting": {}, "audioChannelSetting": "Audio Channel", @@ -291,6 +313,8 @@ "@audioChannelMedia": {}, "volumeSetting": "Volume", "@volumeSetting": {}, + "volumeWhileTasks": "Volume while solving Tasks", + "@volumeWhileTasks": {}, "risingVolumeSetting": "Rising Volume", "@risingVolumeSetting": {}, "timeToFullVolumeSetting": "Time to Full Volume", @@ -409,6 +433,12 @@ "@pausedTimerFilter": {}, "stoppedTimerFilter": "Stopped", "@stoppedTimerFilter": {}, + "selectionStatus": "{n} selected", + "@selectionStatus": {}, + "selectAll": "Select all", + "@selectAll": {}, + "reorder": "Reorder", + "@reorder": {}, "sortGroup": "Sort", "@sortGroup": {}, "defaultLabel": "Default", @@ -439,10 +469,20 @@ "@disableAllFilteredAlarmsAction": {}, "skipAllFilteredAlarmsAction": "Skip all filtered alarms", "@skipAllFilteredAlarmsAction": {}, + "shuffleAlarmMelodiesAction": "Shuffle melodies for all filtered alarms", + "@shuffleAlarmMelodiesAction":{}, "cancelSkipAllFilteredAlarmsAction": "Cancel skip all filtered alarms", "@cancelSkipAllFilteredAlarmsAction": {}, "deleteAllFilteredAction": "Delete all filtered items", "@deleteAllFilteredAction": {}, + "resetAllFilteredTimersAction": "Reset all filtered timers", + "@resetAllFilteredTimersAction": {}, + "playAllFilteredTimersAction": "Play all filtered timers", + "@playAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Pause all filtered timers", + "@pauseAllFilteredTimersAction": {}, + "shuffleTimerMelodiesAction": "Shuffle melodies for all filtered timers", + "@shuffleTimerMelodiesAction": {}, "skippingDescriptionSuffix": "(skipping next occurrence)", "@skippingDescriptionSuffix": {}, "alarmDescriptionSnooze": "Snoozed until {date}", @@ -674,7 +714,62 @@ "editTagLabel": "Edit Tag", "@editTagLabel": {}, "tagNamePlaceholder": "Tag name", - "@tagNamePlaceholder": {} - - + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 week} other{{count} weeks}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 month} other{{count} months}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 year} other{{count} years}}", + "@yearsString": {}, + "lessThanOneMinute": "less than 1 minute", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Alarm will ring in {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Next: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} and {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}h", + "@shortTimeFormat": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showNextAlarm": "Show Next Alarm", + "@showNextAlarm": {}, + "showForegroundNotification": "Show Foreground Notification", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Show a persistent notification to keep app alive", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Allow notifications to be showed", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Show animations that are not polished and might cause frame drops in low-end devices", + "@extraAnimationSettingDescription": {}, + "clockStyleSettingGroup": "Clock Style", + "@clockStyleSettingGroup": {}, + "clockTypeSetting": "Clock type", + "@clockTypeSetting": {}, + "analogClock": "Analog", + "digitalClock": "Digital", + "showClockTicksSetting": "Show ticks", + "majorTicks": "Major ticks only", + "allTicks": "All ticks", + "showNumbersSetting": "Show numbers", + "quarterNumbers": "Quarter numbers only", + "allNumbers": "All numbers", + "none": "None", + "numeralTypeSetting": "Numeral type", + "romanNumeral": "Roman", + "arabicNumeral": "Arabic", + "showDigitalClock": "Show digital clock", + "backgroundServiceIntervalSetting": "Background service interval", + "backgroundServiceIntervalSettingDescription": "Lower interval will help keep the app alive, at the cost of some battery life" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 61302315..1e9f756f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -293,7 +293,7 @@ "@showIstantAlarmButtonSetting": {}, "showIstantTimerButtonSetting": "Mostrar botón de temporizador instantáneo", "@showIstantTimerButtonSetting": {}, - "maxLogsSetting": "Registros máximos", + "maxLogsSetting": "Registro máximo de alarmas", "@maxLogsSetting": {}, "alarmLogSetting": "Registros de alarmas", "@alarmLogSetting": {}, @@ -666,5 +666,115 @@ "noTagsMessage": "No se crearon etiquetas", "@noTagsMessage": {}, "editTagLabel": "Editar etiqueta", - "@editTagLabel": {} + "@editTagLabel": {}, + "minutesString": "{count, plural, =0{} =1{1 minuto} other{{count} minutos}}", + "@minutesString": {}, + "hoursString": "{count, plural, =0{} =1{1 hora} other{{count} horas}}", + "@hoursString": {}, + "monthsString": "{count, plural, =0{} =1{1 mes} other{{count} meses}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 año} other{{count} años}}", + "@yearsString": {}, + "nextAlarmIn": "Siguiente: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} y {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}hr.", + "@shortHoursString": {}, + "alarmRingInMessage": "La alarma sonará en {duration}", + "@alarmRingInMessage": {}, + "shortMinutesString": "{minutes}min", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}seg.", + "@shortSecondsString": {}, + "secondsString": "{count, plural, =0{} =1{1 segundo} other{{count} segundos}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 día} other{{count} días}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 semana} other{{count} semanas}}", + "@weeksString": {}, + "lessThanOneMinute": "menos de 1 minuto", + "@lessThanOneMinute": {}, + "showNextAlarm": "Mostrar la siguiente alarma", + "@showNextAlarm": {}, + "notificationPermissionDescription": "Permitir que se muestren notificaciones", + "@notificationPermissionDescription": {}, + "showForegroundNotification": "Mostrar notificación en primer plano", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Mostrar una notificación persistente para mantener activa la aplicación", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "Muestra animaciones que no están pulidas y que pueden provocar caídas de cuadros en dispositivos de gama baja", + "@extraAnimationSettingDescription": {}, + "longPressActionSetting": "Acción de pulsación prolongada", + "@longPressActionSetting": {}, + "longPressReorderAction": "Reordenar", + "@longPressReorderAction": {}, + "longPressSelectAction": "Selección múltiple", + "@longPressSelectAction": {}, + "saveLogs": "Guardar los registros", + "@saveLogs": {}, + "clearLogs": "Borrar registros", + "@clearLogs": {}, + "selectionStatus": "{n} seleccionado(s)", + "@selectionStatus": {}, + "pickerNumpad": "Teclado numérico", + "@pickerNumpad": {}, + "interactionsSettingGroup": "Interacciones", + "@interactionsSettingGroup": {}, + "showErrorSnackbars": "Mostrar notificaciones con los errores", + "@showErrorSnackbars": {}, + "volumeWhileTasks": "Volumen al resolver tareas", + "@volumeWhileTasks": {}, + "selectAll": "Seleccionar todo", + "@selectAll": {}, + "reorder": "Reordenar", + "@reorder": {}, + "startMelodyAtRandomPos": "Posición aleatoria", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "La melodía comenzará en una posición aleatoria", + "@startMelodyAtRandomPosDescription": {}, + "shuffleAlarmMelodiesAction": "Melodías aleatorias para todas las alarmas filtradas", + "@shuffleAlarmMelodiesAction": {}, + "resetAllFilteredTimersAction": "Reiniciar todos los temporizadores filtrados", + "@resetAllFilteredTimersAction": {}, + "playAllFilteredTimersAction": "Reproducir todos los temporizadores filtrados", + "@playAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Pausar todos los temporizadores filtrados", + "@pauseAllFilteredTimersAction": {}, + "appLogs": "Registros de aplicaciones", + "@appLogs": {}, + "shuffleTimerMelodiesAction": "Melodías aleatorias para todos los temporizadores filtrados", + "@shuffleTimerMelodiesAction": {}, + "showNumbersSetting": "Ver números", + "@showNumbersSetting": {}, + "clockStyleSettingGroup": "Estilo reloj", + "@clockStyleSettingGroup": {}, + "clockTypeSetting": "Tipo de reloj", + "@clockTypeSetting": {}, + "analogClock": "Analógico", + "@analogClock": {}, + "digitalClock": "Digital", + "@digitalClock": {}, + "allNumbers": "Todos los números", + "@allNumbers": {}, + "none": "Ninguno", + "@none": {}, + "arabicNumeral": "Arábigas", + "@arabicNumeral": {}, + "showDigitalClock": "Mostrar reloj digital", + "@showDigitalClock": {}, + "numeralTypeSetting": "Tipo de numeral", + "@numeralTypeSetting": {}, + "romanNumeral": "Romanos", + "@romanNumeral": {}, + "backgroundServiceIntervalSettingDescription": "Un intervalo más bajo ayudará a mantener activa la aplicación, a costa de algo de duración en la batería", + "@backgroundServiceIntervalSettingDescription": {}, + "showClockTicksSetting": "Mostrar marcas", + "@showClockTicksSetting": {}, + "majorTicks": "Solo las mejores marcas", + "@majorTicks": {}, + "allTicks": "Toda las marcas", + "@allTicks": {}, + "backgroundServiceIntervalSetting": "Intervalo del servicio en segundo plano", + "@backgroundServiceIntervalSetting": {} } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb new file mode 100644 index 00000000..946d1a00 --- /dev/null +++ b/lib/l10n/app_fa.arb @@ -0,0 +1,716 @@ +{ + "system": "سامانه", + "@system": {}, + "generalSettingGroup": "فراگیر", + "@generalSettingGroup": {}, + "generalSettingGroupDescription": "کارگذاردن پیکربندی‌های سراسری مانند کارپ زمان", + "@generalSettingGroupDescription": {}, + "timerTitle": "زمان‌سنج", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "longDateFormatSetting": "کارپ تاریخ گسترده", + "@longDateFormatSetting": {}, + "timeFormatSetting": "کارپ زمان", + "@timeFormatSetting": {}, + "timeFormat12": "۱۲ ساعته", + "@timeFormat12": {}, + "timeFormatDevice": "پیکربندی دستگاه", + "@timeFormatDevice": {}, + "pickerDial": "شماره‌گیر", + "@pickerDial": {}, + "pickerInput": "نهادن", + "@pickerInput": {}, + "pickerSpinner": "گردان", + "@pickerSpinner": {}, + "durationPickerSetting": "گاه گزین", + "@durationPickerSetting": {}, + "pickerRings": "زُرفین", + "@pickerRings": {}, + "swipeActionSetting": "کنش کشیدن", + "@swipeActionSetting": {}, + "swipActionCardAction": "کنش کادر", + "@swipActionCardAction": {}, + "swipActionSwitchTabs": "تغییر زبانه‌ها", + "@swipActionSwitchTabs": {}, + "melodiesSetting": "نوا‌ها", + "@melodiesSetting": {}, + "tagsSetting": "برچسب‌ها", + "@tagsSetting": {}, + "allowNotificationSetting": "اجازه به همه‌ی آگاهنده‌ها به صورت دستی", + "@allowNotificationSetting": {}, + "autoStartSetting": "روشن‌شدن خودکار", + "@autoStartSetting": {}, + "permissionsSettingGroup": "مجوز‌ها", + "@permissionsSettingGroup": {}, + "notificationPermissionSetting": "مجوز آگاهنده", + "@notificationPermissionSetting": {}, + "animationSpeedSetting": "تندی انیمیشن‌ها", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "انیمیشن‌های بیشتر", + "@extraAnimationSetting": {}, + "ignoreBatteryOptimizationSetting": "رد‌کردن بهینه‌سازی باتری", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionAlreadyGranted": "اجازه به آگاهنده‌ها از پیش داده شده است", + "@notificationPermissionAlreadyGranted": {}, + "animationSettingGroup": "انیمیشن‌ها", + "@animationSettingGroup": {}, + "appearanceSettingGroup": "نمایه", + "@appearanceSettingGroup": {}, + "nameField": "نام", + "@nameField": {}, + "colorSchemeNamePlaceholder": "الگو رنگ", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "پس‌زمینه", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "رنگ مهند", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "خطا", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "چهارچوب", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "کرانه", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "سایه", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "بکارگیری رنگ مهند برای کرانه", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "بکارگیری رنگ مهند برای سایه", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeNamePlaceholder": "فتن تم", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "سایه", + "@styleThemeShadowSettingGroup": {}, + "styleThemeElevationSetting": "بلندا", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "گردی گوشه", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "شفافیت", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "گسترش", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineSettingGroup": "کرانه", + "@styleThemeOutlineSettingGroup": {}, + "accessibilitySettingGroup": "دسترسی", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "پشتیبان‌گیری", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "برای برنامه‌نویسان", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "نمایش دکمه‌ی هشدار آنی", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "نمایش دکمه‌ی زمان‌سنج آنی", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "گزارش‌ها", + "@logsSettingGroup": {}, + "restoreSettingGroup": "بازگردانی به پیش‌گزیده", + "@restoreSettingGroup": {}, + "resetButton": "بازنشانی", + "@resetButton": {}, + "previewLabel": "پیش‌پرده", + "@previewLabel": {}, + "accentLabel": "رنگ مهند", + "@accentLabel": {}, + "cardLabel": "کادر", + "@cardLabel": {}, + "errorLabel": "خطا", + "@errorLabel": {}, + "displaySettingGroup": "نمایش", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "قابل اعتماد", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "رنگ‌ها", + "@colorsSettingGroup": {}, + "styleSettingGroup": "فتن", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "بکارگیری متریال یو", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "روشنایی", + "@materialBrightnessSetting": {}, + "materialBrightnessSystem": "سامانه", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "روشن", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "تیره", + "@materialBrightnessDark": {}, + "overrideAccentSetting": "برتر کردن رنگ مهند", + "@overrideAccentSetting": {}, + "accentColorSetting": "رنگ مهند", + "@accentColorSetting": {}, + "systemDarkModeSetting": "سان تیره سامانه", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "الگو رنگ", + "@colorSchemeSetting": {}, + "darkColorSchemeSetting": "الگو رنگ تیره", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "ساعت", + "@clockSettingGroup": {}, + "timerSettingGroup": "زمان‌سنج", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "گاه‌شمار", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "برون‌برد یا درون‌برد پیکربندی برزنی خود", + "@backupSettingGroupDescription": {}, + "alarmWeekdaysSetting": "روزهای کاری", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "تاریخ‌ها", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "دامنه‌ی تاریخ", + "@alarmRangeSetting": {}, + "alarmIntervalDaily": "روزانه", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "هفتگی", + "@alarmIntervalWeekly": {}, + "selectTime": "گزینش زمان", + "@selectTime": {}, + "timePickerModeButton": "سان", + "@timePickerModeButton": {}, + "cancelButton": "وازدن", + "@cancelButton": {}, + "customizeButton": "سفارشی سازی", + "@customizeButton": {}, + "saveButton": "ذخیره", + "@saveButton": {}, + "labelFieldPlaceholder": "افزودن عنوان", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "برنامه", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "نوع", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "یکبار", + "@scheduleTypeOnce": {}, + "clockTitle": "ساعت", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "alarmTitle": "هشدار", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "stopwatchTitle": "گاه‌شمار", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "languageSetting": "زبان", + "@languageSetting": {}, + "dateFormatSetting": "کارپ تاریخ", + "@dateFormatSetting": {}, + "showSecondsSetting": "نمایش ثانیه‌ها", + "@showSecondsSetting": {}, + "timePickerSetting": "زمان گزین", + "@timePickerSetting": {}, + "timeFormat24": "۲۴ ساعته", + "@timeFormat24": {}, + "swipeActionCardActionDescription": "روی کادر به چپ یا راست بکشید تا کنش انجام گیرد", + "@swipeActionCardActionDescription": {}, + "swipeActionSwitchTabsDescription": "کشیدن میان زبانه‌ها", + "@swipeActionSwitchTabsDescription": {}, + "vendorSetting": "پیکربندی فرآور", + "@vendorSetting": {}, + "vendorSettingDescription": "خاموش کردن دستی بهینه‌سازی‌های ویژه فرآور", + "@vendorSettingDescription": {}, + "labelField": "سرنام", + "@labelField": {}, + "batteryOptimizationSetting": "خاموش کردن بهینه‌سازی باتری به روش دستی", + "@batteryOptimizationSetting": {}, + "appearanceSettingGroupDescription": "نشاندن تم‌ها، رنگ‌ها و دگرش رونگارها", + "@appearanceSettingGroupDescription": {}, + "colorSetting": "رنگ", + "@colorSetting": {}, + "textColorSetting": "نویسه", + "@textColorSetting": {}, + "styleThemeShapeSettingGroup": "کرپ(شکل)", + "@styleThemeShapeSettingGroup": {}, + "styleThemeBlurSetting": "تاری", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineWidthSetting": "پهنا", + "@styleThemeOutlineWidthSetting": {}, + "maxLogsSetting": "بیشینه‌ی گزارش‌ها", + "@maxLogsSetting": {}, + "alarmLogSetting": "گزارش‌های هشدار", + "@alarmLogSetting": {}, + "aboutSettingGroup": "درباره برنامه", + "@aboutSettingGroup": {}, + "useMaterialStyleSetting": "بکارگیری فتن متریال یو", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "فتن تم", + "@styleThemeSetting": {}, + "alarmDeleteAfterFinishingSetting": "پاک‌کردن پس‌از پایان یافتن", + "@alarmDeleteAfterFinishingSetting": {}, + "alarmDeleteAfterRingingSetting": "پاک‌کردن پس‌از ردکردن", + "@alarmDeleteAfterRingingSetting": {}, + "scheduleTypeDaily": "روزانه", + "@scheduleTypeDaily": {}, + "scheduleTypeDailyDescription": "هر روز زنگ بخورد", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "در روز‌های کاری مشخص", + "@scheduleTypeWeek": {}, + "scheduleTypeDate": "در تاریخ‌های مشخص", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "دامنه‌ی تاریخ", + "@scheduleTypeRange": {}, + "soundAndVibrationSettingGroup": "صدا و لرزش", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "صدا", + "@soundSettingGroup": {}, + "settingGroupMore": "بیشتر", + "@settingGroupMore": {}, + "melodySetting": "نوا", + "@melodySetting": {}, + "audioChannelNotification": "آگاهنده", + "@audioChannelNotification": {}, + "audioChannelRingtone": "صدای‌زنگ", + "@audioChannelRingtone": {}, + "audioChannelMedia": "رسانه", + "@audioChannelMedia": {}, + "volumeSetting": "بلندی صدا", + "@volumeSetting": {}, + "risingVolumeSetting": "صدای فزاینده", + "@risingVolumeSetting": {}, + "snoozeLengthSetting": "مدت", + "@snoozeLengthSetting": {}, + "maxSnoozesSetting": "بیشینه واپَس انداختن‌ها", + "@maxSnoozesSetting": {}, + "snoozeEnableSetting": "روشن", + "@snoozeEnableSetting": {}, + "tasksSetting": "گماران (کار‌ها)", + "@tasksSetting": {}, + "noItemMessage": "هنوز {items}ی افزوده نشده است", + "@noItemMessage": {}, + "chooseTaskTitle": "گزینش گمار(کار) برای افزودن", + "@chooseTaskTitle": {}, + "mathEasyDifficulty": "آسان (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "میانه (X + Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "سخت (X × Y + Z)", + "@mathHardDifficulty": {}, + "taskTryButton": "آزمودن", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "دشواری", + "@mathTaskDifficultySetting": {}, + "retypeLowercaseSetting": "گنجاندن حروف کوچک", + "@retypeLowercaseSetting": {}, + "numberOfProblemsSetting": "شمار پرسش‌ها", + "@numberOfProblemsSetting": {}, + "yesButton": "آری", + "@yesButton": {}, + "noAlarmMessage": "هشداری ساخته نشده است", + "@noAlarmMessage": {}, + "noTagsMessage": "برچسبی ساخته نشده است", + "@noTagsMessage": {}, + "noPresetsMessage": "پیش‌ساختی ایجاد نشده است", + "@noPresetsMessage": {}, + "duplicateButton": "همانند‌سازی", + "@duplicateButton": {}, + "skipAlarmButton": "رد کردن هشدار پسین", + "@skipAlarmButton": {}, + "dismissAlarmButton": "رها‌کردن", + "@dismissAlarmButton": {}, + "allFilter": "همه", + "@allFilter": {}, + "scheduleDateFilterGroup": "برنامه‌ریزی تاریخ", + "@scheduleDateFilterGroup": {}, + "createdDateFilterGroup": "تاریخ ایجاد شدن", + "@createdDateFilterGroup": {}, + "todayFilter": "امروز", + "@todayFilter": {}, + "tomorrowFilter": "فردا", + "@tomorrowFilter": {}, + "stateFilterGroup": "چگونگی", + "@stateFilterGroup": {}, + "snoozedFilter": "واپَس افتاده", + "@snoozedFilter": {}, + "completedFilter": "پایان یافته", + "@completedFilter": {}, + "disabledFilter": "خاموش", + "@disabledFilter": {}, + "pausedTimerFilter": "متوقف", + "@pausedTimerFilter": {}, + "alarmDescriptionWeekly": "هر {days}", + "@alarmDescriptionWeekly": {}, + "vibrationSetting": "لرزش", + "@vibrationSetting": {}, + "audioChannelAlarm": "هشدار", + "@audioChannelAlarm": {}, + "timeToFullVolumeSetting": "زمان تا رسیدن به نهایت حجم صدا", + "@timeToFullVolumeSetting": {}, + "snoozePreventDeletionSetting": "بازداشتن از پاک‌کردن", + "@snoozePreventDeletionSetting": {}, + "snoozeSettingGroup": "واپَساندن", + "@snoozeSettingGroup": {}, + "whileSnoozedSettingGroup": "هنگام واپَسانِش", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "بازداشتن از خاموش کردن", + "@snoozePreventDisablingSetting": {}, + "settings": "پیکربندی", + "@settings": {}, + "mathTask": "پرسش‌های ریاضی", + "@mathTask": {}, + "mathVeryHardDifficulty": "دشوار (X × Y + Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "بازنویسی نوشتار", + "@retypeTask": {}, + "sequenceGridSizeSetting": "اندازه‌ی تکه", + "@sequenceGridSizeSetting": {}, + "retypeIncludeNumSetting": "گنجاندن شماره‌ها", + "@retypeIncludeNumSetting": {}, + "saveReminderAlert": "آیا می خواهید بدون ذخیره بیرون شوید؟", + "@saveReminderAlert": {}, + "noButton": "خیر", + "@noButton": {}, + "noTimerMessage": "زمان‌سنجی ساخته نشده است", + "@noTimerMessage": {}, + "noStopwatchMessage": "گاه‌شماری ساخته نشده است", + "@noStopwatchMessage": {}, + "noTaskMessage": "گماری ایجاد نشده است", + "@noTaskMessage": {}, + "noLogsMessage": "گزارش زنگ هشدار وجود ندارد", + "@noLogsMessage": {}, + "deleteButton": "پاک‌کردن", + "@deleteButton": {}, + "cancelSkipAlarmButton": "وازدن ردشدن", + "@cancelSkipAlarmButton": {}, + "dateFilterGroup": "تاریخ", + "@dateFilterGroup": {}, + "logTypeFilterGroup": "نوع", + "@logTypeFilterGroup": {}, + "activeFilter": "فعال", + "@activeFilter": {}, + "inactiveFilter": "غیرفعال", + "@inactiveFilter": {}, + "runningTimerFilter": "درحال انجام", + "@runningTimerFilter": {}, + "allowNotificationSettingDescription": "نا", + "@allowNotificationSettingDescription": {}, + "alarmIntervalSetting": "درنگ", + "@alarmIntervalSetting": {}, + "scheduleTypeOnceDescription": "دفعه پسین زنگ میزند", + "@scheduleTypeOnceDescription": {}, + "stopwatchTimeFormatSettingGroup": "کارپ زمان", + "@stopwatchTimeFormatSettingGroup": {}, + "editButton": "ویرایش", + "@editButton": {}, + "mondayFull": "دوشنبه", + "@mondayFull": {}, + "wednesdayFull": "چهارشنبه", + "@wednesdayFull": {}, + "thursdayFull": "پنج‌شنبه", + "@thursdayFull": {}, + "saturdayFull": "شنبه", + "@saturdayFull": {}, + "mondayShort": "دش", + "@mondayShort": {}, + "sundayLetter": "ی", + "@sundayLetter": {}, + "textSettingGroup": "نویسه", + "@textSettingGroup": {}, + "lessThanOneMinute": "کمتر از یک دقیقه", + "@lessThanOneMinute": {}, + "showDateSetting": "نمایش تاریخ", + "@showDateSetting": {}, + "alignmentTop": "بالا", + "@alignmentTop": {}, + "alignmentBottom": "پایین", + "@alignmentBottom": {}, + "alignmentLeft": "چپ‌", + "@alignmentLeft": {}, + "alignmentCenter": "میانه", + "@alignmentCenter": {}, + "alignmentRight": "راست", + "@alignmentRight": {}, + "alignmentJustify": "ترازبندی", + "@alignmentJustify": {}, + "fontWeightSetting": "ستبرای هخام", + "@fontWeightSetting": {}, + "dateSettingGroup": "تاریخ", + "@dateSettingGroup": {}, + "timeSettingGroup": "زمان", + "@timeSettingGroup": {}, + "editPresetsTitle": "ویرایش پیش‌گزیده‌ها", + "@editPresetsTitle": {}, + "showMeridiemSetting": "نمایش بامداد/نیمروز", + "@showMeridiemSetting": {}, + "firstDayOfWeekSetting": "روز نخست هفته", + "@firstDayOfWeekSetting": {}, + "translateLink": "ترزبانی", + "@translateLink": {}, + "translateDescription": "کمک‌به ترزبانی برنامه", + "@translateDescription": {}, + "saturdayLetter": "ش", + "@saturdayLetter": {}, + "fridayShort": "آد", + "@fridayShort": {}, + "tuesdayLetter": "س", + "@tuesdayLetter": {}, + "tuesdayFull": "سه‌شنبه", + "@tuesdayFull": {}, + "wednesdayShort": "چ‌ش", + "@wednesdayShort": {}, + "thursdayShort": "پ‌ش", + "@thursdayShort": {}, + "fridayFull": "آدینه", + "@fridayFull": {}, + "sundayFull": "یک‌شنبه", + "@sundayFull": {}, + "tuesdayShort": "س‌ش", + "@tuesdayShort": {}, + "saturdayShort": "ش", + "@saturdayShort": {}, + "sundayShort": "ی‌ش", + "@sundayShort": {}, + "wednesdayLetter": "چ", + "@wednesdayLetter": {}, + "thursdayLetter": "پ", + "@thursdayLetter": {}, + "fridayLetter": "آ", + "@fridayLetter": {}, + "mondayLetter": "د", + "@mondayLetter": {}, + "sizeSetting": "اندازه", + "@sizeSetting": {}, + "defaultPageSetting": "برگه‌ی پیش‌گزیده", + "@defaultPageSetting": {}, + "batteryOptimizationSettingDescription": "خاموش کردن بهینه‌سازی باتری برای این برنامه تا از دیرکرد هشدار‌ها جلوگیری شود", + "@batteryOptimizationSettingDescription": {}, + "autoStartSettingDescription": "برخی دستگاه ها نیاز به باز شدن خودکار برنامه دارند تا هشدار‌ها هنگامی که برنامه بسته شده است زنگ بخورند", + "@autoStartSettingDescription": {}, + "ignoreBatteryOptimizationAlreadyGranted": "اجازه‌ی نادیده گرفتن بهینه سازی باتری از پیش داده شده است.", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "scheduleTypeWeekDescription": "در روزهای مشخصی از هفته واکرد خواهد شد", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeRangeDescription": "در بازه‌ی تاریخ مشخص شده واکرد می شود", + "@scheduleTypeRangeDescription": {}, + "scheduleTypeDateDescription": "در تاریخ‌های مشخص واکرد خواهد شد", + "@scheduleTypeDateDescription": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "نمی‌توان هشدار را هنگامی واپس‌افتاده است، خاموش کرد", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "audioChannelSetting": "کانال صدا", + "@audioChannelSetting": {}, + "sequenceTask": "دنباله", + "@sequenceTask": {}, + "retypeNumberChars": "شمار واچ‌ها", + "@retypeNumberChars": {}, + "sequenceLengthSetting": "درازنای دنباله", + "@sequenceLengthSetting": {}, + "defaultLabel": "پیش‌گزیده", + "@defaultLabel": {}, + "stoppedTimerFilter": "ایستاده", + "@stoppedTimerFilter": {}, + "remainingTimeDesc": "کمترین زمان مانده", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "بیشترین زمان مانده", + "@remainingTimeAsc": {}, + "durationAsc": "کوتاه‌ترین", + "@durationAsc": {}, + "durationDesc": "بلند‌ترین", + "@durationDesc": {}, + "nameAsc": "سرنام الف-ی", + "@nameAsc": {}, + "nameDesc": "سرنام ی- الف", + "@nameDesc": {}, + "timeOfDayAsc": "نخست، ساعات آغازین", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "نخست، ساعات واپسین", + "@timeOfDayDesc": {}, + "filterActions": "پالایه‌ی کنش‌ها", + "@filterActions": {}, + "clearFiltersAction": "پاک‌کردن همه‌ی پالایه‌ها", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "روشن کردن همه‌ی هشدار‌های پالایش شده", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "خاموش کردن همه‌ی هشدار‌های پالایش شده", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "رد کردن همه‌ی هشدار‌های پالایش شده", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "وازدن همه‌ی هشدار‌های پالایش شده", + "@cancelSkipAllFilteredAlarmsAction": {}, + "alarmDescriptionSnooze": "واپَس افتاده تا {date}", + "@alarmDescriptionSnooze": {}, + "deleteAllFilteredAction": "پاک‌کردن همه‌ی مورد‌های پالایش شده", + "@deleteAllFilteredAction": {}, + "skippingDescriptionSuffix": "(پرش به رویداد پسین)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionNotScheduled": "برنامه‌ریزی نشده", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "تنها امروز", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "تنها فردا", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "هر روزه", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "هر پایان هفته", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "پیشین", + "@stopwatchPrevious": {}, + "stopwatchFastest": "تندترین", + "@stopwatchFastest": {}, + "defaultSettingGroup": "پیکربندی پیش‌گزیده", + "@defaultSettingGroup": {}, + "alarmDescriptionDays": "در {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{روزانه} weekly{هفتگی} other{دیگر}} from {startDate} to {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "کندترین", + "@stopwatchSlowest": {}, + "stopwatchAverage": "میانگین", + "@stopwatchAverage": {}, + "alarmsDefaultSettingGroupDescription": "پیش‌گزیده‌ها را برای هشدارهای نو بنشانید", + "@alarmsDefaultSettingGroupDescription": {}, + "notificationsSettingGroup": "آگاهنده‌ها", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "نمایش آگاهنده‌های هشدار پیش‌رو", + "@showUpcomingAlarmNotificationSetting": {}, + "timerDefaultSettingGroupDescription": "پیش‌گزیده‌ها را برای زمان‌سنج‌های نو بنشانید", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "پالایه‌ها", + "@filtersSettingGroup": {}, + "showNotificationSetting": "نمایش آگاهنده", + "@showNotificationSetting": {}, + "showSnoozeNotificationSetting": "نمایش آگاهنده‌ی واپَساندن", + "@showSnoozeNotificationSetting": {}, + "presetsSetting": "پیش‌ساخته‌ها", + "@presetsSetting": {}, + "newPresetPlaceholder": "پیش‌ساخت نو", + "@newPresetPlaceholder": {}, + "upcomingLeadTimeSetting": "زمان یادآوری پیش از هشدار", + "@upcomingLeadTimeSetting": {}, + "dismissActionSetting": "سردگ کنش ردکردن", + "@dismissActionSetting": {}, + "dismissActionSlide": "لغزنده", + "@dismissActionSlide": {}, + "dismissActionButtons": "دکمه‌ای", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "دکمه‌های کرانه‌ای", + "@dismissActionAreaButtons": {}, + "stopwatchShowMillisecondsSetting": "نمایش هزارم ثانیه", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "همسنگی نوارهای چرخه", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "نمایش چرخه پیشین", + "@showPreviousLapSetting": {}, + "showSlowestLapSetting": "نمایش کندترین چرخه", + "@showSlowestLapSetting": {}, + "exportSettingsSettingDescription": "برون‌برد پیکربندی‌ها به پرونده‌ای برزنی", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "درون‌برد", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "درون‌برد پیکربندی‌ها از پرونده‌ای برزنی", + "@importSettingsSettingDescription": {}, + "versionLabel": "نگارش", + "@versionLabel": {}, + "packageNameLabel": "نام بسته", + "@packageNameLabel": {}, + "licenseLabel": "پروانه‌", + "@licenseLabel": {}, + "emailLabel": "ایمیل", + "@emailLabel": {}, + "viewOnGithubLabel": "دیدن در گیت‌هاب", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "پروانه‌های متن‌باز", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "هم‌دستان", + "@contributorsSetting": {}, + "donorsSetting": "پشتیبانان مالی", + "@donorsSetting": {}, + "donateButton": "پشتیبانی مالی کنید", + "@donateButton": {}, + "addLengthSetting": "افزودن درازنا", + "@addLengthSetting": {}, + "sameTime": "هم‌زمان", + "@sameTime": {}, + "searchSettingPlaceholder": "جستجو در پیکربندی‌ها", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "جستجو برای شهر", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "این شهر از پیش در برگزیدگان است", + "@cityAlreadyInFavorites": {}, + "elapsedTime": "زمان گذشته شده", + "@elapsedTime": {}, + "durationPickerTitle": "گزیدن مدت", + "@durationPickerTitle": {}, + "noLapsMessage": "هنوز چرخه‌ای نیست", + "@noLapsMessage": {}, + "donateDescription": "پشتیبانی مالی کنید تا برنامه پیشرفت کند", + "@donateDescription": {}, + "donorsDescription": "پشتیبانان بخشنده‌ی ما", + "@donorsDescription": {}, + "contributorsDescription": "مردمانی که این برنامه را شدنی کردند", + "@contributorsDescription": {}, + "widgetsSettingGroup": "ابزارک‌ها", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "ساعت دیجیتالی", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "رونگار", + "@layoutSettingGroup": {}, + "alarmRingInMessage": "هشدار در {duration} زنگ می‌خورد", + "@alarmRingInMessage": {}, + "nextAlarmIn": "پسین: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} و {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}س", + "@shortHoursString": {}, + "settingsTitle": "پیکربندی‌ها", + "@settingsTitle": {}, + "verticalAlignmentSetting": "چیدمان ستونی", + "@verticalAlignmentSetting": {}, + "horizontalAlignmentSetting": "چیدمان ستانی", + "@horizontalAlignmentSetting": {}, + "separatorSetting": "جدا‌کننده", + "@separatorSetting": {}, + "editTagLabel": "ویرایش برچسب", + "@editTagLabel": {}, + "tagNamePlaceholder": "نام برچسب", + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{۱ ساعت} other{{count} ساعت}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{۱ دقیقه} other{{count} دقیقه}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{۱ ثانیه} other{{count} ثانیه}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{۱ روز} other{{count} روز}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{۱ هفته} other{{count} هفته}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{۱ ماه} other{{count} ماه}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{۱ سال} other{{count} سال}}", + "@yearsString": {}, + "shortMinutesString": "{minutes}د", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}ث", + "@shortSecondsString": {}, + "showNextAlarm": "نمایش هشدار پسین", + "@showNextAlarm": {}, + "showForegroundNotification": "نمایش آگاهنده‌ها در پیش زمینه", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "نمایش آگاهنده ‌های پایا برای بکارگیری پایدار برنامه", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "نمایش انیمیشن‌هایی که بهینه نیستند و می‌تواند مایه کاهش نرخ فریم در دستگاه‌های با کارکرد پایین شوند", + "@extraAnimationSettingDescription": {}, + "notificationPermissionDescription": "اجازه به آگاهنده‌ها که نشان‌داده شوند", + "@notificationPermissionDescription": {}, + "sortGroup": "چینش", + "@sortGroup": {}, + "showFastestLapSetting": "نمایش تندترین چرخه", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "نمایش چرخه‌ی مِدییَه", + "@showAverageLapSetting": {}, + "leftHandedSetting": "سان چپ دست", + "@leftHandedSetting": {}, + "exportSettingsSetting": "برون‌برد", + "@exportSettingsSetting": {}, + "alarmDescriptionFinished": "بی واکرد در آینده", + "@alarmDescriptionFinished": {}, + "alarmDescriptionWeekday": "هر روز‌کاری هفته", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionDates": "On {date}{count, plural, =0{} =1{ و یک تاریخ دیگر} other{ و {count} دیگر تاریخ‌ها}}", + "@alarmDescriptionDates": {}, + "showFiltersSetting": "نمایش پالایه‌ها", + "@showFiltersSetting": {}, + "showSortSetting": "نمایش چینش", + "@showSortSetting": {}, + "relativeTime": "{hours}س {relative, select, ahead{جلو است} behind{عقب است} other{دیگر}}", + "@relativeTime": {} +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 95a9cff3..6a71c90c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -656,5 +656,47 @@ "showMeridiemSetting": "Afficher Am/Pm", "@showMeridiemSetting": {}, "fontWeightSetting": "Hauteur de la police", - "@fontWeightSetting": {} + "@fontWeightSetting": {}, + "translateLink": "Traduire", + "@translateLink": {}, + "translateDescription": "Aider à traduire l'application", + "@translateDescription": {}, + "editPresetsTitle": "Modifier les préréglages", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Premier jour de la semaine", + "@firstDayOfWeekSetting": {}, + "editTagLabel": "Modifier l'étiquette", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nom de l'étiquette", + "@tagNamePlaceholder": {}, + "longDateFormatSetting": "Format de date longue", + "@longDateFormatSetting": {}, + "noTagsMessage": "Aucun tag créé", + "@noTagsMessage": {}, + "separatorSetting": "Séparateur", + "@separatorSetting": {}, + "combinedTime": "{hours} et {minutes}", + "@combinedTime": {}, + "lessThanOneMinute": "moins de 1 minute", + "@lessThanOneMinute": {}, + "nextAlarmIn": "Suivant : {duration}", + "@nextAlarmIn": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showForegroundNotification": "Afficher une notification au premier plan", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Afficher une notification persistante pour empêcher l'appli d'être tuée", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Autorisez les notifications à être affichées", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Afficher des animations en cours de développement qui pourraient causer des chutes de fréquence d'images sur les appareils d'entrée de gamme", + "@extraAnimationSettingDescription": {}, + "alarmRingInMessage": "L'alarme sonnera dans {duration}", + "@alarmRingInMessage": {}, + "showNextAlarm": "Afficher la prochaine alarme", + "@showNextAlarm": {} } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb new file mode 100644 index 00000000..985744cc --- /dev/null +++ b/lib/l10n/app_hu.arb @@ -0,0 +1,50 @@ +{ + "clockTitle": "Óra", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "timerTitle": "Időzítő", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Stopperóra", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "generalSettingGroup": "Általános", + "@generalSettingGroup": {}, + "generalSettingGroupDescription": "Alkalmazáson belüli beállítások, mint a dátumformátum", + "@generalSettingGroupDescription": {}, + "languageSetting": "Nyelv", + "@languageSetting": {}, + "longDateFormatSetting": "Hosszú dátumformátum", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Idő Formátum", + "@timeFormatSetting": {}, + "timeFormat12": "12 órás", + "@timeFormat12": {}, + "timeFormat24": "24 órás", + "@timeFormat24": {}, + "timeFormatDevice": "Eszközbeállítások", + "@timeFormatDevice": {}, + "timePickerSetting": "Idő Kiválasztók", + "@timePickerSetting": {}, + "pickerDial": "Tárcsázós", + "@pickerDial": {}, + "pickerInput": "Beírás", + "@pickerInput": {}, + "swipeActionSetting": "csúsztatás", + "@swipeActionSetting": {}, + "system": "Rendszer", + "@system": {}, + "dateFormatSetting": "Dátumformátum", + "@dateFormatSetting": {}, + "durationPickerSetting": "Időtartam-választó", + "@durationPickerSetting": {}, + "pickerRings": "Gyűrűk", + "@pickerRings": {}, + "showSecondsSetting": "Másodpercek mutatása", + "@showSecondsSetting": {}, + "pickerSpinner": "Pörgetős", + "@pickerSpinner": {} +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 7b24eefb..38888dec 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -66,5 +66,645 @@ "generalSettingGroupDescription": "Impostare la applicazione, es. formato dell'ora", "@generalSettingGroupDescription": {}, "pickerDial": "Quadrante", - "@pickerDial": {} + "@pickerDial": {}, + "notificationPermissionAlreadyGranted": "Il permesso di notifiche è già stato concesso", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Ignora il permesso d'ottimizzazione della batteria già concesso", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "backupSettingGroup": "Copia di sicurezza", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Opzioni dello sviluppatore", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "Mostrare il tasto dell'allarme istantanea", + "@showIstantAlarmButtonSetting": {}, + "alarmWeekdaysSetting": "Giorni della settimana", + "@alarmWeekdaysSetting": {}, + "systemDarkModeSetting": "Modalità scura del sistema", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "Schema di colori", + "@colorSchemeSetting": {}, + "alarmDatesSetting": "Date", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Intervallo di date", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "intervallo", + "@alarmIntervalSetting": {}, + "snoozePreventDisablingSetting": "Evitare disattivazione", + "@snoozePreventDisablingSetting": {}, + "sequenceTask": "Sequenza", + "@sequenceTask": {}, + "taskTryButton": "Provare", + "@taskTryButton": {}, + "accessibilitySettingGroup": "Accessibilità", + "@accessibilitySettingGroup": {}, + "settingGroupMore": "Altro", + "@settingGroupMore": {}, + "longDateFormatSetting": "Formato per date lunghe", + "@longDateFormatSetting": {}, + "vendorSetting": "Impostazioni del fornitore", + "@vendorSetting": {}, + "vendorSettingDescription": "Disattivare manualmente le ottimizzazioni specifiche del fornitore", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Disattivare le ottimizzazioni della batteria manualmente", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Disattivare le ottimizzazioni della batteria di quest'applicazione per evitare ritardi nelle allarme", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Permettere notificazioni nella schermata di blocco per allarmi e temporizzatori", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Alcuni dispositivi richiedono l'attivazione dell'inizio automatico perché le allarme possano suonare quando l'applicazione sia chiusa", + "@autoStartSettingDescription": {}, + "permissionsSettingGroup": "Permessi", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Ignorare l'ottimizzazione della batteria", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Permessi di notifiche", + "@notificationPermissionSetting": {}, + "colorSchemeShadowSettingGroup": "Ombra", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "Utilizzi i colori accentuati per il contorno", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Utilizzare il colore accentuato come ombra", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeNamePlaceholder": "Stilo del tema", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Ombra", + "@styleThemeShadowSettingGroup": {}, + "styleThemeShapeSettingGroup": "Forma", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Elevazione", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "Curvatura degl'angoli", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Opacità", + "@styleThemeOpacitySetting": {}, + "styleThemeOutlineSettingGroup": "Contorno", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeBlurSetting": "Sfumare", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Stendere", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineWidthSetting": "Largo", + "@styleThemeOutlineWidthSetting": {}, + "showIstantTimerButtonSetting": "Mostrare il tasto del temporizzatore istantaneo", + "@showIstantTimerButtonSetting": {}, + "maxLogsSetting": "Massimo di registri", + "@maxLogsSetting": {}, + "alarmLogSetting": "Registri di allarmi", + "@alarmLogSetting": {}, + "aboutSettingGroup": "A proposito di", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Ripristinare i valori predeterminati", + "@restoreSettingGroup": {}, + "overrideAccentSetting": "Annullare l'accentuazione del colore", + "@overrideAccentSetting": {}, + "accentColorSetting": "Accentuare il colore", + "@accentColorSetting": {}, + "useMaterialStyleSetting": "Utilizzare Material Style", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "Stile del tema", + "@styleThemeSetting": {}, + "darkColorSchemeSetting": "Schema di colori scuro", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "Orologio", + "@clockSettingGroup": {}, + "timerSettingGroup": "Timer", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Cronometro", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Esportare o importare le tue impostazioni localmente", + "@backupSettingGroupDescription": {}, + "alarmIntervalWeekly": "Settimanalmente", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Eliminare dopo scartare", + "@alarmDeleteAfterRingingSetting": {}, + "alarmDeleteAfterFinishingSetting": "Eliminare una volta finito", + "@alarmDeleteAfterFinishingSetting": {}, + "selectTime": "Selezionare l'ora", + "@selectTime": {}, + "scheduleTypeRange": "Intervallo di date", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Si ripeterà in date specificate", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Si ripeterà durante un intervallo specifico di date", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Suono e vibrazione", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "suono", + "@soundSettingGroup": {}, + "melodySetting": "Melodia", + "@melodySetting": {}, + "vibrationSetting": "Vibrazione", + "@vibrationSetting": {}, + "audioChannelSetting": "Canale audio", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Allarme", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Notifica", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Suoneria", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Media", + "@audioChannelMedia": {}, + "volumeSetting": "Volume", + "@volumeSetting": {}, + "risingVolumeSetting": "Alzare il volume", + "@risingVolumeSetting": {}, + "snoozeEnableSetting": "Abilitare", + "@snoozeEnableSetting": {}, + "snoozeLengthSetting": "Durata", + "@snoozeLengthSetting": {}, + "snoozeSettingGroup": "Posponi", + "@snoozeSettingGroup": {}, + "settings": "Impostazioni", + "@settings": {}, + "tasksSetting": "Compiti", + "@tasksSetting": {}, + "noItemMessage": "Non ci sono ancora {items} aggiunti", + "@noItemMessage": {}, + "chooseTaskTitle": "Scegli compiti da aggiungere", + "@chooseTaskTitle": {}, + "mathTask": "Problemi matematici", + "@mathTask": {}, + "mathEasyDifficulty": "Facile (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Medio (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Difficile (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Molto difficile (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Ripetere il testo", + "@retypeTask": {}, + "mathTaskDifficultySetting": "Difficoltà", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Numero di caratteri", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Includere numeri", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Includere minuscole", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Largo della sequenza", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Dimensione della griglia", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Numero di problemi", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Vuoi uscire senza salvare?", + "@saveReminderAlert": {}, + "yesButton": "Si", + "@yesButton": {}, + "noButton": "No", + "@noButton": {}, + "noAlarmMessage": "Nessun allarme creata", + "@noAlarmMessage": {}, + "noTimerMessage": "Nessun timer creato", + "@noTimerMessage": {}, + "noTagsMessage": "Nessun etichetta creata", + "@noTagsMessage": {}, + "scheduleTypeField": "Tipo", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Una volta", + "@scheduleTypeOnce": {}, + "scheduleTypeOnceDescription": "Suonerà alla prossima ora fissata", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Suonerà ogni giorno", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "In giorni specifici della settimana", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Si ripeterà in giorni specifici della settimana", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "In date specifiche", + "@scheduleTypeDate": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Non è possibile disattivare l'allarme essendo stata sospesa", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "timePickerModeButton": "Modo", + "@timePickerModeButton": {}, + "cancelButton": "Cancellare", + "@cancelButton": {}, + "customizeButton": "Customizzare", + "@customizeButton": {}, + "saveButton": "Salvare", + "@saveButton": {}, + "labelField": "Etichettare", + "@labelField": {}, + "labelFieldPlaceholder": "Aggiungi un'etichetta", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Pianificare", + "@alarmScheduleSettingGroup": {}, + "maxSnoozesSetting": "Massimo di ripetizioni", + "@maxSnoozesSetting": {}, + "whileSnoozedSettingGroup": "Mentre posponi", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDeletionSetting": "Evitare eliminazione", + "@snoozePreventDeletionSetting": {}, + "durationPickerSetting": "Selettore di durata", + "@durationPickerSetting": {}, + "swipeActionCardActionDescription": "Trascini il dito a sinistra o a destra nella carta per realizzare azioni", + "@swipeActionCardActionDescription": {}, + "swipActionSwitchTabs": "Cambiare finestre", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Passare da una finestra all'altra", + "@swipeActionSwitchTabsDescription": {}, + "tagsSetting": "Etichette", + "@tagsSetting": {}, + "autoStartSetting": "Inizio automatico", + "@autoStartSetting": {}, + "allowNotificationSetting": "Permettere manualmente tutte le notifiche", + "@allowNotificationSetting": {}, + "colorSchemeAccentSettingGroup": "Accentuare", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeCardSettingGroup": "Carta", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Contorno", + "@colorSchemeOutlineSettingGroup": {}, + "logsSettingGroup": "Registri", + "@logsSettingGroup": {}, + "resetButton": "Ripristinare", + "@resetButton": {}, + "cardLabel": "Carta", + "@cardLabel": {}, + "materialBrightnessSetting": "Luminosità", + "@materialBrightnessSetting": {}, + "accentLabel": "Accentuare", + "@accentLabel": {}, + "errorLabel": "Errore", + "@errorLabel": {}, + "previewLabel": "Anteprima", + "@previewLabel": {}, + "displaySettingGroup": "Schermo", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "Fiducia", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Colori", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Stile", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "Utilizzare Material You", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSystem": "Sistema", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Chiaro", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Scuro", + "@materialBrightnessDark": {}, + "scheduleTypeDaily": "Ogni giorno", + "@scheduleTypeDaily": {}, + "showSortSetting": "Mostra ordinati", + "@showSortSetting": {}, + "timerDefaultSettingGroupDescription": "Stabilire valori determinati per nuovi temporizzatori", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Filtri", + "@filtersSettingGroup": {}, + "notificationsSettingGroup": "Notifiche", + "@notificationsSettingGroup": {}, + "dateFilterGroup": "Data", + "@dateFilterGroup": {}, + "activeFilter": "Attiva", + "@activeFilter": {}, + "createdDateFilterGroup": "Data di creazione", + "@createdDateFilterGroup": {}, + "stateFilterGroup": "Stato", + "@stateFilterGroup": {}, + "inactiveFilter": "Inattivo", + "@inactiveFilter": {}, + "completedFilter": "Completo", + "@completedFilter": {}, + "pausedTimerFilter": "Pausato", + "@pausedTimerFilter": {}, + "snoozedFilter": "Posposto", + "@snoozedFilter": {}, + "skipAllFilteredAlarmsAction": "Omettere tutte le allarmi filtrate", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Cancellare l'omissione delle allarmi filtrate", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Eliminare tutti gli elementi filtrati", + "@deleteAllFilteredAction": {}, + "showFiltersSetting": "Mostra Filtri", + "@showFiltersSetting": {}, + "alarmIntervalDaily": "Ogni giorno", + "@alarmIntervalDaily": {}, + "timeToFullVolumeSetting": "Tempo per il volume massimo", + "@timeToFullVolumeSetting": {}, + "noStopwatchMessage": "Non sono stati creati cronometri", + "@noStopwatchMessage": {}, + "noTaskMessage": "Non ci sono compiti creati", + "@noTaskMessage": {}, + "noPresetsMessage": "Configurazioni predefinite non ancora create", + "@noPresetsMessage": {}, + "noLogsMessage": "Nessun registro d'allarme", + "@noLogsMessage": {}, + "deleteButton": "Eliminare", + "@deleteButton": {}, + "duplicateButton": "Duplicare", + "@duplicateButton": {}, + "skipAlarmButton": "Ignora il prossimo allarme", + "@skipAlarmButton": {}, + "dismissAlarmButton": "Scartare", + "@dismissAlarmButton": {}, + "allFilter": "Tutte", + "@allFilter": {}, + "scheduleDateFilterGroup": "Data programmata", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Tipo", + "@logTypeFilterGroup": {}, + "todayFilter": "Oggi", + "@todayFilter": {}, + "tomorrowFilter": "Domani", + "@tomorrowFilter": {}, + "disabledFilter": "Disabilitato", + "@disabledFilter": {}, + "runningTimerFilter": "In Esecuzione", + "@runningTimerFilter": {}, + "nameDesc": "Nome Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Le prime ore per prima", + "@timeOfDayAsc": {}, + "disableAllFilteredAlarmsAction": "Disabilitare tutte le allarmi filtrate", + "@disableAllFilteredAlarmsAction": {}, + "alarmDescriptionWeekly": "Ogni {days}", + "@alarmDescriptionWeekly": {}, + "alarmDescriptionRange": "{interval, select, daily{Diariamente} weekly{Settimanalmente} other{Altri}} da {startDate} a {endDate}", + "@alarmDescriptionRange": {}, + "alarmDescriptionSnooze": "Posposto fino a{date}", + "@alarmDescriptionSnooze": {}, + "stopwatchSlowest": "Più lenta", + "@stopwatchSlowest": {}, + "alarmDescriptionDates": "Il {date}{count, plural, =0{} =1{e 1 data in più} other{ e {count} altre date}}", + "@alarmDescriptionDates": {}, + "defaultSettingGroup": "Impostazioni per difetto", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Stabilire valori predeterminati per nuove allarme", + "@alarmsDefaultSettingGroupDescription": {}, + "showUpcomingAlarmNotificationSetting": "Visualizza prossime notifiche di allarme", + "@showUpcomingAlarmNotificationSetting": {}, + "horizontalAlignmentSetting": "Allineamento orizzontale", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Allineamento verticale", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Sopra", + "@alignmentTop": {}, + "alignmentBottom": "Sotto", + "@alignmentBottom": {}, + "alignmentLeft": "Sinistra", + "@alignmentLeft": {}, + "alignmentCenter": "Centro", + "@alignmentCenter": {}, + "alignmentRight": "Destra", + "@alignmentRight": {}, + "alignmentJustify": "Giustificare", + "@alignmentJustify": {}, + "fontWeightSetting": "Spessore lettere", + "@fontWeightSetting": {}, + "dateSettingGroup": "Data", + "@dateSettingGroup": {}, + "timeSettingGroup": "Tempo", + "@timeSettingGroup": {}, + "sizeSetting": "Grandezza", + "@sizeSetting": {}, + "defaultPageSetting": "Etichetta predeterminata", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Modo AM/PM", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Modifica i Preset", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Primo giorno della settimana", + "@firstDayOfWeekSetting": {}, + "translateLink": "Traduci", + "@translateLink": {}, + "translateDescription": "Aiuta a tradurre l'app", + "@translateDescription": {}, + "separatorSetting": "Separatore", + "@separatorSetting": {}, + "editTagLabel": "Modifica Etichetta", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nome dell'etichetta", + "@tagNamePlaceholder": {}, + "stoppedTimerFilter": "Fermato", + "@stoppedTimerFilter": {}, + "durationAsc": "Più corto", + "@durationAsc": {}, + "durationDesc": "Più lungo", + "@durationDesc": {}, + "nameAsc": "Nome A-Z", + "@nameAsc": {}, + "sortGroup": "Ordinare", + "@sortGroup": {}, + "defaultLabel": "Per difetto", + "@defaultLabel": {}, + "remainingTimeDesc": "Poco tempo rimasto", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Più tempo rimasto", + "@remainingTimeAsc": {}, + "filterActions": "Filtrare azioni", + "@filterActions": {}, + "clearFiltersAction": "Pulire tutti i filtri", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Abilitare tutte le allarmi filtrate", + "@enableAllFilteredAlarmsAction": {}, + "pickerSpinner": "Spinner", + "@pickerSpinner": {}, + "pickerRings": "Suonerie", + "@pickerRings": {}, + "swipActionCardAction": "Azioni con carte", + "@swipActionCardAction": {}, + "skippingDescriptionSuffix": "(omettere il prossimo allarme)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionFinished": "Non ci sono prossime date", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Non programmato", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "Solo per oggi", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "Solo domani", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Tutti i giorni", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Ogni fine settimana", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Precedente", + "@stopwatchPrevious": {}, + "alarmDescriptionWeekday": "Ogni giorno lavorativo della settimana", + "@alarmDescriptionWeekday": {}, + "stopwatchFastest": "Più rapida", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "Tutti i{days}", + "@alarmDescriptionDays": {}, + "digitalClockSettingGroup": "Orologio digitale", + "@digitalClockSettingGroup": {}, + "textSettingGroup": "Testo", + "@textSettingGroup": {}, + "settingsTitle": "Impostazioni", + "@settingsTitle": {}, + "showDateSetting": "Mostrare Data", + "@showDateSetting": {}, + "timeOfDayDesc": "Le ultime ore per prima", + "@timeOfDayDesc": {}, + "stopwatchAverage": "Promedio", + "@stopwatchAverage": {}, + "cancelSkipAlarmButton": "Cancella ignorare", + "@cancelSkipAlarmButton": {}, + "upcomingLeadTimeSetting": "Configurare notificazione previa", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Mostra notifiche rinviate", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Visualizza notificazioni", + "@showNotificationSetting": {}, + "presetsSetting": "Configurazioni prestabilite", + "@presetsSetting": {}, + "newPresetPlaceholder": "Nuove configurazioni", + "@newPresetPlaceholder": {}, + "dismissActionSetting": "Scartare tipo di azione", + "@dismissActionSetting": {}, + "dismissActionSlide": "Scorrere", + "@dismissActionSlide": {}, + "dismissActionButtons": "Tasti", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Tasti di area", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Formato dell'ora", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Visualizza millisecondi", + "@stopwatchShowMillisecondsSetting": {}, + "showAverageLapSetting": "Visualizza giro promedio", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Visualizza il giro più lento", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Modo per mancini", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Esportare", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Esportare configurazioni a un file locale", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Importare", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importare configurazioni da un file locale", + "@importSettingsSettingDescription": {}, + "versionLabel": "Versione", + "@versionLabel": {}, + "packageNameLabel": "Nome del pacchetto", + "@packageNameLabel": {}, + "licenseLabel": "licenza", + "@licenseLabel": {}, + "emailLabel": "direzione di posta elettronica", + "@emailLabel": {}, + "viewOnGithubLabel": "Visualizza in GitHub", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Licenza Open Source", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Collaboratori", + "@contributorsSetting": {}, + "addLengthSetting": "Aggiungi durata", + "@addLengthSetting": {}, + "showFastestLapSetting": "Visualizza il giro più veloce", + "@showFastestLapSetting": {}, + "donorsSetting": "Donatori", + "@donorsSetting": {}, + "donateButton": "Donare", + "@donateButton": {}, + "sundayLetter": "D", + "@sundayLetter": {}, + "tuesdayShort": "Mar", + "@tuesdayShort": {}, + "sundayFull": "Domenica", + "@sundayFull": {}, + "thursdayLetter": "G", + "@thursdayLetter": {}, + "fridayLetter": "V", + "@fridayLetter": {}, + "wednesdayShort": "Mer", + "@wednesdayShort": {}, + "thursdayShort": "Giov", + "@thursdayShort": {}, + "fridayShort": "Ven", + "@fridayShort": {}, + "saturdayShort": "Sab", + "@saturdayShort": {}, + "sundayShort": "Dom", + "@sundayShort": {}, + "mondayLetter": "L", + "@mondayLetter": {}, + "tuesdayLetter": "M", + "@tuesdayLetter": {}, + "wednesdayLetter": "M", + "@wednesdayLetter": {}, + "donateDescription": "Fai una donazione per supportare lo sviluppo dell'app", + "@donateDescription": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "donorsDescription": "I nostri donatori", + "@donorsDescription": {}, + "showPreviousLapSetting": "Mostra barra precedente", + "@showPreviousLapSetting": {}, + "cityAlreadyInFavorites": "Questa città è già presente tra i tuoi preferiti", + "@cityAlreadyInFavorites": {}, + "editButton": "Modifica", + "@editButton": {}, + "saturdayFull": "Sabato", + "@saturdayFull": {}, + "mondayShort": "Lun", + "@mondayShort": {}, + "contributorsDescription": "Chi rende questa app possibile", + "@contributorsDescription": {}, + "sameTime": "Stessa ora", + "@sameTime": {}, + "relativeTime": "{hours}ore {relative, select, ahead{in avanti} behind{indietro} other{altro}}", + "@relativeTime": {}, + "searchCityPlaceholder": "Cerca città", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Scegli durata", + "@durationPickerTitle": {}, + "elapsedTime": "Tempo passato", + "@elapsedTime": {}, + "mondayFull": "Lunedì", + "@mondayFull": {}, + "tuesdayFull": "Martedì", + "@tuesdayFull": {}, + "wednesdayFull": "Mercoledì", + "@wednesdayFull": {}, + "thursdayFull": "Giovedì", + "@thursdayFull": {}, + "fridayFull": "Venerdì", + "@fridayFull": {}, + "widgetsSettingGroup": "Widgets", + "@widgetsSettingGroup": {}, + "layoutSettingGroup": "Disposizione", + "@layoutSettingGroup": {}, + "searchSettingPlaceholder": "Cerca impostazione", + "@searchSettingPlaceholder": {}, + "combinedTime": "{hours} e {minutes}", + "@combinedTime": {}, + "alarmRingInMessage": "L'allarme suonerà tra {duration}", + "@alarmRingInMessage": {}, + "showForegroundNotification": "Mostra Notifica in Primo Piano", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Mostra una notifica persistente per mantenere in esecuzione l'app", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Consenti la visualizzazione delle notifiche", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Mostra animazioni che non sono ottimizzate e potrebbero causare perdita di frame nei dispositivi di fascia bassa", + "@extraAnimationSettingDescription": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "noLapsMessage": "Nessun giro", + "@noLapsMessage": {}, + "nextAlarmIn": "Prossimo: {duration}", + "@nextAlarmIn": {}, + "showNextAlarm": "Mostra il Prossimo Allarme", + "@showNextAlarm": {}, + "comparisonLapBarsSettingGroup": "Barre di confronto dei giri", + "@comparisonLapBarsSettingGroup": {}, + "lessThanOneMinute": "meno di 1 minuto", + "@lessThanOneMinute": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}min", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {} } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb new file mode 100644 index 00000000..46e9d7eb --- /dev/null +++ b/lib/l10n/app_nl.arb @@ -0,0 +1,112 @@ +{ + "timerTitle": "Timer", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "generalSettingGroupDescription": "App-brede instellingen zoals tijdsformaat", + "@generalSettingGroupDescription": {}, + "longDateFormatSetting": "Lange datumformaat", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Tijdsformaat", + "@timeFormatSetting": {}, + "timeFormat12": "12 uur", + "@timeFormat12": {}, + "timeFormat24": "24 uur", + "@timeFormat24": {}, + "pickerInput": "Invoerveld", + "@pickerInput": {}, + "timePickerSetting": "Tijdsselectie", + "@timePickerSetting": {}, + "pickerDial": "Wijzerplaat", + "@pickerDial": {}, + "durationPickerSetting": "Duurselectie", + "@durationPickerSetting": {}, + "pickerRings": "Ringen", + "@pickerRings": {}, + "swipeActionSetting": "Swipeactie", + "@swipeActionSetting": {}, + "swipActionCardAction": "Kaartacties", + "@swipActionCardAction": {}, + "swipeActionCardActionDescription": "Swipe naar links of rechts op de kaart om acties uit te voeren", + "@swipeActionCardActionDescription": {}, + "swipeActionSwitchTabsDescription": "Swipe tussen tabbladen", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Melodieën", + "@melodiesSetting": {}, + "vendorSetting": "Fabrikantsinstellingen", + "@vendorSetting": {}, + "allowNotificationSetting": "Alle notificaties handmatig toestaan", + "@allowNotificationSetting": {}, + "permissionsSettingGroup": "Rechten", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Batterijoptimalisaties negeren", + "@ignoreBatteryOptimizationSetting": {}, + "clockTitle": "Klok", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "languageSetting": "Taal", + "@languageSetting": {}, + "alarmTitle": "Wekker", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "stopwatchTitle": "Stopwatch", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "system": "Systeem", + "@system": {}, + "generalSettingGroup": "Algemeen", + "@generalSettingGroup": {}, + "dateFormatSetting": "Datumformaat", + "@dateFormatSetting": {}, + "timeFormatDevice": "Apparaatinstellingen", + "@timeFormatDevice": {}, + "swipActionSwitchTabs": "Wissel tabbladen", + "@swipActionSwitchTabs": {}, + "showSecondsSetting": "Toon seconden", + "@showSecondsSetting": {}, + "pickerSpinner": "Spinner", + "@pickerSpinner": {}, + "tagsSetting": "Tags", + "@tagsSetting": {}, + "vendorSettingDescription": "Fabrikantsspecifieke optimalisaties handmatig uitschakelen", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Batterijoptimalisaties handmatig uitschakelen", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Batterijoptimalisaties uitschakelen voor deze app om vertraging van de wekker te voorkomen", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Meldingen op het vergrendelscherm voor wekkers en timers toestaan", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Sommige toestellen hebben Autostart nodig om wekkers af te laten gaan terwijl de app gesloten is", + "@autoStartSettingDescription": {}, + "autoStartSetting": "Autostart", + "@autoStartSetting": {}, + "animationSettingGroup": "Animaties", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Animatie Snelheid", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Extra Animaties", + "@extraAnimationSetting": {}, + "colorSchemeAccentSettingGroup": "Accent", + "@colorSchemeAccentSettingGroup": {}, + "appearanceSettingGroup": "Uiterlijk", + "@appearanceSettingGroup": {}, + "appearanceSettingGroupDescription": "Thema's, kleuren en lay-out wijzigen", + "@appearanceSettingGroupDescription": {}, + "nameField": "Naam", + "@nameField": {}, + "colorSetting": "Kleur", + "@colorSetting": {}, + "textColorSetting": "Tekst", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Kleuren schema", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Achtergrond", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Fout", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Kaart", + "@colorSchemeCardSettingGroup": {} +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 951757b9..9240194f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -3,11 +3,11 @@ "@restoreSettingGroup": {}, "colorsSettingGroup": "Kolory", "@colorsSettingGroup": {}, - "styleSettingGroup": "Styl", + "styleSettingGroup": "Style", "@styleSettingGroup": {}, "useMaterialYouColorSetting": "Użyj Material You", "@useMaterialYouColorSetting": {}, - "materialBrightnessSetting": "Jasność", + "materialBrightnessSetting": "Motyw", "@materialBrightnessSetting": {}, "colorSchemeSetting": "Paleta kolorów", "@colorSchemeSetting": {}, @@ -51,7 +51,7 @@ "@backupSettingGroup": {}, "developerOptionsSettingGroup": "Opcje programistyczne", "@developerOptionsSettingGroup": {}, - "styleThemeSetting": "Motyw", + "styleThemeSetting": "Style motywu", "@styleThemeSetting": {}, "clockSettingGroup": "Zegar", "@clockSettingGroup": {}, @@ -101,9 +101,9 @@ "@todayFilter": {}, "tomorrowFilter": "Jutro", "@tomorrowFilter": {}, - "disabledFilter": "Wyłączono", + "disabledFilter": "Wyłączony", "@disabledFilter": {}, - "completedFilter": "Zakończone", + "completedFilter": "Zakończony", "@completedFilter": {}, "timeFormatDevice": "Ustawienia urządzenia", "@timeFormatDevice": {}, @@ -115,7 +115,7 @@ "@ignoreBatteryOptimizationSetting": {}, "notificationPermissionSetting": "Uprawnienia powiadomień", "@notificationPermissionSetting": {}, - "notificationPermissionAlreadyGranted": "Uprawnienia na powiadomenia już zostały nadane", + "notificationPermissionAlreadyGranted": "Uprawnienia na powiadomienia zostały przyznane", "@notificationPermissionAlreadyGranted": {}, "ignoreBatteryOptimizationAlreadyGranted": "Optymalizacje baterii zostały już wyłączone", "@ignoreBatteryOptimizationAlreadyGranted": {}, @@ -137,13 +137,13 @@ "@timeFormatSetting": {}, "timeFormat12": "12 godzinny", "@timeFormat12": {}, - "batteryOptimizationSetting": "Wyłącz optymalizacje baterii", + "batteryOptimizationSetting": "Wyłączenie optymalizacje baterii", "@batteryOptimizationSetting": {}, - "batteryOptimizationSettingDescription": "Wyłącz optymalizacje tej aplikacji aby alarmy nie były opóźnione", + "batteryOptimizationSettingDescription": "Wyłącz optymalizacje baterii dla tej aplikacji, aby alarmy nie były opóźnione", "@batteryOptimizationSettingDescription": {}, - "allowNotificationSettingDescription": "Zezwół na alarmy na blokadzie ekrany", + "allowNotificationSettingDescription": "Zezwalaj na powiadomienia na ekranie blokady dla alarmów i czasomierzy", "@allowNotificationSettingDescription": {}, - "allowNotificationSetting": "Zezwól na powiadomienia manualnie", + "allowNotificationSetting": "Zezwól na wszystkie powiadomienia", "@allowNotificationSetting": {}, "autoStartSetting": "Automatyczne uruchamianie", "@autoStartSetting": {}, @@ -152,5 +152,491 @@ "animationSpeedSetting": "Szybkość animacji", "@animationSpeedSetting": {}, "extraAnimationSetting": "Dodatkowe animacje", - "@extraAnimationSetting": {} + "@extraAnimationSetting": {}, + "generalSettingGroupDescription": "Ustawienia aplikacji, takie jak format czasu", + "@generalSettingGroupDescription": {}, + "longDateFormatSetting": "Długi format daty", + "@longDateFormatSetting": {}, + "timePickerSetting": "Selektor czasu", + "@timePickerSetting": {}, + "pickerDial": "Tarcza", + "@pickerDial": {}, + "pickerInput": "Wprowadzanie", + "@pickerInput": {}, + "pickerRings": "Dzwonki", + "@pickerRings": {}, + "noTaskMessage": "Nie utworzono zadań", + "@noTaskMessage": {}, + "vendorSetting": "Ustawienia producenta", + "@vendorSetting": {}, + "melodiesSetting": "Melodie", + "@melodiesSetting": {}, + "autoStartSettingDescription": "Niektóre urządzenia wymagają włączenia funkcji auto-startu dla alarmów, podczas gdy aplikacja jest zamknięta", + "@autoStartSettingDescription": {}, + "colorSchemeBackgroundSettingGroup": "Tło", + "@colorSchemeBackgroundSettingGroup": {}, + "nameField": "Nazwa", + "@nameField": {}, + "colorSetting": "Kolor", + "@colorSetting": {}, + "appearanceSettingGroupDescription": "Ustawiaj motywy, kolory i zmieniaj układ", + "@appearanceSettingGroupDescription": {}, + "textColorSetting": "tekst", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "schemat kolorów", + "@colorSchemeNamePlaceholder": {}, + "tagsSetting": "znaczniki", + "@tagsSetting": {}, + "vendorSettingDescription": "Ręcznie wyłącz optymalizacje specyficzne dla danego producenta urządzenia", + "@vendorSettingDescription": {}, + "pickerSpinner": "Obrotomierz", + "@pickerSpinner": {}, + "durationPickerSetting": "Wybór czasu trwania", + "@durationPickerSetting": {}, + "swipActionCardAction": "Akcje sekcji", + "@swipActionCardAction": {}, + "swipeActionCardActionDescription": "Przesuń w lewo lub w prawo na sekcji, aby wykonać czynności", + "@swipeActionCardActionDescription": {}, + "swipActionSwitchTabs": "Przełącz karty", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Przesuwaj między kartami", + "@swipeActionSwitchTabsDescription": {}, + "showIstantAlarmButtonSetting": "Pokaż przycisk natychmiastowego alarmu", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Pokaż przycisk natychmiastowego czasomierza", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Logi", + "@logsSettingGroup": {}, + "maxLogsSetting": "Maksymalna liczba logów", + "@maxLogsSetting": {}, + "alarmLogSetting": "Logi alarmów", + "@alarmLogSetting": {}, + "resetButton": "Resetuj", + "@resetButton": {}, + "previewLabel": "Podgląd", + "@previewLabel": {}, + "cardLabel": "Sekcja", + "@cardLabel": {}, + "accentLabel": "Akcent", + "@accentLabel": {}, + "materialBrightnessLight": "Jasny", + "@materialBrightnessLight": {}, + "materialBrightnessSystem": "Systemowy", + "@materialBrightnessSystem": {}, + "materialBrightnessDark": "Ciemny", + "@materialBrightnessDark": {}, + "accentColorSetting": "Kolor akcentu", + "@accentColorSetting": {}, + "errorLabel": "Błąd", + "@errorLabel": {}, + "alarmWeekdaysSetting": "Dni tygodnia", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Daty", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Zakres dat", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Interwał", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Codziennie", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Usuń po zakończeniu", + "@alarmDeleteAfterFinishingSetting": {}, + "alarmDeleteAfterRingingSetting": "Usuń po odrzuceniu", + "@alarmDeleteAfterRingingSetting": {}, + "alarmIntervalWeekly": "Co tydzień", + "@alarmIntervalWeekly": {}, + "mathVeryHardDifficulty": "Bardzo trudny (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "sequenceTask": "Sekwencja", + "@sequenceTask": {}, + "taskTryButton": "Wypróbuj", + "@taskTryButton": {}, + "retypeNumberChars": "Liczba znaków", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Uwzględnij liczby", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Uwzględnij małe litery", + "@retypeLowercaseSetting": {}, + "numberOfProblemsSetting": "Liczba działań", + "@numberOfProblemsSetting": {}, + "noAlarmMessage": "Nie utworzono żadnych alarmów", + "@noAlarmMessage": {}, + "noTimerMessage": "Nie utworzono żadnych czasomierzy", + "@noTimerMessage": {}, + "noTagsMessage": "Nie utworzono żadnych znaczników", + "@noTagsMessage": {}, + "noLogsMessage": "Brak logów alarmów", + "@noLogsMessage": {}, + "noStopwatchMessage": "Nie utworzono żadnych stoperów", + "@noStopwatchMessage": {}, + "noPresetsMessage": "Nie utworzono ustawień wstępnych", + "@noPresetsMessage": {}, + "cancelSkipAlarmButton": "Anuluj pominięcie", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Odrzuć", + "@dismissAlarmButton": {}, + "scheduleDateFilterGroup": "Data zaplanowania", + "@scheduleDateFilterGroup": {}, + "nameAsc": "Etykieta A-Z", + "@nameAsc": {}, + "nameDesc": "Etykieta Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Najpierw wczesne godziny", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Najpierw późne godziny", + "@timeOfDayDesc": {}, + "filterActions": "Działania filtra", + "@filterActions": {}, + "clearFiltersAction": "Wyczyść wszystkie filtry", + "@clearFiltersAction": {}, + "emailLabel": "E-mail", + "@emailLabel": {}, + "viewOnGithubLabel": "Zobacz na GitHubie", + "@viewOnGithubLabel": {}, + "thursdayShort": "Czw", + "@thursdayShort": {}, + "fridayShort": "Pt", + "@fridayShort": {}, + "saturdayShort": "Sob", + "@saturdayShort": {}, + "sundayShort": "Ndz", + "@sundayShort": {}, + "mondayLetter": "P", + "@mondayLetter": {}, + "tuesdayLetter": "W", + "@tuesdayLetter": {}, + "wednesdayLetter": "Ś", + "@wednesdayLetter": {}, + "thursdayLetter": "C", + "@thursdayLetter": {}, + "fridayLetter": "P", + "@fridayLetter": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "donateDescription": "Przekaż darowiznę, aby wesprzeć rozwój aplikacji", + "@donateDescription": {}, + "donorsDescription": "Nasi hojni patroni", + "@donorsDescription": {}, + "contributorsDescription": "Ludzie, dzięki którym ta aplikacja jest tworzona", + "@contributorsDescription": {}, + "snoozePreventDeletionSetting": "Zapobiegaj usuwaniu", + "@snoozePreventDeletionSetting": {}, + "mathEasyDifficulty": "Łatwy (X + Y)", + "@mathEasyDifficulty": {}, + "mathHardDifficulty": "Trudny (X × Y + Z)", + "@mathHardDifficulty": {}, + "activeFilter": "Aktywny", + "@activeFilter": {}, + "inactiveFilter": "Nieaktywny", + "@inactiveFilter": {}, + "createdDateFilterGroup": "Data utworzenia", + "@createdDateFilterGroup": {}, + "runningTimerFilter": "Uruchomiony", + "@runningTimerFilter": {}, + "logTypeFilterGroup": "Typ", + "@logTypeFilterGroup": {}, + "pausedTimerFilter": "Wstrzymany", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Zatrzymany", + "@stoppedTimerFilter": {}, + "stateFilterGroup": "Stan", + "@stateFilterGroup": {}, + "sortGroup": "Sortowanie", + "@sortGroup": {}, + "defaultLabel": "Donyślne", + "@defaultLabel": {}, + "remainingTimeDesc": "Najkrótszy pozostały czas", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Najdłuższy pozostały czas", + "@remainingTimeAsc": {}, + "durationAsc": "Najkrótszy", + "@durationAsc": {}, + "durationDesc": "Najdłuższy", + "@durationDesc": {}, + "skipAllFilteredAlarmsAction": "Pomiń wszystkie przefiltrowane alarmy", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Anuluj pominięcie wszystkich przefiltrowanych alarmów", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Usuń wszystkie przefiltrowane elementy", + "@deleteAllFilteredAction": {}, + "alarmDescriptionWeekly": "Każdy {days}", + "@alarmDescriptionWeekly": {}, + "versionLabel": "Wersja", + "@versionLabel": {}, + "packageNameLabel": "Nazwa pakietu", + "@packageNameLabel": {}, + "licenseLabel": "Licencja", + "@licenseLabel": {}, + "importSettingsSetting": "Importuj", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importuj ustawienia z pliku zapisanego lokalnie", + "@importSettingsSettingDescription": {}, + "noLapsMessage": "Brak okrążeń", + "@noLapsMessage": {}, + "elapsedTime": "Łączny czas, który upłynął", + "@elapsedTime": {}, + "tuesdayFull": "Wtorek", + "@tuesdayFull": {}, + "wednesdayFull": "Środa", + "@wednesdayFull": {}, + "sundayLetter": "N", + "@sundayLetter": {}, + "saturdayFull": "Sobota", + "@saturdayFull": {}, + "mondayShort": "Pon", + "@mondayShort": {}, + "styleThemeShapeSettingGroup": "Kształt", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Wysokość", + "@styleThemeElevationSetting": {}, + "maxSnoozesSetting": "Maksymalna ilość drzemek", + "@maxSnoozesSetting": {}, + "mathMediumDifficulty": "Średni (X × Y)", + "@mathMediumDifficulty": {}, + "dateFilterGroup": "Data", + "@dateFilterGroup": {}, + "enableAllFilteredAlarmsAction": "Włącz wszystkie przefiltrowane alarmy", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Wyłącz wszystkie przefiltrowane alarmy", + "@disableAllFilteredAlarmsAction": {}, + "openSourceLicensesSetting": "Licencje Open Source", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Współtwórcy", + "@contributorsSetting": {}, + "donorsSetting": "Darczyńcy", + "@donorsSetting": {}, + "donateButton": "Przekaż darowiznę", + "@donateButton": {}, + "searchSettingPlaceholder": "Wyszukaj ustawienie", + "@searchSettingPlaceholder": {}, + "cityAlreadyInFavorites": "To miasto jest już na liście ulubionych", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Wybierz czas trwania", + "@durationPickerTitle": {}, + "thursdayFull": "Czwartek", + "@thursdayFull": {}, + "tuesdayShort": "Wt", + "@tuesdayShort": {}, + "wednesdayShort": "Śr", + "@wednesdayShort": {}, + "searchCityPlaceholder": "Wyszukaj miasto", + "@searchCityPlaceholder": {}, + "alarmDescriptionEveryDay": "Codziennie", + "@alarmDescriptionEveryDay": {}, + "editButton": "Edytuj", + "@editButton": {}, + "mondayFull": "Poniedziałek", + "@mondayFull": {}, + "fridayFull": "Piątek", + "@fridayFull": {}, + "sundayFull": "Niedziela", + "@sundayFull": {}, + "reliabilitySettingGroup": "Niezawodność", + "@reliabilitySettingGroup": {}, + "displaySettingGroup": "Wyświetlanie", + "@displaySettingGroup": {}, + "systemDarkModeSetting": "Systemowy tryb ciemny", + "@systemDarkModeSetting": {}, + "stopwatchSettingGroup": "Stoper", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Eksportuj lub importuj swoje ustawienia lokalnie", + "@backupSettingGroupDescription": {}, + "saveButton": "Zapisz", + "@saveButton": {}, + "labelFieldPlaceholder": "Dodaj etykietę", + "@labelFieldPlaceholder": {}, + "scheduleTypeWeek": "W określone dni tygodnia", + "@scheduleTypeWeek": {}, + "scheduleTypeRange": "Zakres dni", + "@scheduleTypeRange": {}, + "snoozeSettingGroup": "Drzemka", + "@snoozeSettingGroup": {}, + "whileSnoozedSettingGroup": "Podczas drzemki", + "@whileSnoozedSettingGroup": {}, + "soundSettingGroup": "Dźwięk", + "@soundSettingGroup": {}, + "snoozePreventDisablingSetting": "Zapobiegaj wyłączaniu", + "@snoozePreventDisablingSetting": {}, + "mathTask": "Działania matematyczne", + "@mathTask": {}, + "retypeTask": "Przepisywanie tekstu", + "@retypeTask": {}, + "audioChannelSetting": "Kanał audio", + "@audioChannelSetting": {}, + "scheduleTypeDateDescription": "Będzie powtarzany w określonych dniach", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Będzie powtarzany w określonym zakresie dni", + "@scheduleTypeRangeDescription": {}, + "risingVolumeSetting": "Rosnąca głośność", + "@risingVolumeSetting": {}, + "chooseTaskTitle": "Wybierz Zadanie do dodania", + "@chooseTaskTitle": {}, + "skipAlarmButton": "Pomiń następny alarm", + "@skipAlarmButton": {}, + "snoozedFilter": "Ustawiona drzemka", + "@snoozedFilter": {}, + "skippingDescriptionSuffix": "(pominięcie następnego zdarzenia)", + "@skippingDescriptionSuffix": {}, + "sequenceLengthSetting": "Długość sekwencji", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Rozmiar siatki", + "@sequenceGridSizeSetting": {}, + "saveReminderAlert": "Czy chcesz wyjść bez zapisywania?", + "@saveReminderAlert": {}, + "alarmDescriptionFinished": "Brak przyszłych terminów", + "@alarmDescriptionFinished": {}, + "alarmDescriptionSnooze": "Drzemka do {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionNotScheduled": "Nie zaplanowano", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionTomorrow": "Tylko jutro", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionToday": "Tylko dzisiaj", + "@alarmDescriptionToday": {}, + "alarmDescriptionDays": "{days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Codziennie} weekly{Co tydzień} other{Inne}} od {startDate} do {endDate}", + "@alarmDescriptionRange": {}, + "alarmDescriptionWeekend": "W każdy weekend", + "@alarmDescriptionWeekend": {}, + "colorSchemeAccentSettingGroup": "Akcent", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Błąd", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Kontur", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Cień", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsShadowSetting": "Używaj koloru akcentu w cieniu", + "@colorSchemeUseAccentAsShadowSetting": {}, + "colorSchemeUseAccentAsOutlineSetting": "Używaj koloru akcentu w konturze", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeCardSettingGroup": "Sekcja", + "@colorSchemeCardSettingGroup": {}, + "styleThemeNamePlaceholder": "Styl Motywu", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Cień", + "@styleThemeShadowSettingGroup": {}, + "styleThemeBlurSetting": "Rozmycie", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineSettingGroup": "Kontur", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Szerokość", + "@styleThemeOutlineWidthSetting": {}, + "styleThemeRadiusSetting": "Zaokrąglenie narożników", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Krycie", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "Rozpiętość", + "@styleThemeSpreadSetting": {}, + "layoutSettingGroup": "Układ", + "@layoutSettingGroup": {}, + "widgetsSettingGroup": "Widżety", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Zegar cyfrowy", + "@digitalClockSettingGroup": {}, + "showDateSetting": "Pokaż datę", + "@showDateSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Nie można wyłączyć alarmu podczas drzemki", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "scheduleTypeOnceDescription": "Zadzwoni przy następnym nadejściu tej godziny", + "@scheduleTypeOnceDescription": {}, + "timeToFullVolumeSetting": "Czas do osiągnięcia pełnej głośności", + "@timeToFullVolumeSetting": {}, + "timePickerModeButton": "Tryb", + "@timePickerModeButton": {}, + "scheduleTypeDailyDescription": "Zadzwoni każdego dnia", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeekDescription": "Będzie powtarzany w określone dni tygodnia", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "W określonych dniach", + "@scheduleTypeDate": {}, + "soundAndVibrationSettingGroup": "Dźwięk i wibracje", + "@soundAndVibrationSettingGroup": {}, + "audioChannelAlarm": "Alarmy", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Powiadomienie", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Dzwonek", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Multimedia", + "@audioChannelMedia": {}, + "textSettingGroup": "Tekst", + "@textSettingGroup": {}, + "settingsTitle": "Ustawienia", + "@settingsTitle": {}, + "showForegroundNotification": "Pokaż powiadomienie na pierwszym planie", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Wyświetlaj stałe powiadomienie, aby utrzymać działanie aplikacji", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Zezwalaj na wyświetlanie powiadomień", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Wyświetlanie animacji, które nie są dopracowane i mogą powodować spadki klatek na słabszych urządzeniach", + "@extraAnimationSettingDescription": {}, + "notificationsSettingGroup": "Powiadomienia", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Pokazuj powiadomienia o nadchodzących alarmach", + "@showUpcomingAlarmNotificationSetting": {}, + "showSnoozeNotificationSetting": "Pokazuj powiadomienia o drzemce", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Pokaż powiadomienie", + "@showNotificationSetting": {}, + "presetsSetting": "Ustawienia wstępne", + "@presetsSetting": {}, + "dismissActionSetting": "Odrzuć za pomocą", + "@dismissActionSetting": {}, + "dismissActionSlide": "Suwak", + "@dismissActionSlide": {}, + "dismissActionButtons": "Przyciski", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Duże przyciski", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Format czasu", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Pokaż milisekundy", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Porównanie okrążeń", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "Pokaż poprzednie okrążenie", + "@showPreviousLapSetting": {}, + "exportSettingsSetting": "Eksportuj", + "@exportSettingsSetting": {}, + "addLengthSetting": "Przycisk do dodawania czasu trwania", + "@addLengthSetting": {}, + "relativeTime": "{hours}h {relative, select, ahead{do przodu} behind{do tyłu} other{Inne}}", + "@relativeTime": {}, + "defaultSettingGroup": "Ustawienia domyślne", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Ustaw wartości domyślne dla nowych alarmów", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Ustaw wartości domyślne dla nowych czasomierzy", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Filtry", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Pokaż filtry", + "@showFiltersSetting": {}, + "showSortSetting": "Pokaż sortowanie", + "@showSortSetting": {}, + "exportSettingsSettingDescription": "Eksportuj ustawienia do pliku lokalnego", + "@exportSettingsSettingDescription": {}, + "sameTime": "Ten sam czas", + "@sameTime": {}, + "newPresetPlaceholder": "Nowe ustawienie wstępne", + "@newPresetPlaceholder": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "combinedTime": "{hours} i {minutes}", + "@combinedTime": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "showNextAlarm": "Pokaż następny alarm", + "@showNextAlarm": {}, + "showFastestLapSetting": "Pokaż najszybsze okrążenie", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Pokaż średnią okrążeń", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Pokaż najwolniejsze okrążenie", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Tryb leworęczny", + "@leftHandedSetting": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fe324b06..751545f9 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -337,7 +337,7 @@ "@timePickerModeButton": {}, "scheduleTypeDate": "Em datas específicas", "@scheduleTypeDate": {}, - "soundAndVibrationSettingGroup": "Sim e vibração", + "soundAndVibrationSettingGroup": "Som e vibração", "@soundAndVibrationSettingGroup": {}, "timeToFullVolumeSetting": "Duração para volume total", "@timeToFullVolumeSetting": {}, @@ -650,5 +650,41 @@ "ignoreBatteryOptimizationAlreadyGranted": "Permissão para ignorar otimização de bateria já concedida", "@ignoreBatteryOptimizationAlreadyGranted": {}, "showSnoozeNotificationSetting": "Mostrar notificação para snooze", - "@showSnoozeNotificationSetting": {} + "@showSnoozeNotificationSetting": {}, + "noTagsMessage": "Não existem etiquetas", + "@noTagsMessage": {}, + "separatorSetting": "Separador", + "@separatorSetting": {}, + "editTagLabel": "Editar etiqueta", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nome da etiqueta", + "@tagNamePlaceholder": {}, + "longDateFormatSetting": "Formato longo de data", + "@longDateFormatSetting": {}, + "showForegroundNotification": "Mostrar notificação de primeiro plano", + "@showForegroundNotification": {}, + "notificationPermissionDescription": "Permitir a exibição de notificações", + "@notificationPermissionDescription": {}, + "showForegroundNotificationDescription": "Mostrar uma notificação persistente para manter a aplicação ativa", + "@showForegroundNotificationDescription": {}, + "upcomingLeadTimeSetting": "Próximos prazos de entrega", + "@upcomingLeadTimeSetting": {}, + "editPresetsTitle": "Editar predefinições", + "@editPresetsTitle": {}, + "remainingTimeDesc": "Menos tempo restante", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Mais tempo restante", + "@remainingTimeAsc": {}, + "styleThemeSpreadSetting": "Espalhar", + "@styleThemeSpreadSetting": {}, + "lessThanOneMinute": "menos de 1 minuto", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "O alarme tocará em {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Próximo: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} e {minutes}", + "@combinedTime": {}, + "showNextAlarm": "Mostrar o próximo alarme", + "@showNextAlarm": {} } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b08427d2..eedf2e6b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -29,13 +29,13 @@ "@displaySettingGroup": {}, "colorsSettingGroup": "Цвета", "@colorsSettingGroup": {}, - "useMaterialYouColorSetting": "Использовать Material You дизайн", + "useMaterialYouColorSetting": "Использовать Material You", "@useMaterialYouColorSetting": {}, "overrideAccentSetting": "Изменить цвет акцента", "@overrideAccentSetting": {}, - "materialBrightnessSetting": "Тёмная тема", + "materialBrightnessSetting": "Тема", "@materialBrightnessSetting": {}, - "styleThemeSetting": "Тема", + "styleThemeSetting": "Выбор стиля", "@styleThemeSetting": {}, "useMaterialStyleSetting": "Использовать Material дизайн", "@useMaterialStyleSetting": {}, @@ -61,7 +61,7 @@ "@saveButton": {}, "vibrationSetting": "Вибрация", "@vibrationSetting": {}, - "backupSettingGroup": "Настройки", + "backupSettingGroup": "Резервное копирование", "@backupSettingGroup": {}, "systemDarkModeSetting": "Тёмная тема системы", "@systemDarkModeSetting": {}, @@ -83,11 +83,11 @@ "@showSecondsSetting": {}, "swipeActionCardActionDescription": "Смахните карточку влево или вправо, чтобы выполнить действие", "@swipeActionCardActionDescription": {}, - "batteryOptimizationSetting": "Отключить оптимизацию батареи", + "batteryOptimizationSetting": "Отключить оптимизацию батареи вручную", "@batteryOptimizationSetting": {}, "batteryOptimizationSettingDescription": "Отключить оптимизацию батареи для этого приложения, чтобы избежать откладывания будильника", "@batteryOptimizationSettingDescription": {}, - "allowNotificationSetting": "Разрешить уведомления", + "allowNotificationSetting": "Разрешение всех уведомлений вручную", "@allowNotificationSetting": {}, "autoStartSetting": "Автозапуск", "@autoStartSetting": {}, @@ -101,11 +101,11 @@ "@nameField": {}, "colorSetting": "Цвет", "@colorSetting": {}, - "swipeActionSetting": "Смахивание", + "swipeActionSetting": "Действие свайпа", "@swipeActionSetting": {}, "swipActionCardAction": "Действия карточки", "@swipActionCardAction": {}, - "swipActionSwitchTabs": "Сменить вкладу", + "swipActionSwitchTabs": "Сменить вкладку", "@swipActionSwitchTabs": {}, "melodiesSetting": "Мелодии", "@melodiesSetting": {}, @@ -183,6 +183,608 @@ "@permissionsSettingGroup": {}, "notificationPermissionSetting": "Разрешения уведомлений", "@notificationPermissionSetting": {}, - "pickerInput": "Интерфейс выбора времени, состоящий из полей ввода.", - "@pickerInput": {} + "pickerInput": "Ввод", + "@pickerInput": {}, + "longDateFormatSetting": "Длинный формат даты", + "@longDateFormatSetting": {}, + "pickerRings": "Кольца", + "@pickerRings": {}, + "pickerSpinner": "Спиннер", + "@pickerSpinner": {}, + "durationPickerSetting": "Выбор стиля длительности", + "@durationPickerSetting": {}, + "showIstantAlarmButtonSetting": "Показать кнопку быстрого будильника", + "@showIstantAlarmButtonSetting": {}, + "maxLogsSetting": "Макс. журналов будильников", + "@maxLogsSetting": {}, + "snoozeLengthSetting": "Длина", + "@snoozeLengthSetting": {}, + "alarmDeleteAfterRingingSetting": "Удалить после закрытия", + "@alarmDeleteAfterRingingSetting": {}, + "alarmWeekdaysSetting": "Дни недели", + "@alarmWeekdaysSetting": {}, + "dismissActionAreaButtons": "Кнопки области", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат времени", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Показывать миллисекунды", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Сравнение кругов", + "@comparisonLapBarsSettingGroup": {}, + "saveReminderAlert": "Вы хотите выйти без сохранения?", + "@saveReminderAlert": {}, + "yesButton": "Да", + "@yesButton": {}, + "noStopwatchMessage": "Секундомеры не созданы", + "@noStopwatchMessage": {}, + "noTagsMessage": "Теги не созданы", + "@noTagsMessage": {}, + "alarmsDefaultSettingGroupDescription": "Установить значения по умолчанию для новых будильников", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Установить значения по умолчанию для новых таймеров", + "@timerDefaultSettingGroupDescription": {}, + "alarmDescriptionWeekday": "Каждый рабочий день", + "@alarmDescriptionWeekday": {}, + "stopwatchAverage": "Средний", + "@stopwatchAverage": {}, + "alarmDescriptionWeekly": "Каждые {days}", + "@alarmDescriptionWeekly": {}, + "defaultSettingGroup": "Настройки по умолчанию", + "@defaultSettingGroup": {}, + "deleteAllFilteredAction": "Удалить все отфильтрованные элементы", + "@deleteAllFilteredAction": {}, + "editButton": "Редактировать", + "@editButton": {}, + "noLapsMessage": "Кругов еще нет", + "@noLapsMessage": {}, + "fridayFull": "Пятница", + "@fridayFull": {}, + "sundayFull": "Воскресенье", + "@sundayFull": {}, + "mondayShort": "Пн", + "@mondayShort": {}, + "alignmentCenter": "Центр", + "@alignmentCenter": {}, + "alignmentRight": "Право", + "@alignmentRight": {}, + "alignmentJustify": "По ширине", + "@alignmentJustify": {}, + "translateLink": "Перевести", + "@translateLink": {}, + "translateDescription": "Помогите перевести приложение", + "@translateDescription": {}, + "aboutSettingGroup": "О приложении", + "@aboutSettingGroup": {}, + "reliabilitySettingGroup": "Стабильность", + "@reliabilitySettingGroup": {}, + "styleSettingGroup": "Стиль", + "@styleSettingGroup": {}, + "backupSettingGroupDescription": "Локальный экспорт или импорт настроек", + "@backupSettingGroupDescription": {}, + "selectTime": "Выберите время", + "@selectTime": {}, + "labelField": "Метка", + "@labelField": {}, + "labelFieldPlaceholder": "Добавить метку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Расписание", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Один раз", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Ежедневно", + "@scheduleTypeDaily": {}, + "scheduleTypeDailyDescription": "Будет звонить каждый день", + "@scheduleTypeDailyDescription": {}, + "swipeActionSwitchTabsDescription": "Перелистывание вкладок", + "@swipeActionSwitchTabsDescription": {}, + "styleThemeNamePlaceholder": "Тема стиля", + "@styleThemeNamePlaceholder": {}, + "styleThemeElevationSetting": "Высота", + "@styleThemeElevationSetting": {}, + "styleThemeSpreadSetting": "Размах", + "@styleThemeSpreadSetting": {}, + "showIstantTimerButtonSetting": "Показать кнопку быстрого таймера", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Журналы", + "@logsSettingGroup": {}, + "previewLabel": "Предварительный просмотр", + "@previewLabel": {}, + "alarmDatesSetting": "Даты", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Диапазон дат", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Интервал", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Ежедневно", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Еженедельно", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterFinishingSetting": "Удалить после завершения", + "@alarmDeleteAfterFinishingSetting": {}, + "scheduleTypeOnceDescription": "Зазвонит при следующем наступлении времени", + "@scheduleTypeOnceDescription": {}, + "mathTaskDifficultySetting": "Сложность", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Количество символов", + "@retypeNumberChars": {}, + "noTimerMessage": "Таймеры не созданы", + "@noTimerMessage": {}, + "filterActions": "Фильтр действий", + "@filterActions": {}, + "clearFiltersAction": "Очистить все фильтры", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Включить все отфильтрованные будильники", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Отключить все отфильтрованные будильники", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Пропустить все отфильтрованные будильники", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Отменить пропуск всех отфильтрованных будильников", + "@cancelSkipAllFilteredAlarmsAction": {}, + "skippingDescriptionSuffix": "(пропуск следующего события)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Отложено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionToday": "Только сегодня", + "@alarmDescriptionToday": {}, + "alarmDescriptionEveryDay": "Каждый день", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Каждые выходные", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Предыдущий", + "@stopwatchPrevious": {}, + "dismissActionSetting": "Тип действия при отклонении", + "@dismissActionSetting": {}, + "dismissActionButtons": "Кнопки", + "@dismissActionButtons": {}, + "showPreviousLapSetting": "Показать предыдущий круг", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Показать лучший круг", + "@showFastestLapSetting": {}, + "sameTime": "Такое же время", + "@sameTime": {}, + "searchCityPlaceholder": "Поиск города", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Выбрать длительность", + "@durationPickerTitle": {}, + "elapsedTime": "Прошедшее время", + "@elapsedTime": {}, + "mondayFull": "Понедельник", + "@mondayFull": {}, + "tuesdayFull": "Вторник", + "@tuesdayFull": {}, + "wednesdayFull": "Среда", + "@wednesdayFull": {}, + "sundayShort": "Вс", + "@sundayShort": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "В", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "В", + "@sundayLetter": {}, + "donateDescription": "Пожертвуйте на поддержку разработки приложения", + "@donateDescription": {}, + "donorsDescription": "Наши щедрые патроны", + "@donorsDescription": {}, + "contributorsDescription": "Люди, благодаря которым это приложение стало возможным", + "@contributorsDescription": {}, + "horizontalAlignmentSetting": "Горизонтальное выравнивание", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикальное выравнивание", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Верх", + "@alignmentTop": {}, + "alignmentBottom": "Низ", + "@alignmentBottom": {}, + "alignmentLeft": "Лево", + "@alignmentLeft": {}, + "editPresetsTitle": "Редактировать предустановки", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Первый день недели", + "@firstDayOfWeekSetting": {}, + "separatorSetting": "Разделитель", + "@separatorSetting": {}, + "editTagLabel": "Редактировать тег", + "@editTagLabel": {}, + "tagNamePlaceholder": "Название тега", + "@tagNamePlaceholder": {}, + "scheduleTypeDate": "В определённые даты", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "Диапазон дат", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Будет повторяться в указанные даты", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Будет повторяться в указанном диапазоне дат", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Звук и вибрация", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Ещё", + "@settingGroupMore": {}, + "melodySetting": "Мелодия", + "@melodySetting": {}, + "audioChannelSetting": "Аудиоканал", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Будильник", + "@audioChannelAlarm": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "scheduleTypeWeek": "В определённые дни недели", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Будет повторяться в указанные дни недели", + "@scheduleTypeWeekDescription": {}, + "volumeSetting": "Громкость", + "@volumeSetting": {}, + "risingVolumeSetting": "Нарастающая громкость", + "@risingVolumeSetting": {}, + "snoozeSettingGroup": "Отсрочка", + "@snoozeSettingGroup": {}, + "snoozeEnableSetting": "Включено", + "@snoozeEnableSetting": {}, + "whileSnoozedSettingGroup": "Во время отсрочки", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "Запретить отключение", + "@snoozePreventDisablingSetting": {}, + "settings": "Настройки", + "@settings": {}, + "tasksSetting": "Задачи", + "@tasksSetting": {}, + "noItemMessage": "Пока нет добавленных {items}", + "@noItemMessage": {}, + "retypeIncludeNumSetting": "Включить числа", + "@retypeIncludeNumSetting": {}, + "noButton": "Нет", + "@noButton": {}, + "noAlarmMessage": "Будильники не созданы", + "@noAlarmMessage": {}, + "retypeLowercaseSetting": "Включить строчные буквы", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Длина последовательности", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Размер сетки", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Количество задач", + "@numberOfProblemsSetting": {}, + "cancelSkipAlarmButton": "Отменить пропуск", + "@cancelSkipAlarmButton": {}, + "durationDesc": "Самый длинный", + "@durationDesc": {}, + "timeOfDayAsc": "Ранние часы первыми", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Поздние часы первыми", + "@timeOfDayDesc": {}, + "dismissAlarmButton": "Отклонить", + "@dismissAlarmButton": {}, + "durationAsc": "Самый короткий", + "@durationAsc": {}, + "stopwatchSlowest": "Самый медленный", + "@stopwatchSlowest": {}, + "newPresetPlaceholder": "Новая предустановка", + "@newPresetPlaceholder": {}, + "filtersSettingGroup": "Фильтры", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Показать фильтры", + "@showFiltersSetting": {}, + "showSortSetting": "Показать сортировку", + "@showSortSetting": {}, + "notificationsSettingGroup": "Уведомления", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Показать уведомления о предстоящих будильниках", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Время до наступления события", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Показать уведомления об отложенных будильниках", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Показать уведомление", + "@showNotificationSetting": {}, + "saturdayFull": "Суббота", + "@saturdayFull": {}, + "tuesdayShort": "Вт", + "@tuesdayShort": {}, + "wednesdayShort": "Ср", + "@wednesdayShort": {}, + "thursdayShort": "Чт", + "@thursdayShort": {}, + "fridayShort": "Пт", + "@fridayShort": {}, + "saturdayShort": "Сб", + "@saturdayShort": {}, + "thursdayFull": "Четверг", + "@thursdayFull": {}, + "openSourceLicensesSetting": "Лицензии открытого исходного кода", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Участники", + "@contributorsSetting": {}, + "donorsSetting": "Спонсоры", + "@donorsSetting": {}, + "donateButton": "Пожертвовать", + "@donateButton": {}, + "searchSettingPlaceholder": "Поиск настройки", + "@searchSettingPlaceholder": {}, + "cityAlreadyInFavorites": "Этот город уже в избранном", + "@cityAlreadyInFavorites": {}, + "alarmLogSetting": "Журналы будильника", + "@alarmLogSetting": {}, + "duplicateButton": "Дублировать", + "@duplicateButton": {}, + "skipAlarmButton": "Пропустить следующий будильник", + "@skipAlarmButton": {}, + "allFilter": "Все", + "@allFilter": {}, + "todayFilter": "Сегодня", + "@todayFilter": {}, + "tomorrowFilter": "Завтра", + "@tomorrowFilter": {}, + "snoozedFilter": "Отложенный", + "@snoozedFilter": {}, + "alarmDescriptionFinished": "Нет будущих дат", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Не запланировано", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionTomorrow": "Только завтра", + "@alarmDescriptionTomorrow": {}, + "stopwatchFastest": "Самый быстрый", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "По {days}", + "@alarmDescriptionDays": {}, + "widgetsSettingGroup": "Виджеты", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Цифровые часы", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Макет", + "@layoutSettingGroup": {}, + "nameAsc": "Метка от А до Я", + "@nameAsc": {}, + "nameDesc": "Метка от Я до А", + "@nameDesc": {}, + "presetsSetting": "Предустановки", + "@presetsSetting": {}, + "showAverageLapSetting": "Показать средний круг", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Показать медленный круг", + "@showSlowestLapSetting": {}, + "exportSettingsSetting": "Экспорт", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Экспорт настроек в локальный файл", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Импорт", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Импорт настроек из локального файла", + "@importSettingsSettingDescription": {}, + "versionLabel": "Версия", + "@versionLabel": {}, + "leftHandedSetting": "Режим для левой руки", + "@leftHandedSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Невозможно отключить будильник, пока он отложен", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "sequenceTask": "Последовательность", + "@sequenceTask": {}, + "taskTryButton": "Попробовать", + "@taskTryButton": {}, + "maxSnoozesSetting": "Макс. отсрочек", + "@maxSnoozesSetting": {}, + "snoozePreventDeletionSetting": "Запретить удаление", + "@snoozePreventDeletionSetting": {}, + "audioChannelRingtone": "Рингтон", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Медиа", + "@audioChannelMedia": {}, + "chooseTaskTitle": "Выберите задачу для добавления", + "@chooseTaskTitle": {}, + "mathTask": "Математические задачи", + "@mathTask": {}, + "mathEasyDifficulty": "Легко (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Средне (X × Y)", + "@mathMediumDifficulty": {}, + "timeToFullVolumeSetting": "Время до полной громкости", + "@timeToFullVolumeSetting": {}, + "mathHardDifficulty": "Сложно (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Очень сложно (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Повторить ввод текста", + "@retypeTask": {}, + "audioChannelNotification": "Уведомление", + "@audioChannelNotification": {}, + "dateFilterGroup": "Дата", + "@dateFilterGroup": {}, + "scheduleDateFilterGroup": "Запланированная дата", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Дата создания", + "@createdDateFilterGroup": {}, + "stateFilterGroup": "Состояние", + "@stateFilterGroup": {}, + "activeFilter": "Активный", + "@activeFilter": {}, + "inactiveFilter": "Неактивный", + "@inactiveFilter": {}, + "disabledFilter": "Отключенный", + "@disabledFilter": {}, + "completedFilter": "Завершенный", + "@completedFilter": {}, + "runningTimerFilter": "Работающий", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Приостановленный", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Остановленный", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортировать", + "@sortGroup": {}, + "defaultLabel": "По умолчанию", + "@defaultLabel": {}, + "remainingTimeDesc": "Минимальное оставшееся время", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Максимальное оставшееся время", + "@remainingTimeAsc": {}, + "noTaskMessage": "Задачи не созданы", + "@noTaskMessage": {}, + "noPresetsMessage": "Пресеты не созданы", + "@noPresetsMessage": {}, + "noLogsMessage": "Нет журналов будильника", + "@noLogsMessage": {}, + "deleteButton": "Удалить", + "@deleteButton": {}, + "emailLabel": "Email", + "@emailLabel": {}, + "packageNameLabel": "Название пакета", + "@packageNameLabel": {}, + "licenseLabel": "Лицензия", + "@licenseLabel": {}, + "viewOnGithubLabel": "Посмотреть на GitHub", + "@viewOnGithubLabel": {}, + "addLengthSetting": "Добавить длительность", + "@addLengthSetting": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "showDateSetting": "Показать дату", + "@showDateSetting": {}, + "settingsTitle": "Настройки", + "@settingsTitle": {}, + "fontWeightSetting": "Начертание шрифта", + "@fontWeightSetting": {}, + "dateSettingGroup": "Дата", + "@dateSettingGroup": {}, + "timeSettingGroup": "Время", + "@timeSettingGroup": {}, + "sizeSetting": "Размер", + "@sizeSetting": {}, + "defaultPageSetting": "Вкладка по умолчанию", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Показывать AM/PM", + "@showMeridiemSetting": {}, + "dismissActionSlide": "Скольжение", + "@dismissActionSlide": {}, + "relativeTime": "на {hours}ч {relative, select, ahead{опережает} behind{отстаёт} other{другое}}", + "@relativeTime": {}, + "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ и ещё 1 дата} other{ и ещё {count} дат(ы)}}", + "@alarmDescriptionDates": {}, + "alarmDescriptionRange": "{interval, select, daily{Ежедневно} weekly{Еженедельно} other{Другое}} с {startDate} по {endDate}", + "@alarmDescriptionRange": {}, + "lessThanOneMinute": "менее 1 минуты", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Будильник прозвонит через {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Следующий: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} и {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}ч", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}м", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}с", + "@shortSecondsString": {}, + "showNextAlarm": "Показать следующий будильник", + "@showNextAlarm": {}, + "extraAnimationSettingDescription": "Показывать анимации, которые не оптимизированы и могут вызывать снижение частоты кадров на устройствах с низкой производительностью", + "@extraAnimationSettingDescription": {}, + "showForegroundNotification": "Показать уведомление на переднем плане", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Показывать постоянное уведомление, чтобы приложение оставалось активным", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Разрешить вывод уведомлений", + "@notificationPermissionDescription": {}, + "hoursString": "{count, plural, =0{} =1{1 час} other{{count} часа(ов)}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 минута} other{{count} минут(ы)}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунд(ы)}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 день} other{{count} дня(ей)}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 неделя} other{{count} недель(и)}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 месяц} other{{count} месяца(ев)}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 год} other{{count} года(лет)}}", + "@yearsString": {}, + "appLogs": "Журналы приложения", + "@appLogs": {}, + "showErrorSnackbars": "Показать снэкбары ошибок", + "@showErrorSnackbars": {}, + "clearLogs": "Очистить журналы", + "@clearLogs": {}, + "selectionStatus": "{n} выбрано", + "@selectionStatus": {}, + "shuffleTimerMelodiesAction": "Случайно перемешать мелодии для всех отобранных таймеров", + "@shuffleTimerMelodiesAction": {}, + "pickerNumpad": "Цифровая клавиатура", + "@pickerNumpad": {}, + "saveLogs": "Сохранить журналы", + "@saveLogs": {}, + "interactionsSettingGroup": "Взаимодействие", + "@interactionsSettingGroup": {}, + "longPressReorderAction": "Изменить порядок", + "@longPressReorderAction": {}, + "longPressActionSetting": "Действие при долгом нажатии", + "@longPressActionSetting": {}, + "longPressSelectAction": "Множественный выбор", + "@longPressSelectAction": {}, + "startMelodyAtRandomPos": "Случайная позиция", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "Мелодия начнётся с произвольной позиции", + "@startMelodyAtRandomPosDescription": {}, + "volumeWhileTasks": "Громкость при решении заданий", + "@volumeWhileTasks": {}, + "selectAll": "Выбрать все", + "@selectAll": {}, + "reorder": "Изменить порядок", + "@reorder": {}, + "shuffleAlarmMelodiesAction": "Случайно перемешать мелодии для всех отобранных сигналов будильника", + "@shuffleAlarmMelodiesAction": {}, + "resetAllFilteredTimersAction": "Сбросить все отобранные таймеры", + "@resetAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Приостановить все отобранные таймеры", + "@pauseAllFilteredTimersAction": {}, + "clockTypeSetting": "Тип часов", + "@clockTypeSetting": {}, + "playAllFilteredTimersAction": "Воспроизвести все отобранные таймеры", + "@playAllFilteredTimersAction": {}, + "clockStyleSettingGroup": "Стиль часов", + "@clockStyleSettingGroup": {}, + "analogClock": "Стрелочные", + "@analogClock": {}, + "digitalClock": "Цифровые", + "@digitalClock": {}, + "showClockTicksSetting": "Показать деления", + "@showClockTicksSetting": {}, + "majorTicks": "Только крупные деления", + "@majorTicks": {}, + "allTicks": "Все деления", + "@allTicks": {}, + "showNumbersSetting": "Показать цифры", + "@showNumbersSetting": {}, + "quarterNumbers": "Только четверти часа", + "@quarterNumbers": {}, + "allNumbers": "Все цифры", + "@allNumbers": {}, + "none": "Нет", + "@none": {}, + "numeralTypeSetting": "Тип цифр", + "@numeralTypeSetting": {}, + "arabicNumeral": "Арабские", + "@arabicNumeral": {}, + "romanNumeral": "Римские", + "@romanNumeral": {}, + "showDigitalClock": "Показать цифровые часы", + "@showDigitalClock": {}, + "backgroundServiceIntervalSetting": "Интервал обновления фоновой службы", + "@backgroundServiceIntervalSetting": {}, + "backgroundServiceIntervalSettingDescription": "Установка более короткого интервала позволит сохранить приложение активным, но может привести к увеличению расхода батареи", + "@backgroundServiceIntervalSettingDescription": {} } diff --git a/lib/l10n/app_sr.arb b/lib/l10n/app_sr.arb new file mode 100644 index 00000000..7511817c --- /dev/null +++ b/lib/l10n/app_sr.arb @@ -0,0 +1,740 @@ +{ + "timerTitle": "Тајмер", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Штоперица", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "generalSettingGroupDescription": "Поставите општа подешавања апликације, као што је формат времена", + "@generalSettingGroupDescription": {}, + "timeFormat12": "12-часовни", + "@timeFormat12": {}, + "timeFormat24": "24-часовни", + "@timeFormat24": {}, + "timeFormatDevice": "Подешавања уређаја", + "@timeFormatDevice": {}, + "showSecondsSetting": "Прикажи секунде", + "@showSecondsSetting": {}, + "timePickerSetting": "Избор времена", + "@timePickerSetting": {}, + "pickerDial": "Бројчаник", + "@pickerDial": {}, + "pickerInput": "Унос", + "@pickerInput": {}, + "pickerSpinner": "Ротациони бирач", + "@pickerSpinner": {}, + "durationPickerSetting": "Избор трајања", + "@durationPickerSetting": {}, + "swipActionCardAction": "Акције на картицама", + "@swipActionCardAction": {}, + "swipActionSwitchTabs": "Промени картицу", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Прелистајте између картица", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Мелодије", + "@melodiesSetting": {}, + "tagsSetting": "Ознаке", + "@tagsSetting": {}, + "vendorSetting": "Подешавања произвођача", + "@vendorSetting": {}, + "vendorSettingDescription": "Ручно онемогући оптимизације специфичне за произвођача", + "@vendorSettingDescription": {}, + "batteryOptimizationSettingDescription": "Онемогућите оптимизацију батерије за ову апликацију како бисте спречили одлагање alarma", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Дозволите обавештења на закључаном екрану за аларме и тајмере", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Неки уређаји захтевају да буде омогућено \"Аутоматско покретање\" како би аларми звонили док је апликација затворена", + "@autoStartSettingDescription": {}, + "autoStartSetting": "Аутоматско покретање", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Дозволе", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Игнориши оптимизацију батерије", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Дозвола за обавештења", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Дозвола за обавештења је већ дата", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Дозвола за игнорисање оптимизације батерије је већ дата", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "appearanceSettingGroupDescription": "Поставите теме, боје и промените изглед", + "@appearanceSettingGroupDescription": {}, + "nameField": "Назив", + "@nameField": {}, + "colorSetting": "Боја", + "@colorSetting": {}, + "textColorSetting": "Текст", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Шема боја", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Позадина", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Обод", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Сенка", + "@colorSchemeShadowSettingGroup": {}, + "styleThemeNamePlaceholder": "Тема стила", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Сенка", + "@styleThemeShadowSettingGroup": {}, + "styleThemeShapeSettingGroup": "Облик", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Уздигнутост", + "@styleThemeElevationSetting": {}, + "styleThemeBlurSetting": "Замућење", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineSettingGroup": "Обод", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Ширина", + "@styleThemeOutlineWidthSetting": {}, + "accessibilitySettingGroup": "Приступачност", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "Резервна копија", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Опције за програмере", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "Прикажи дугме за тренутни аларм", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Прикажи дугме за тренутни тајмер", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Дневници", + "@logsSettingGroup": {}, + "alarmLogSetting": "Дневници аларма", + "@alarmLogSetting": {}, + "aboutSettingGroup": "О апликацији", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Врати подразумеване вредности", + "@restoreSettingGroup": {}, + "resetButton": "Врати", + "@resetButton": {}, + "previewLabel": "Преглед", + "@previewLabel": {}, + "cardLabel": "Картица", + "@cardLabel": {}, + "displaySettingGroup": "Екран", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "Поузданост", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Боје", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Стил", + "@styleSettingGroup": {}, + "materialBrightnessLight": "Светлост", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Тамно", + "@materialBrightnessDark": {}, + "colorSchemeUseAccentAsOutlineSetting": "Боја нагласка као Обод", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Боја нагласка као Сенка", + "@colorSchemeUseAccentAsShadowSetting": {}, + "accentLabel": "Боја нагласка", + "@accentLabel": {}, + "useMaterialStyleSetting": "Користи \"Meterial\" стил", + "@useMaterialStyleSetting": {}, + "alarmTitle": "Аларм", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "clockTitle": "Сат", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "system": "Систем", + "@system": {}, + "generalSettingGroup": "Опште", + "@generalSettingGroup": {}, + "timeFormatSetting": "Формат времена", + "@timeFormatSetting": {}, + "languageSetting": "Језик", + "@languageSetting": {}, + "longDateFormatSetting": "Дугачки формат датума", + "@longDateFormatSetting": {}, + "dateFormatSetting": "Формат датума", + "@dateFormatSetting": {}, + "pickerRings": "Прстенови", + "@pickerRings": {}, + "swipeActionSetting": "При превлачењу", + "@swipeActionSetting": {}, + "swipeActionCardActionDescription": "Превуците лево или десно на картици да бисте извршили акције", + "@swipeActionCardActionDescription": {}, + "batteryOptimizationSetting": "Ручно онемогући оптимизацију батерије", + "@batteryOptimizationSetting": {}, + "allowNotificationSetting": "Ручно дозволите сва обавештења", + "@allowNotificationSetting": {}, + "animationSettingGroup": "Анимације", + "@animationSettingGroup": {}, + "extraAnimationSetting": "Додатне анимације", + "@extraAnimationSetting": {}, + "animationSpeedSetting": "Брзина анимације", + "@animationSpeedSetting": {}, + "appearanceSettingGroup": "Изглед", + "@appearanceSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Боја нагласка", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Грешка", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Картица", + "@colorSchemeCardSettingGroup": {}, + "styleThemeRadiusSetting": "Заобљеност углова", + "@styleThemeRadiusSetting": {}, + "errorLabel": "Грешка", + "@errorLabel": {}, + "styleThemeOpacitySetting": "Видљивост", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "Распрострањеност", + "@styleThemeSpreadSetting": {}, + "maxLogsSetting": "Максималан број дневника", + "@maxLogsSetting": {}, + "materialBrightnessSystem": "Систем", + "@materialBrightnessSystem": {}, + "useMaterialYouColorSetting": "Користи \"Material You\"", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "Осветљеност", + "@materialBrightnessSetting": {}, + "overrideAccentSetting": "Превазиђи боју нагласка", + "@overrideAccentSetting": {}, + "accentColorSetting": "Боја нагласка", + "@accentColorSetting": {}, + "styleThemeSetting": "Стил теме", + "@styleThemeSetting": {}, + "systemDarkModeSetting": "Системски тамни режим", + "@systemDarkModeSetting": {}, + "longPressSelectAction": "Вишеструки избор", + "@longPressSelectAction": {}, + "alarmDeleteAfterFinishingSetting": "Обриши након завршетка", + "@alarmDeleteAfterFinishingSetting": {}, + "scheduleTypeOnce": "Једном", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Дневно", + "@scheduleTypeDaily": {}, + "scheduleTypeRange": "Опсег датума", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Понављаће се у одређене датуме", + "@scheduleTypeDateDescription": {}, + "audioChannelSetting": "Аудио канал", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Аларм", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Обавештење", + "@audioChannelNotification": {}, + "saveLogs": "Сачувај записе", + "@saveLogs": {}, + "showErrorSnackbars": "Прикажи грешке у обавештењима", + "@showErrorSnackbars": {}, + "clearLogs": "Очисти записе", + "@clearLogs": {}, + "risingVolumeSetting": "Повећавање јачине звука", + "@risingVolumeSetting": {}, + "chooseTaskTitle": "Изабери задатак за додавање", + "@chooseTaskTitle": {}, + "mathTask": "Математички проблеми", + "@mathTask": {}, + "mathHardDifficulty": "Тешко (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Веома тешко (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Поново унеси текст", + "@retypeTask": {}, + "sequenceTask": "Секвенца", + "@sequenceTask": {}, + "taskTryButton": "Пробај", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "Тежина", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Број карактера", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Укључи бројеве", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Укључи мала слова", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Дужина секвенце", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Величина мреже", + "@sequenceGridSizeSetting": {}, + "dismissAlarmButton": "Одложи", + "@dismissAlarmButton": {}, + "allFilter": "Све", + "@allFilter": {}, + "dateFilterGroup": "Датум", + "@dateFilterGroup": {}, + "scheduleDateFilterGroup": "Распоред датума", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Датум креирања", + "@createdDateFilterGroup": {}, + "completedFilter": "Завршен", + "@completedFilter": {}, + "runningTimerFilter": "У току", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Паузиран", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Заустављен", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортирај", + "@sortGroup": {}, + "defaultLabel": "Подразумевано", + "@defaultLabel": {}, + "remainingTimeDesc": "Најмање времена преостало", + "@remainingTimeDesc": {}, + "enableAllFilteredAlarmsAction": "Омогући све филтриране аларме", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Онемогући све филтриране аларме", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Прескочи све филтриране аларме", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Откажи прескакање свих филтрираних аларма", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Обриши све филтриране ставке", + "@deleteAllFilteredAction": {}, + "alarmDescriptionToday": "Само данас", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "Само сутра", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Сваког дана", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Сваког викенда", + "@alarmDescriptionWeekend": {}, + "volumeWhileTasks": "Јачина звука током решавања задатака", + "@volumeWhileTasks": {}, + "selectionStatus": "{n} изабрано", + "@selectionStatus": {}, + "selectAll": "Изабери све", + "@selectAll": {}, + "reorder": "Прераспореди", + "@reorder": {}, + "alarmDescriptionWeekday": "Сваког радног дана", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionDays": "На {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Дневно} weekly{Недељно} other{Остало}} од {startDate} до {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "Најспорије", + "@stopwatchSlowest": {}, + "stopwatchAverage": "Просечно", + "@stopwatchAverage": {}, + "sundayShort": "Нед", + "@sundayShort": {}, + "widgetsSettingGroup": "Виџети", + "@widgetsSettingGroup": {}, + "alignmentTop": "Врх", + "@alignmentTop": {}, + "alignmentBottom": "Дно", + "@alignmentBottom": {}, + "alignmentLeft": "Лево", + "@alignmentLeft": {}, + "alignmentCenter": "Центар", + "@alignmentCenter": {}, + "alignmentRight": "Десно", + "@alignmentRight": {}, + "alignmentJustify": "Поравнај", + "@alignmentJustify": {}, + "fontWeightSetting": "Тежина фонта", + "@fontWeightSetting": {}, + "dateSettingGroup": "Датум", + "@dateSettingGroup": {}, + "timeSettingGroup": "Време", + "@timeSettingGroup": {}, + "sizeSetting": "Величина", + "@sizeSetting": {}, + "defaultPageSetting": "Подразумевана картица", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Прикажи АМ/ПМ", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Уреди предефинисане поставке", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Први дан у недељи", + "@firstDayOfWeekSetting": {}, + "translateLink": "Преведи", + "@translateLink": {}, + "translateDescription": "Помозите у превођењу апликације", + "@translateDescription": {}, + "yearsString": "{count, plural, =0{} =1{1 година} other{{count} година}}", + "@yearsString": {}, + "lessThanOneMinute": "мање од 1 минут", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Аларм ће се огласити за {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Следеће: {duration}", + "@nextAlarmIn": {}, + "showForegroundNotificationDescription": "Прикажи упорно обавештење да би апликација остала активна", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Дозволи приказивање обавештења", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Прикажи анимације које нису полиране и могу изазвати пад кадрова на уређајима нижег ранга", + "@extraAnimationSettingDescription": {}, + "defaultSettingGroup": "Подразумевана подешавања", + "@defaultSettingGroup": {}, + "filtersSettingGroup": "Филтери", + "@filtersSettingGroup": {}, + "audioChannelRingtone": "Звоно", + "@audioChannelRingtone": {}, + "settings": "Подешавања", + "@settings": {}, + "notificationsSettingGroup": "Обавештења", + "@notificationsSettingGroup": {}, + "showNotificationSetting": "Прикажи обавештење", + "@showNotificationSetting": {}, + "packageNameLabel": "Име пакета", + "@packageNameLabel": {}, + "timeToFullVolumeSetting": "Време до пуне јачине звука", + "@timeToFullVolumeSetting": {}, + "snoozeSettingGroup": "Одлагање", + "@snoozeSettingGroup": {}, + "snoozeEnableSetting": "Омогућено", + "@snoozeEnableSetting": {}, + "snoozeLengthSetting": "Дужина", + "@snoozeLengthSetting": {}, + "tasksSetting": "Задаци", + "@tasksSetting": {}, + "audioChannelMedia": "Медији", + "@audioChannelMedia": {}, + "volumeSetting": "Јачина звука", + "@volumeSetting": {}, + "maxSnoozesSetting": "Максимално одлагања", + "@maxSnoozesSetting": {}, + "snoozePreventDisablingSetting": "Спречи онемогућавање", + "@snoozePreventDisablingSetting": {}, + "snoozePreventDeletionSetting": "Спречи брисање", + "@snoozePreventDeletionSetting": {}, + "whileSnoozedSettingGroup": "Док је одложено", + "@whileSnoozedSettingGroup": {}, + "noItemMessage": "Још увек нема додатих {items}", + "@noItemMessage": {}, + "showSortSetting": "Прикажи сортирање", + "@showSortSetting": {}, + "tuesdayFull": "Уторак", + "@tuesdayFull": {}, + "stopwatchPrevious": "Претходно", + "@stopwatchPrevious": {}, + "stopwatchFastest": "Најбрже", + "@stopwatchFastest": {}, + "alarmDescriptionWeekly": "Сваког {days}", + "@alarmDescriptionWeekly": {}, + "combinedTime": "{hours} и {minutes}", + "@combinedTime": {}, + "pickerNumpad": "Нумпад", + "@pickerNumpad": {}, + "alarmDescriptionDates": "На {date}{count, plural, =0{} =1{ и још 1 датум} other{ и још {count} датума}}", + "@alarmDescriptionDates": {}, + "showUpcomingAlarmNotificationSetting": "Прикажи обавештења о предстојећим алармима", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Време предстојећег обавештења", + "@upcomingLeadTimeSetting": {}, + "alarmsDefaultSettingGroupDescription": "Постави подразумеване вредности за нове аларме", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Постави подразумеване вредности за нове тајмере", + "@timerDefaultSettingGroupDescription": {}, + "showFiltersSetting": "Прикажи филтере", + "@showFiltersSetting": {}, + "showSnoozeNotificationSetting": "Прикажи обавештења о одлагању", + "@showSnoozeNotificationSetting": {}, + "licenseLabel": "Лиценца", + "@licenseLabel": {}, + "elapsedTime": "Протекло време", + "@elapsedTime": {}, + "mondayFull": "Понедељак", + "@mondayFull": {}, + "wednesdayFull": "Среда", + "@wednesdayFull": {}, + "thursdayFull": "Четвртак", + "@thursdayFull": {}, + "versionLabel": "Верзија", + "@versionLabel": {}, + "addLengthSetting": "Додај дужину", + "@addLengthSetting": {}, + "fridayFull": "Петак", + "@fridayFull": {}, + "saturdayFull": "Субота", + "@saturdayFull": {}, + "tagNamePlaceholder": "Назив ознаке", + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{1 сат} other{{count} сати}}", + "@hoursString": {}, + "shortHoursString": "{hours}ч", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}м", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}с", + "@shortSecondsString": {}, + "showForegroundNotification": "Прикажи обавештење у првом плану", + "@showForegroundNotification": {}, + "showNextAlarm": "Прикажи следећи аларм", + "@showNextAlarm": {}, + "interactionsSettingGroup": "Интеракције", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "Акција дугог притиска", + "@longPressActionSetting": {}, + "longPressReorderAction": "Прераспореди", + "@longPressReorderAction": {}, + "colorSchemeSetting": "Шема боја", + "@colorSchemeSetting": {}, + "darkColorSchemeSetting": "Тамна шема боја", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "Сат", + "@clockSettingGroup": {}, + "timerSettingGroup": "Тајмер", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Штоперица", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Извези или увези своја подешавања локално", + "@backupSettingGroupDescription": {}, + "alarmWeekdaysSetting": "Дани у недељи", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Датуми", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Распон датума", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Интервал", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Дневно", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Недељно", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Обриши након одлагања", + "@alarmDeleteAfterRingingSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Не може се онемогућити аларм док је одложен", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "selectTime": "Изабери време", + "@selectTime": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "cancelButton": "Откажи", + "@cancelButton": {}, + "customizeButton": "Прилагоди", + "@customizeButton": {}, + "saveButton": "Сачувај", + "@saveButton": {}, + "labelField": "Ознака", + "@labelField": {}, + "labelFieldPlaceholder": "Додај ознаку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Распоред", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnceDescription": "Звониће при следећем појављивању времена", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Звониће сваког дана", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "У одређене дане у недељи", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Понављаће се у одређене дане у недељи", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "У одређене датуме", + "@scheduleTypeDate": {}, + "scheduleTypeRangeDescription": "Понављаће се током одређеног распона датума", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Звук и вибрација", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Више", + "@settingGroupMore": {}, + "melodySetting": "Мелодија", + "@melodySetting": {}, + "vibrationSetting": "Вибрација", + "@vibrationSetting": {}, + "mathEasyDifficulty": "Лако (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Средње (X × Y)", + "@mathMediumDifficulty": {}, + "noAlarmMessage": "Нема креираних аларма", + "@noAlarmMessage": {}, + "noTimerMessage": "Нема креираних тајмера", + "@noTimerMessage": {}, + "numberOfProblemsSetting": "Број проблема", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Да ли желите да изађете без чувања?", + "@saveReminderAlert": {}, + "yesButton": "Да", + "@yesButton": {}, + "noButton": "Не", + "@noButton": {}, + "cancelSkipAlarmButton": "Откажи прескакање", + "@cancelSkipAlarmButton": {}, + "noTagsMessage": "Нема креираних ознака", + "@noTagsMessage": {}, + "noStopwatchMessage": "Нема креираних штоперица", + "@noStopwatchMessage": {}, + "noTaskMessage": "Нема креираних задатака", + "@noTaskMessage": {}, + "noPresetsMessage": "Нема креираних унапред подешених вредности", + "@noPresetsMessage": {}, + "noLogsMessage": "Нема дневника аларма", + "@noLogsMessage": {}, + "deleteButton": "Обриши", + "@deleteButton": {}, + "duplicateButton": "Дуплирај", + "@duplicateButton": {}, + "skipAlarmButton": "Прескочи следећи аларм", + "@skipAlarmButton": {}, + "todayFilter": "Данас", + "@todayFilter": {}, + "tomorrowFilter": "Сутра", + "@tomorrowFilter": {}, + "stateFilterGroup": "Стање", + "@stateFilterGroup": {}, + "activeFilter": "Активан", + "@activeFilter": {}, + "inactiveFilter": "Неактиван", + "@inactiveFilter": {}, + "snoozedFilter": "Одложен", + "@snoozedFilter": {}, + "disabledFilter": "Онемогућен", + "@disabledFilter": {}, + "clearFiltersAction": "Очисти све филтере", + "@clearFiltersAction": {}, + "remainingTimeAsc": "Највише времена преостало", + "@remainingTimeAsc": {}, + "durationAsc": "Најкраће", + "@durationAsc": {}, + "durationDesc": "Најдуже", + "@durationDesc": {}, + "nameDesc": "Ознака Ш-А", + "@nameDesc": {}, + "nameAsc": "Ознака А-Ш", + "@nameAsc": {}, + "timeOfDayAsc": "Рани сати први", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Касни сати први", + "@timeOfDayDesc": {}, + "filterActions": "Филтер акције", + "@filterActions": {}, + "skippingDescriptionSuffix": "(прескакање следећег појављивања)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Одложено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionFinished": "Нема будућих датума", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Није заказано", + "@alarmDescriptionNotScheduled": {}, + "presetsSetting": "Пресети", + "@presetsSetting": {}, + "newPresetPlaceholder": "Нови пресет", + "@newPresetPlaceholder": {}, + "dismissActionSetting": "Тип акције за одбацивање", + "@dismissActionSetting": {}, + "dismissActionSlide": "Клизање", + "@dismissActionSlide": {}, + "dismissActionButtons": "Дугмад", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Дугмад у области", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат времена", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Прикажи милисекунде", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Поређење кругова", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "Прикажи претходни круг", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Прикажи најбржи круг", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Прикажи просечан круг", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Прикажи најспорији круг", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Режим за леворуке", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Извоз", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Извези подешавања у локални фајл", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Увоз", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Увези подешавања из локалног фајла", + "@importSettingsSettingDescription": {}, + "emailLabel": "Имејл", + "@emailLabel": {}, + "viewOnGithubLabel": "Погледај на GitHub-у", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Лиценце отвореног кода", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Сарадници", + "@contributorsSetting": {}, + "donorsSetting": "Донатори", + "@donorsSetting": {}, + "donateButton": "Донација", + "@donateButton": {}, + "relativeTime": "{hours}ч {relative, select, ahead{унапред} behind{уназад} other{Остало}}", + "@relativeTime": {}, + "sameTime": "Исто време", + "@sameTime": {}, + "searchSettingPlaceholder": "Претражи подешавање", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "Претражи град", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "Овај град је већ у вашим омиљеним", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Изабери трајање", + "@durationPickerTitle": {}, + "editButton": "Уреди", + "@editButton": {}, + "noLapsMessage": "Још нема кругова", + "@noLapsMessage": {}, + "sundayFull": "Недеља", + "@sundayFull": {}, + "mondayShort": "Пон", + "@mondayShort": {}, + "tuesdayShort": "Уто", + "@tuesdayShort": {}, + "wednesdayShort": "Сре", + "@wednesdayShort": {}, + "thursdayShort": "Чет", + "@thursdayShort": {}, + "fridayShort": "Пет", + "@fridayShort": {}, + "saturdayShort": "Суб", + "@saturdayShort": {}, + "contributorsDescription": "Људи који омогућавају ову апликацију", + "@contributorsDescription": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "У", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "Н", + "@sundayLetter": {}, + "donateDescription": "Донирајте за подршку развоју апликације", + "@donateDescription": {}, + "donorsDescription": "Наши великодушни патрони", + "@donorsDescription": {}, + "digitalClockSettingGroup": "Дигитални сат", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Распоред", + "@layoutSettingGroup": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "showDateSetting": "Прикажи датум", + "@showDateSetting": {}, + "settingsTitle": "Подешавања", + "@settingsTitle": {}, + "horizontalAlignmentSetting": "Хоризонтално поравнање", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикално поравнање", + "@verticalAlignmentSetting": {}, + "separatorSetting": "Сепаратор", + "@separatorSetting": {}, + "editTagLabel": "Уреди ознаку", + "@editTagLabel": {}, + "minutesString": "{count, plural, =0{} =1{1 минут} other{{count} минута}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунди}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 дан} other{{count} дана}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 недеља} other{{count} недеља}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 месец} other{{count} месеци}}", + "@monthsString": {} +} diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 0b5daa01..6a17a780 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -225,7 +225,7 @@ "@showSecondsSetting": {}, "pickerInput": "Girdi", "@pickerInput": {}, - "timePickerSetting": "https://hosted.weblate.org/translate/flathub/frontend/tr/?checksum=f9ea9ac0139d213e", + "timePickerSetting": "Zaman Seçici", "@timePickerSetting": {}, "ignoreBatteryOptimizationSetting": "Batarya İyileştirmesini Yoksay", "@ignoreBatteryOptimizationSetting": {}, @@ -674,5 +674,43 @@ "noLapsMessage": "Henüz hiç tur yok", "@noLapsMessage": {}, "searchSettingPlaceholder": "Bir ayar ara", - "@searchSettingPlaceholder": {} + "@searchSettingPlaceholder": {}, + "notificationPermissionDescription": "Bildirimlerin görüntülenmesine izin ver", + "@notificationPermissionDescription": {}, + "lessThanOneMinute": "1 dakikadan daha az", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Alarm {duration} içerisinde çalacak", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Sıradaki:{duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} ve {minutes}", + "@combinedTime": {}, + "showNextAlarm": "Sıradaki Alarmı Göster", + "@showNextAlarm": {}, + "showForegroundNotification": "Ön Plan Bildirimini Göster", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Uygulamayı aktif tutmak için kalıcı bir bildirim gösterin", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "Düşük özellikli aygıtlarda kare düşüşlerine neden olabilecek animasyonlar göster", + "@extraAnimationSettingDescription": {}, + "shortHoursString": "{hours}sa", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}dak", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}sn", + "@shortSecondsString": {}, + "secondsString": "{count, plural, =0{} =1{1 saniye} other{{count} saniye}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 gün} other{{count} gün}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 hafta} other{{count} hafta}}", + "@weeksString": {}, + "hoursString": "{count, plural, =0{} =1{1 saat} other{{count} saat}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 dakika} other{{count} dakika}}", + "@minutesString": {}, + "monthsString": "{count, plural, =0{} =1{1 ay} other{{count} ay}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 yıl} other{{count} yıl}}", + "@yearsString": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb new file mode 100644 index 00000000..a4ce233c --- /dev/null +++ b/lib/l10n/app_uk.arb @@ -0,0 +1,690 @@ +{ + "generalSettingGroupDescription": "Налаштування загальних налаштувань програми, таких як формат часу", + "@generalSettingGroupDescription": {}, + "languageSetting": "Мова", + "@languageSetting": {}, + "dateFormatSetting": "Формат дати", + "@dateFormatSetting": {}, + "longDateFormatSetting": "Довгий формат дати", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Формат часу", + "@timeFormatSetting": {}, + "timePickerSetting": "Вибір часу", + "@timePickerSetting": {}, + "pickerDial": "Циферблат", + "@pickerDial": {}, + "timeFormat12": "12-ти годинний", + "@timeFormat12": {}, + "pickerInput": "Ввід", + "@pickerInput": {}, + "timeFormat24": "24 годинний", + "@timeFormat24": {}, + "timeFormatDevice": "Налаштування пристрою", + "@timeFormatDevice": {}, + "showSecondsSetting": "Показувати секунди", + "@showSecondsSetting": {}, + "durationPickerSetting": "Вибір тривалості", + "@durationPickerSetting": {}, + "pickerRings": "Кільця", + "@pickerRings": {}, + "swipeActionSetting": "Дія при свайпі", + "@swipeActionSetting": {}, + "swipeActionCardActionDescription": "Проведіть по картці вліво або вправо, щоб виконати дію", + "@swipeActionCardActionDescription": {}, + "clockTitle": "Годинник", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "timerTitle": "Таймер", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Секундомір", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "system": "Система", + "@system": {}, + "swipActionSwitchTabs": "Перемикання вкладок", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Перемикатись між вкладками", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Мелодії", + "@melodiesSetting": {}, + "tagsSetting": "Мітки", + "@tagsSetting": {}, + "pickerSpinner": "Спінер", + "@pickerSpinner": {}, + "vendorSetting": "Налаштування постачальника", + "@vendorSetting": {}, + "vendorSettingDescription": "Вручну вимкнути оптимізацію для конкретного постачальника", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Вручну вимкнути оптимізацію батареї", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Вимкніть оптимізацію батареї для цієї програми, щоб запобігти затримці будильників", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Увімкнути сповіщення на екрані блокування для будильників і таймерів", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "На деяких пристроях потрібно увімкнути автоматичний запуск, щоб будильники дзвонили, коли програму закрито", + "@autoStartSettingDescription": {}, + "allowNotificationSetting": "Вручну дозволити всі сповіщення", + "@allowNotificationSetting": {}, + "autoStartSetting": "Автоматичний запуск", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Дозволи", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Ігнорувати оптимізацію батареї", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Дозвіл на сповіження", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Дозвіл на сповіження успішно надано", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Ігнорувати вже наданий дозвіл на оптимізацію батареї", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "animationSettingGroup": "Анімації", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Швидкість анімації", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Додаткові анімації", + "@extraAnimationSetting": {}, + "appearanceSettingGroup": "Зовнішній вигляд", + "@appearanceSettingGroup": {}, + "appearanceSettingGroupDescription": "Встановлюйте теми, кольори та змінюйте макет", + "@appearanceSettingGroupDescription": {}, + "nameField": "Назва", + "@nameField": {}, + "colorSetting": "Колір", + "@colorSetting": {}, + "textColorSetting": "Текст", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Кольорова схема", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Фон", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Акцент", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Помилка", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Картка", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Завнішня лінія", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Тінь", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "Використовувати акцент як зовнішню лінію", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Використовувати акцент як тінь", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeShapeSettingGroup": "Форма", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Висота", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "Заокруглення кутів", + "@styleThemeRadiusSetting": {}, + "styleThemeBlurSetting": "Розмиття", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Розподіл", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineSettingGroup": "Зовнішня лінія", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Ширина", + "@styleThemeOutlineWidthSetting": {}, + "accessibilitySettingGroup": "Доступність", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "Резервне копіювання", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Параметри розробника", + "@developerOptionsSettingGroup": {}, + "logsSettingGroup": "Журнали", + "@logsSettingGroup": {}, + "maxLogsSetting": "Максимальний журнал", + "@maxLogsSetting": {}, + "alarmLogSetting": "Журнал будильників", + "@alarmLogSetting": {}, + "aboutSettingGroup": "Про", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Відновити стандартні значення", + "@restoreSettingGroup": {}, + "resetButton": "Скинути", + "@resetButton": {}, + "previewLabel": "Попередній перегляд", + "@previewLabel": {}, + "cardLabel": "Картка", + "@cardLabel": {}, + "accentLabel": "Акцент", + "@accentLabel": {}, + "errorLabel": "Помилка", + "@errorLabel": {}, + "reliabilitySettingGroup": "Надійність", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Кольори", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Стиль", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "Використовувати Material You", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "Яскравість", + "@materialBrightnessSetting": {}, + "materialBrightnessDark": "Темна", + "@materialBrightnessDark": {}, + "overrideAccentSetting": "Перевизначити колір акценту", + "@overrideAccentSetting": {}, + "accentColorSetting": "Акцентний колір", + "@accentColorSetting": {}, + "useMaterialStyleSetting": "Використовувати стиль Material", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "Тема стилю", + "@styleThemeSetting": {}, + "systemDarkModeSetting": "Темний режим системи", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "Кольорова схема", + "@colorSchemeSetting": {}, + "clockSettingGroup": "Годинник", + "@clockSettingGroup": {}, + "timerSettingGroup": "Таймер", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Секундомір", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Експортуйте або імпортуйте локальні налаштування", + "@backupSettingGroupDescription": {}, + "alarmRangeSetting": "Діапазон дат", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Проміжок", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Щодня", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Видалити після завершення", + "@alarmDeleteAfterFinishingSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Неможливо вимкнути будильник, коли він відкладений", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "selectTime": "Вибрати час", + "@selectTime": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "cancelButton": "Скасувати", + "@cancelButton": {}, + "customizeButton": "Налаштувати", + "@customizeButton": {}, + "saveButton": "Зберегти", + "@saveButton": {}, + "labelField": "Мітка", + "@labelField": {}, + "labelFieldPlaceholder": "Додати мітку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Розклад", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Одноразово", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Щодня", + "@scheduleTypeDaily": {}, + "scheduleTypeOnceDescription": "Дзвонитиме при наступному настанні часу", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Дзвонитиме щодня", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "У визначені дні тижня", + "@scheduleTypeWeek": {}, + "scheduleTypeDate": "У конкретні дати", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "Діапазон дат", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Повторюватиметься у зазначені дати", + "@scheduleTypeDateDescription": {}, + "soundAndVibrationSettingGroup": "Звук і вібрація", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Більше", + "@settingGroupMore": {}, + "melodySetting": "Мелодія", + "@melodySetting": {}, + "vibrationSetting": "Вібрація", + "@vibrationSetting": {}, + "audioChannelSetting": "Аудіо канал", + "@audioChannelSetting": {}, + "audioChannelNotification": "Сповіщення", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Рингтон", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Медіа", + "@audioChannelMedia": {}, + "volumeSetting": "Гучність", + "@volumeSetting": {}, + "risingVolumeSetting": "Зростання гучності", + "@risingVolumeSetting": {}, + "timeToFullVolumeSetting": "Час до повної гучності", + "@timeToFullVolumeSetting": {}, + "snoozeSettingGroup": "Відкласти", + "@snoozeSettingGroup": {}, + "snoozeLengthSetting": "Довжина", + "@snoozeLengthSetting": {}, + "maxSnoozesSetting": "Максимальне відкладення", + "@maxSnoozesSetting": {}, + "whileSnoozedSettingGroup": "Поки відкладений", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "Запобігти відключенню", + "@snoozePreventDisablingSetting": {}, + "snoozePreventDeletionSetting": "Запобігти видаленню", + "@snoozePreventDeletionSetting": {}, + "settings": "Налаштування", + "@settings": {}, + "noItemMessage": "{items} ще не додано", + "@noItemMessage": {}, + "chooseTaskTitle": "Виберіть завдання для додавання", + "@chooseTaskTitle": {}, + "mathTask": "Математичні завдання", + "@mathTask": {}, + "mathMediumDifficulty": "Середні (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Важкі (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Дуже важкі (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Повторно введіть текст", + "@retypeTask": {}, + "sequenceTask": "Послідовність", + "@sequenceTask": {}, + "taskTryButton": "Спробуйте", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "Складність", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Кількість символів", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Включити числа", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Включити малі літери", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Довжина послідовності", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Розмір таблиці", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Кількість завдань", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Ви хочете вийти без збереження?", + "@saveReminderAlert": {}, + "yesButton": "Так", + "@yesButton": {}, + "noButton": "Ні", + "@noButton": {}, + "noAlarmMessage": "Не створено жодного будильника", + "@noAlarmMessage": {}, + "noTimerMessage": "Немає жодного таймера", + "@noTimerMessage": {}, + "noTagsMessage": "Немає створених міток", + "@noTagsMessage": {}, + "noStopwatchMessage": "Немає створених секундомірів", + "@noStopwatchMessage": {}, + "noPresetsMessage": "Не створено набору налаштувань", + "@noPresetsMessage": {}, + "noLogsMessage": "Немає журналів будильників", + "@noLogsMessage": {}, + "deleteButton": "Видалити", + "@deleteButton": {}, + "skipAlarmButton": "Пропустити наступний будильник", + "@skipAlarmButton": {}, + "cancelSkipAlarmButton": "Скасувати пропуск", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Відхилити", + "@dismissAlarmButton": {}, + "scheduleDateFilterGroup": "Дата розкладу", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Дату створено", + "@createdDateFilterGroup": {}, + "todayFilter": "Сьогодні", + "@todayFilter": {}, + "tomorrowFilter": "Завтра", + "@tomorrowFilter": {}, + "stateFilterGroup": "Стан", + "@stateFilterGroup": {}, + "activeFilter": "Активний", + "@activeFilter": {}, + "inactiveFilter": "Неактивний", + "@inactiveFilter": {}, + "snoozedFilter": "Відкладений", + "@snoozedFilter": {}, + "disabledFilter": "Вимкнений", + "@disabledFilter": {}, + "completedFilter": "Виконаний", + "@completedFilter": {}, + "runningTimerFilter": "Виконується", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Призупинено", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Зупинено", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортувати", + "@sortGroup": {}, + "remainingTimeAsc": "Залишилося найбільше часу", + "@remainingTimeAsc": {}, + "durationAsc": "Найкоротший", + "@durationAsc": {}, + "durationDesc": "Найдовший", + "@durationDesc": {}, + "nameAsc": "Мітка А-Я", + "@nameAsc": {}, + "nameDesc": "Мітка Я-А", + "@nameDesc": {}, + "filterActions": "Дії фільтру", + "@filterActions": {}, + "clearFiltersAction": "Очистити всі фільтри", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Увімкнути всі відфільтровані будильники", + "@enableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Пропустити всі відфльтровані будильники", + "@skipAllFilteredAlarmsAction": {}, + "alarmDescriptionFinished": "Немає наєбутніх дат", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Не заплановано", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "Тільки сьогодні", + "@alarmDescriptionToday": {}, + "alarmDescriptionWeekend": "Кожні вихідні", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Попередній", + "@stopwatchPrevious": {}, + "alarmDescriptionWeekday": "Кожен будній день", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionWeekly": "Кожні {days}", + "@alarmDescriptionWeekly": {}, + "stopwatchFastest": "Найшвидший", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "У {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Щоденно} weekly{Щотжня} other{Інше}} з {startDate} да {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "Найповільніший", + "@stopwatchSlowest": {}, + "stopwatchAverage": "Середнє", + "@stopwatchAverage": {}, + "defaultSettingGroup": "Стандартні налаштування", + "@defaultSettingGroup": {}, + "timerDefaultSettingGroupDescription": "Встановити стандартні значення для нових таймерів", + "@timerDefaultSettingGroupDescription": {}, + "showUpcomingAlarmNotificationSetting": "Показувати сповіщення про майбутні будильники", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Найближчий час виконання", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Показувати сповіщення про відкладення", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Показувати сповіщення", + "@showNotificationSetting": {}, + "presetsSetting": "Набори налаштувань", + "@presetsSetting": {}, + "dismissActionSetting": "Тип дії відхилення", + "@dismissActionSetting": {}, + "dismissActionButtons": "Кнопки", + "@dismissActionButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат часу", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Показувати мілісекунди", + "@stopwatchShowMillisecondsSetting": {}, + "showPreviousLapSetting": "Показувати попереднє коло", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Показувати найшвидше коло", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Показувати середнє коло", + "@showAverageLapSetting": {}, + "exportSettingsSettingDescription": "Експортувати налаштування до локального файлу", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Імпорт", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Імпортувати налаштування з локального файлу", + "@importSettingsSettingDescription": {}, + "versionLabel": "Версія", + "@versionLabel": {}, + "packageNameLabel": "Назва пакунка", + "@packageNameLabel": {}, + "licenseLabel": "Ліцензія", + "@licenseLabel": {}, + "emailLabel": "Е-Пошта", + "@emailLabel": {}, + "viewOnGithubLabel": "Дивитись на GitHub", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Відкрити джерело ліцензій", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Учасники", + "@contributorsSetting": {}, + "donorsSetting": "Донори", + "@donorsSetting": {}, + "donateButton": "Спонсорувати", + "@donateButton": {}, + "sameTime": "Той самий час", + "@sameTime": {}, + "searchSettingPlaceholder": "Пошук налаштувань", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "Пошук міста", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "Це місто вже присутнє у ваших обраних", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Вибрати тривалість", + "@durationPickerTitle": {}, + "editButton": "Редагувати", + "@editButton": {}, + "tuesdayShort": "Вт", + "@tuesdayShort": {}, + "wednesdayShort": "Ср", + "@wednesdayShort": {}, + "thursdayShort": "Чт", + "@thursdayShort": {}, + "fridayShort": "Пт", + "@fridayShort": {}, + "saturdayShort": "Сб", + "@saturdayShort": {}, + "sundayShort": "Нд", + "@sundayShort": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "В", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "Н", + "@sundayLetter": {}, + "donateDescription": "Зробіть пожертву, щоб підтримати розробку програми", + "@donateDescription": {}, + "donorsDescription": "Наші щедрі меценати", + "@donorsDescription": {}, + "contributorsDescription": "Люди, завдяки яким ця програма стала можливою", + "@contributorsDescription": {}, + "widgetsSettingGroup": "Віджети", + "@widgetsSettingGroup": {}, + "layoutSettingGroup": "Макет", + "@layoutSettingGroup": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "defaultPageSetting": "Стандартна вкладка", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Показувати ДО/ПО", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Редагувати набори налаштувань", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Перший день тижня", + "@firstDayOfWeekSetting": {}, + "translateLink": "Перекласти", + "@translateLink": {}, + "translateDescription": "Допомогти з перекладом програми", + "@translateDescription": {}, + "separatorSetting": "Розділювач", + "@separatorSetting": {}, + "editTagLabel": "Редагувати мітку", + "@editTagLabel": {}, + "tagNamePlaceholder": "Назва мітки", + "@tagNamePlaceholder": {}, + "alarmTitle": "Будильник", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "generalSettingGroup": "Загальні", + "@generalSettingGroup": {}, + "swipActionCardAction": "Дії з картками", + "@swipActionCardAction": {}, + "styleThemeNamePlaceholder": "Тема стилю", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Тінь", + "@styleThemeShadowSettingGroup": {}, + "showIstantAlarmButtonSetting": "Показувати кнопку екземпляра будильника", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Показувати кнопку екземпляру такмера", + "@showIstantTimerButtonSetting": {}, + "styleThemeOpacitySetting": "Прозорість", + "@styleThemeOpacitySetting": {}, + "displaySettingGroup": "Дисплей", + "@displaySettingGroup": {}, + "materialBrightnessSystem": "Система", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Світла", + "@materialBrightnessLight": {}, + "darkColorSchemeSetting": "Темна кольорова схема", + "@darkColorSchemeSetting": {}, + "alarmWeekdaysSetting": "Дні тиждня", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Дати", + "@alarmDatesSetting": {}, + "alarmIntervalWeekly": "Щотижня", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Видалити після відхилення", + "@alarmDeleteAfterRingingSetting": {}, + "audioChannelAlarm": "Будильник", + "@audioChannelAlarm": {}, + "scheduleTypeWeekDescription": "Повторюватиметься у вказані дні тижня", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeRangeDescription": "Повторюватиметься протягом вказаного діапазону дат", + "@scheduleTypeRangeDescription": {}, + "snoozeEnableSetting": "Увімкнено", + "@snoozeEnableSetting": {}, + "tasksSetting": "Завдання", + "@tasksSetting": {}, + "mathEasyDifficulty": "Лугкі (X + Y)", + "@mathEasyDifficulty": {}, + "noTaskMessage": "Не створено жодного завдання", + "@noTaskMessage": {}, + "duplicateButton": "Дублювати", + "@duplicateButton": {}, + "dateFilterGroup": "Дата", + "@dateFilterGroup": {}, + "allFilter": "Всі", + "@allFilter": {}, + "defaultLabel": "За замовчуванням", + "@defaultLabel": {}, + "remainingTimeDesc": "Залишилося найменше часу", + "@remainingTimeDesc": {}, + "disableAllFilteredAlarmsAction": "Вимкнути всі відфільтровані будильники", + "@disableAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Скасувати пропуск всіх відфльтрованих будильників", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Видалити всі відфільтровані будильники", + "@deleteAllFilteredAction": {}, + "skippingDescriptionSuffix": "(пропускаючи наступне повторення)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Відкладено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionTomorrow": "Тільки завтра", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Кожного дня", + "@alarmDescriptionEveryDay": {}, + "alarmsDefaultSettingGroupDescription": "Встановити стандартні значення для нових будильників", + "@alarmsDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Фільтри", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Показати фільтри", + "@showFiltersSetting": {}, + "showSortSetting": "Показати сортування", + "@showSortSetting": {}, + "notificationsSettingGroup": "Сповіщення", + "@notificationsSettingGroup": {}, + "newPresetPlaceholder": "Новий набір налаштувань", + "@newPresetPlaceholder": {}, + "showSlowestLapSetting": "Показувати найповільніше коло", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Режим для лівші", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Експорт", + "@exportSettingsSetting": {}, + "noLapsMessage": "Ще немає кіл", + "@noLapsMessage": {}, + "elapsedTime": "Витрачений час", + "@elapsedTime": {}, + "mondayFull": "Понеділок", + "@mondayFull": {}, + "tuesdayFull": "Вівторок", + "@tuesdayFull": {}, + "sundayFull": "Неділя", + "@sundayFull": {}, + "wednesdayFull": "Середа", + "@wednesdayFull": {}, + "thursdayFull": "Четвер", + "@thursdayFull": {}, + "fridayFull": "Пʼятниця", + "@fridayFull": {}, + "saturdayFull": "Субота", + "@saturdayFull": {}, + "mondayShort": "Пн", + "@mondayShort": {}, + "digitalClockSettingGroup": "Цифровий годинник", + "@digitalClockSettingGroup": {}, + "showDateSetting": "Показувати дату", + "@showDateSetting": {}, + "settingsTitle": "Налаштування", + "@settingsTitle": {}, + "horizontalAlignmentSetting": "Горизонтальне вирівнювання", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикальне вирівнювання", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Вгорі", + "@alignmentTop": {}, + "alignmentBottom": "Внизу", + "@alignmentBottom": {}, + "alignmentLeft": "Зліва", + "@alignmentLeft": {}, + "alignmentCenter": "Центер", + "@alignmentCenter": {}, + "alignmentRight": "Зправа", + "@alignmentRight": {}, + "fontWeightSetting": "Товщина шрифту", + "@fontWeightSetting": {}, + "dateSettingGroup": "Дата", + "@dateSettingGroup": {}, + "timeSettingGroup": "Час", + "@timeSettingGroup": {}, + "sizeSetting": "Розмір", + "@sizeSetting": {}, + "timeOfDayAsc": "Спочатку ранні години", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Спочатку пізні години", + "@timeOfDayDesc": {}, + "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ і 1 інша дата} other{ і {count} інші дати}}", + "@alarmDescriptionDates": {}, + "dismissActionSlide": "Ковзання", + "@dismissActionSlide": {}, + "dismissActionAreaButtons": "Об'ємні кнопки", + "@dismissActionAreaButtons": {}, + "comparisonLapBarsSettingGroup": "Смуги порівняння кіл", + "@comparisonLapBarsSettingGroup": {}, + "addLengthSetting": "Додати тривалість", + "@addLengthSetting": {}, + "relativeTime": "{hours} г. {relative, select, ahead{попереду} behind{позаду} other{Other}}", + "@relativeTime": {}, + "alignmentJustify": "Вирівняти по ширині", + "@alignmentJustify": {}, + "hoursString": "{count, plural, =0{} =1{1 година} other{{count} години}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 хвилина} other{{count} хвилини}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунди}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 день} other{{count} дні}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 тиждень} other{{count} тижнів}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 місяць} other{{count} місяці}}", + "@monthsString": {} +} diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index ec9a416c..e20707a1 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -3,7 +3,7 @@ "@sequenceGridSizeSetting": {}, "scheduleTypeWeekDescription": "Sẽ lặp lại vào các ngày trong tuần được chỉ định", "@scheduleTypeWeekDescription": {}, - "soundSettingGroup": "Âm thanh và Rung", + "soundSettingGroup": "Âm thanh", "@soundSettingGroup": {}, "saveReminderAlert": "Bạn có muốn rời đi mà không lưu?", "@saveReminderAlert": {}, @@ -95,7 +95,7 @@ "@appearanceSettingGroupDescription": {}, "backupSettingGroupDescription": "Xuất hoặc nhập thiết đặt của bạn cục bộ", "@backupSettingGroupDescription": {}, - "saveButton": "Nút lưu", + "saveButton": "Lưu", "@saveButton": {}, "labelField": "Nhãn", "@labelField": {}, @@ -252,5 +252,335 @@ "pickerInput": "Đầu vào", "@pickerInput": {}, "longDateFormatSetting": "Định dạng ngày theo thứ, tháng, ngày và năm", - "@longDateFormatSetting": {} + "@longDateFormatSetting": {}, + "firstDayOfWeekSetting": "Ngày đầu tuần", + "@firstDayOfWeekSetting": {}, + "translateDescription": "Giúp dịch ứng dụng", + "@translateDescription": {}, + "interactionsSettingGroup": "Các tương tác", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "Hành động nhấn giữ", + "@longPressActionSetting": {}, + "longPressReorderAction": "Thay đổi thứ tự", + "@longPressReorderAction": {}, + "longPressSelectAction": "Chọn nhiều", + "@longPressSelectAction": {}, + "allowNotificationSetting": "Cho phép tất cả các thông báo thủ công", + "@allowNotificationSetting": {}, + "animationSettingGroup": "Hoạt ảnh", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Tốc độ hoạt ảnh", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Hoạt ảnh bổ sung", + "@extraAnimationSetting": {}, + "nameField": "Tên", + "@nameField": {}, + "colorSetting": "Màu sắc", + "@colorSetting": {}, + "textColorSetting": "Màu chữ", + "@textColorSetting": {}, + "colorSchemeBackgroundSettingGroup": "Nền", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Lỗi", + "@colorSchemeErrorSettingGroup": {}, + "saveLogs": "Lưu nhật ký", + "@saveLogs": {}, + "clearLogs": "Xoá nhật ký", + "@clearLogs": {}, + "errorLabel": "Lỗi", + "@errorLabel": {}, + "materialBrightnessSystem": "Hệ thống", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Sáng", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Tối", + "@materialBrightnessDark": {}, + "soundAndVibrationSettingGroup": "Âm thanh và rung", + "@soundAndVibrationSettingGroup": {}, + "audioChannelRingtone": "Nhạc chuông", + "@audioChannelRingtone": {}, + "numberOfProblemsSetting": "Số lượng bài toàn", + "@numberOfProblemsSetting": {}, + "logTypeFilterGroup": "Loại", + "@logTypeFilterGroup": {}, + "stateFilterGroup": "Trạng thái", + "@stateFilterGroup": {}, + "activeFilter": "Đang hoạt động", + "@activeFilter": {}, + "inactiveFilter": "Không hoạt động", + "@inactiveFilter": {}, + "reorder": "Xắp xếp lại", + "@reorder": {}, + "defaultLabel": "Mặc định", + "@defaultLabel": {}, + "durationAsc": "Ngắn nhất", + "@durationAsc": {}, + "durationDesc": "Dài nhất", + "@durationDesc": {}, + "nameAsc": "Nhãn A-Z", + "@nameAsc": {}, + "nameDesc": "Nhãn Z-A", + "@nameDesc": {}, + "filterActions": "Hành động lọc", + "@filterActions": {}, + "disableAllFilteredAlarmsAction": "Tắt tất cả báo thức được lọc", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Bỏ qua tất cả báo thức được lọc", + "@skipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Xoá tất cả các mục được lọc", + "@deleteAllFilteredAction": {}, + "exportSettingsSettingDescription": "Xuất cài đặt ra tệp tin cục bộ", + "@exportSettingsSettingDescription": {}, + "importSettingsSettingDescription": "Nhập cài đặt từ tệp tin cục bộ", + "@importSettingsSettingDescription": {}, + "packageNameLabel": "Tên gói", + "@packageNameLabel": {}, + "licenseLabel": "Giấy phép", + "@licenseLabel": {}, + "emailLabel": "Email", + "@emailLabel": {}, + "viewOnGithubLabel": "Xem trên Github", + "@viewOnGithubLabel": {}, + "wednesdayFull": "Thứ 4", + "@wednesdayFull": {}, + "thursdayFull": "Thứ 5", + "@thursdayFull": {}, + "fridayFull": "Thứ 6", + "@fridayFull": {}, + "tuesdayLetter": "3", + "@tuesdayLetter": {}, + "wednesdayLetter": "4", + "@wednesdayLetter": {}, + "saturdayLetter": "7", + "@saturdayLetter": {}, + "alignmentBottom": "Dưới cùng", + "@alignmentBottom": {}, + "alignmentCenter": "Trung tâm", + "@alignmentCenter": {}, + "alignmentRight": "Bên phải", + "@alignmentRight": {}, + "alignmentLeft": "Bên trái", + "@alignmentLeft": {}, + "alignmentJustify": "Căn chỉnh", + "@alignmentJustify": {}, + "fontWeightSetting": "Độ đậm chữ", + "@fontWeightSetting": {}, + "dateSettingGroup": "Ngày", + "@dateSettingGroup": {}, + "timeSettingGroup": "Thời gian", + "@timeSettingGroup": {}, + "sizeSetting": "Kích cỡ", + "@sizeSetting": {}, + "defaultPageSetting": "Tab mặc định", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Hiển thị AM/PM", + "@showMeridiemSetting": {}, + "alarmRingInMessage": "Báo thức sẽ reo trong {duration}", + "@alarmRingInMessage": {}, + "pickerNumpad": "Bàn phím số", + "@pickerNumpad": {}, + "autoStartSettingDescription": "Một số thiết bị yêu cầu phải bật Auto Start để báo thức có thể đổ chuông khi ứng dụng đang đóng", + "@autoStartSettingDescription": {}, + "thursdayLetter": "5", + "@thursdayLetter": {}, + "colorSchemeNamePlaceholder": "Bảng màu", + "@colorSchemeNamePlaceholder": {}, + "showIstantAlarmButtonSetting": "Hiển thị nút báo động tức thì", + "@showIstantAlarmButtonSetting": {}, + "styleThemeOutlineWidthSetting": "Độ rộng", + "@styleThemeOutlineWidthSetting": {}, + "styleThemeOutlineSettingGroup": "Viền", + "@styleThemeOutlineSettingGroup": {}, + "showIstantTimerButtonSetting": "Hiển thị nút hẹn giờ tức thì", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Nhật ký", + "@logsSettingGroup": {}, + "selectAll": "Chọn tất cả", + "@selectAll": {}, + "selectionStatus": "Đã chọn {n}", + "@selectionStatus": {}, + "clearFiltersAction": "Xoá tất cả bộ lọc", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Bật tất cả báo thức được lọc", + "@enableAllFilteredAlarmsAction": {}, + "fridayLetter": "6", + "@fridayLetter": {}, + "importSettingsSetting": "Nhập", + "@importSettingsSetting": {}, + "combinedTime": "{hours} và {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours} giờ", + "@shortHoursString": {}, + "shortMinutesString": "{minutes} phút", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds} giây", + "@shortSecondsString": {}, + "permissionsSettingGroup": "Quyền", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Bỏ qua tối ưu hóa pin", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Quyền thông báo", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Quyền thông báo đã được cấp", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Quyền bỏ qua tối ưu hóa pin đã được cấp", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "colorSchemeCardSettingGroup": "Thẻ", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Viền", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Đổ bóng", + "@colorSchemeShadowSettingGroup": {}, + "styleThemeNamePlaceholder": "Phong cách chủ đề", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Đổ bóng", + "@styleThemeShadowSettingGroup": {}, + "styleThemeRadiusSetting": "Bo góc", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Độ trong suốt", + "@styleThemeOpacitySetting": {}, + "styleThemeBlurSetting": "Độ mờ", + "@styleThemeBlurSetting": {}, + "resetButton": "Đặt lại", + "@resetButton": {}, + "previewLabel": "Xem trước", + "@previewLabel": {}, + "cardLabel": "Thẻ", + "@cardLabel": {}, + "alarmIntervalDaily": "Hàng ngày", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Hàng tuần", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterFinishingSetting": "Xoá sau khi hoàn tất", + "@alarmDeleteAfterFinishingSetting": {}, + "audioChannelNotification": "Thông báo", + "@audioChannelNotification": {}, + "snoozePreventDeletionSetting": "Ngăn chặn xoá", + "@snoozePreventDeletionSetting": {}, + "noAlarmMessage": "Không có báo thức nào được tạo", + "@noAlarmMessage": {}, + "createdDateFilterGroup": "Ngày tạo", + "@createdDateFilterGroup": {}, + "showNotificationSetting": "Hiển thị thông báo", + "@showNotificationSetting": {}, + "stopwatchTimeFormatSettingGroup": "Định dạng thời gian", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Hiển thị mili-giây", + "@stopwatchShowMillisecondsSetting": {}, + "openSourceLicensesSetting": "Giấy phép mã nguồn mở", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Đóng góp", + "@contributorsSetting": {}, + "donorsSetting": "Tài trợ", + "@donorsSetting": {}, + "donateButton": "Ủng hộ", + "@donateButton": {}, + "cityAlreadyInFavorites": "Thành phố này đã nằm trong danh sách yêu thích của bạn", + "@cityAlreadyInFavorites": {}, + "saturdayFull": "Thứ 7", + "@saturdayFull": {}, + "sundayFull": "Chủ nhật", + "@sundayFull": {}, + "mondayShort": "T2", + "@mondayShort": {}, + "tuesdayShort": "T3", + "@tuesdayShort": {}, + "wednesdayShort": "T4", + "@wednesdayShort": {}, + "thursdayShort": "T5", + "@thursdayShort": {}, + "fridayShort": "T6", + "@fridayShort": {}, + "mondayLetter": "2", + "@mondayLetter": {}, + "donateDescription": "Quyên góp để hỗ trợ sự phát triển của ứng dụng", + "@donateDescription": {}, + "translateLink": "Dịch", + "@translateLink": {}, + "timePickerModeButton": "Chế độ", + "@timePickerModeButton": {}, + "scheduleTypeDate": "Vào những ngày cụ thể", + "@scheduleTypeDate": {}, + "mathEasyDifficulty": "Dễ (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Trung bình (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Khó (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Rất khó (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "leftHandedSetting": "Chế độ thuận tay trái", + "@leftHandedSetting": {}, + "runningTimerFilter": "Đang chạy", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Tạm dừng", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Đã dừng", + "@stoppedTimerFilter": {}, + "sortGroup": "Sắp xếp", + "@sortGroup": {}, + "alarmDescriptionWeekly": "Mỗi {days}", + "@alarmDescriptionWeekly": {}, + "sameTime": "Cùng thời gian", + "@sameTime": {}, + "searchCityPlaceholder": "Tìm kiếm thành phố", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Chọn thời lượng", + "@durationPickerTitle": {}, + "editButton": "Chỉnh sửa", + "@editButton": {}, + "mondayFull": "Thứ 2", + "@mondayFull": {}, + "tuesdayFull": "Thứ 3", + "@tuesdayFull": {}, + "saturdayShort": "T7", + "@saturdayShort": {}, + "sundayShort": "CN", + "@sundayShort": {}, + "sundayLetter": "CN", + "@sundayLetter": {}, + "pickerSpinner": "Vòng xoay", + "@pickerSpinner": {}, + "tagsSetting": "Nhãn", + "@tagsSetting": {}, + "batteryOptimizationSettingDescription": "Vô hiệu hóa tối ưu hóa pin cho ứng dụng này để ngăn chặn việc báo thức bị trễ", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Cho phép thông báo trên màn hình khóa cho báo thức và hẹn giờ", + "@allowNotificationSettingDescription": {}, + "autoStartSetting": "Tự khởi chạy", + "@autoStartSetting": {}, + "digitalClockSettingGroup": "Đồng hồ kỹ thuật số", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Bố cục", + "@layoutSettingGroup": {}, + "lessThanOneMinute": "ít hơn một phút", + "@lessThanOneMinute": {}, + "remainingTimeDesc": "Thời gian còn lại ít nhất", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Thời gian còn lại nhiều nhất", + "@remainingTimeAsc": {}, + "showFiltersSetting": "Hiển thị các bộ lọc", + "@showFiltersSetting": {}, + "showSortSetting": "Hiển thị sắp xếp", + "@showSortSetting": {}, + "notificationsSettingGroup": "Các thông báo", + "@notificationsSettingGroup": {}, + "defaultSettingGroup": "Cài đặt mặc định", + "@defaultSettingGroup": {}, + "filtersSettingGroup": "Bộ lọc", + "@filtersSettingGroup": {}, + "exportSettingsSetting": "Xuất", + "@exportSettingsSetting": {}, + "versionLabel": "Phiên bản", + "@versionLabel": {}, + "showDateSetting": "Hiển thị ngày", + "@showDateSetting": {}, + "textSettingGroup": "Văn bản", + "@textSettingGroup": {}, + "settingsTitle": "Cài đặt", + "@settingsTitle": {}, + "alignmentTop": "Trên cùng", + "@alignmentTop": {}, + "searchSettingPlaceholder": "Tìm kiếm cài đặt", + "@searchSettingPlaceholder": {} } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 66221052..77632bc2 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -674,5 +674,67 @@ "tagNamePlaceholder": "标签名称", "@tagNamePlaceholder": {}, "longDateFormatSetting": "长日期格式", - "@longDateFormatSetting": {} + "@longDateFormatSetting": {}, + "alarmRingInMessage": "闹钟将于{duration}后响铃", + "@alarmRingInMessage": {}, + "nextAlarmIn": "下一次:{duration}", + "@nextAlarmIn": {}, + "showNextAlarm": "显示下一个闹钟", + "@showNextAlarm": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "lessThanOneMinute": "不到一分钟", + "@lessThanOneMinute": {}, + "hoursString": "{count, plural, =0{} =1{1 小时} other{{count} 小时}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 分钟} other{{count} 分钟}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 秒} other{{count} 秒}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 天} other{{count} 天}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 周} other{{count} 周}}", + "@weeksString": {}, + "yearsString": "{count, plural, =0{} =1{1 年} other{{count} 年}}", + "@yearsString": {}, + "monthsString": "{count, plural, =0{} =1{1 个月} other{{count} 个月}}", + "@monthsString": {}, + "combinedTime": "{hours}与{minutes}", + "@combinedTime": {}, + "extraAnimationSettingDescription": "显示完整动画(这可能会导致低端设备出现动画卡顿现象)", + "@extraAnimationSettingDescription": {}, + "showForegroundNotification": "允许前台显示通知", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "显示持久通知以保持应用运行", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "允许显示通知", + "@notificationPermissionDescription": {}, + "interactionsSettingGroup": "互动", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "长按操作", + "@longPressActionSetting": {}, + "longPressReorderAction": "重新购买", + "@longPressReorderAction": {}, + "longPressSelectAction": "多选", + "@longPressSelectAction": {}, + "saveLogs": "保存日志", + "@saveLogs": {}, + "showErrorSnackbars": "显示错误提示", + "@showErrorSnackbars": {}, + "pickerNumpad": "数字键盘", + "@pickerNumpad": {}, + "volumeWhileTasks": "响铃时的音量", + "@volumeWhileTasks": {}, + "selectionStatus": "选择{n}", + "@selectionStatus": {}, + "selectAll": "选择全部", + "@selectAll": {}, + "clearLogs": "清除日志", + "@clearLogs": {}, + "reorder": "重新购买", + "@reorder": {} } diff --git a/lib/main.dart b/lib/main.dart index bfd15198..8e5dc5a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,21 @@ import 'dart:core'; -import 'dart:isolate'; -import 'dart:ui'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; -import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/audio/logic/audio_session.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; -import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/common/data/paths.dart'; -import 'package:clock_app/common/utils/debug.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; +import 'package:clock_app/notifications/logic/foreground_task.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; import 'package:clock_app/settings/logic/initialize_settings.dart'; -import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/system/data/device_info.dart'; +import 'package:clock_app/system/logic/background_service.dart'; import 'package:clock_app/system/logic/handle_boot.dart'; +import 'package:clock_app/system/logic/initialize_isolate_ports.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_boot_receiver/flutter_boot_receiver.dart'; @@ -25,6 +23,10 @@ import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { + // FlutterError.onError = (FlutterErrorDetails details) { + // logger.e(details.exception.toString(), stackTrace: details.stack,); + // }; + WidgetsFlutterBinding.ensureInitialized(); initializeTimeZones(); @@ -38,28 +40,21 @@ void main() async { RingtonePlayer.initialize(), initializeAudioSession(), FlutterShowWhenLocked().hide(), - initializeDatabases(), ]; await Future.wait(initializeData); + + // These rely on initializeAppDataDirectory await initializeStorage(); await initializeSettings(); - await updateAlarms("Update Alarms on Start"); - await updateTimers("Update Timers on Start"); - AppVisibility.initialize(); - ReceivePort receivePort = ReceivePort(); - IsolateNameServer.removePortNameMapping(updatePortName); - IsolateNameServer.registerPortWithName(receivePort.sendPort, updatePortName); - printIsolateInfo(); - receivePort.listen((message) { - if (message == "updateAlarms") { - ListenerManager.notifyListeners("alarms"); - } else if (message == "updateTimers") { - ListenerManager.notifyListeners("timers"); - } else if (message == "updateStopwatches") { - ListenerManager.notifyListeners("stopwatch"); - } - }); + updateAlarms("Update Alarms on Start"); + updateTimers("Update Timers on Start"); + AppVisibility.initialize(); + initForegroundTask(); + initBackgroundService(); + initializeIsolatePorts(); runApp(const App()); + + registerHeadlessBackgroundService(); } diff --git a/lib/navigation/data/tabs.dart b/lib/navigation/data/tabs.dart index dd70ab1a..e9327fe6 100644 --- a/lib/navigation/data/tabs.dart +++ b/lib/navigation/data/tabs.dart @@ -1,4 +1,5 @@ import 'package:clock_app/alarm/screens/alarm_screen.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/stopwatch/screens/stopwatch_screen.dart'; import 'package:clock_app/timer/screens/timer_screen.dart'; @@ -8,15 +9,28 @@ import 'package:clock_app/navigation/types/tab.dart'; import 'package:flutter/material.dart' hide Tab; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -List getTabs(BuildContext context) { +List getTabs(BuildContext context, + [QuickActionController? actionController]) { return [ - Tab(title: AppLocalizations.of(context)!.alarmTitle, icon: FluxIcons.alarm, widget: const AlarmScreen()), - Tab(title: AppLocalizations.of(context)!.clockTitle, icon: FluxIcons.clock, widget: const ClockScreen()), - Tab(title: AppLocalizations.of(context)!.timerTitle, icon: FluxIcons.timer, widget: const TimerScreen()), - Tab( - title: AppLocalizations.of(context)!.stopwatchTitle, - icon: FluxIcons.stopwatch, - widget: const StopwatchScreen()), -]; + Tab( + id: "alarm", + title: AppLocalizations.of(context)!.alarmTitle, + icon: FluxIcons.alarm, + widget: AlarmScreen(actionController: actionController)), + Tab( + id: "clock", + title: AppLocalizations.of(context)!.clockTitle, + icon: FluxIcons.clock, + widget: const ClockScreen()), + Tab( + id: "timer", + title: AppLocalizations.of(context)!.timerTitle, + icon: FluxIcons.timer, + widget: TimerScreen(actionController: actionController)), + Tab( + id: "stopwatch", + title: AppLocalizations.of(context)!.stopwatchTitle, + icon: FluxIcons.stopwatch, + widget: const StopwatchScreen()), + ]; } diff --git a/lib/navigation/data/visisbility.dart b/lib/navigation/data/visisbility.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 0770b4a8..2a40b788 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'dart:isolate'; import 'package:clock_app/alarm/logic/new_alarm_snackbar.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/navigation/data/tabs.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/navigation/widgets/app_navigation_bar.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; @@ -12,10 +15,46 @@ import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/screens/settings_group_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/system/logic/handle_intents.dart'; +import 'package:clock_app/system/logic/quick_actions.dart'; +import 'package:clock_app/theme/types/theme_extension.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:material_color_utilities/palettes/tonal_palette.dart'; import 'package:receive_intent/receive_intent.dart' as intent_handler; +// The callback function should always be a top-level function. +@pragma('vm:entry-point') +void startCallback() { + // The setTaskHandler function must be called to handle the task in the background. + FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +} + +class FirstTaskHandler extends TaskHandler { + SendPort? _sendPort; + + // Called when the task is started. + @override + void onStart(DateTime timestamp, SendPort? sendPort) async { + _sendPort = sendPort; + } + + @override + void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {} + + @override + void onDestroy(DateTime timestamp, SendPort? sendPort) async {} + + @override + void onNotificationButtonPressed(String id) {} + + @override + void onNotificationPressed() { + FlutterForegroundTask.launchApp("/"); + } +} + class NavScaffold extends StatefulWidget { const NavScaffold({super.key, this.initialTabIndex = 0}); @@ -27,18 +66,25 @@ class NavScaffold extends StatefulWidget { class _NavScaffoldState extends State { late int _selectedTabIndex; - late Setting useMaterialNavBarSetting; late Setting swipeActionSetting; + late Setting showForegroundSetting; late StreamSubscription _sub; late PageController _controller; + QuickActionController quickActionController = QuickActionController(); - void _onTabSelected(int index) { + void _onTabSelected(int index, [String? tabInitAction]) { ScaffoldMessenger.of(context).removeCurrentSnackBar(); setState(() { _controller.jumpToPage(index); _selectedTabIndex = index; }); + + if (tabInitAction != null) { + SchedulerBinding.instance.addPostFrameCallback((_) { + quickActionController.callAction(tabInitAction); + }); + } } void _handlePageViewChanged(int currentPageIndex) { @@ -56,8 +102,9 @@ class _NavScaffoldState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmSnackbarText(alarm), fab: true, navBar: true)); + ScaffoldMessenger.of(context).showSnackBar(getThemedSnackBar( + context, getNewAlarmText(context, alarm), + fab: true, navBar: true)); }); } @@ -86,26 +133,49 @@ class _NavScaffoldState extends State { }); } + Future _updateForegroundNotification(dynamic value) async { + if (!value) { + return FlutterForegroundTask.stopService(); + } + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.updateService( + notificationTitle: 'Foreground service is running', + notificationText: '', + callback: startCallback, + ); + } else { + return FlutterForegroundTask.startService( + notificationTitle: 'Foreground service is running', + notificationText: '', + callback: startCallback, + ); + } + } + @override void initState() { super.initState(); + initializeQuickActions(context, _onTabSelected); initReceiveIntent(); - useMaterialNavBarSetting = appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Use Material Style"); + swipeActionSetting = appSettings.getGroup("General").getSetting("Swipe Action"); + showForegroundSetting = appSettings + .getGroup("General") + .getGroup("Reliability") + .getSetting("Show Foreground Notification"); swipeActionSetting.addListener(update); - useMaterialNavBarSetting.addListener(update); + showForegroundSetting.addListener(_updateForegroundNotification); _controller = PageController(initialPage: widget.initialTabIndex); _selectedTabIndex = widget.initialTabIndex; + + _updateForegroundNotification(showForegroundSetting.value); } @override void dispose() { - useMaterialNavBarSetting.removeListener(update); swipeActionSetting.removeListener(update); + showForegroundSetting.removeListener(_updateForegroundNotification); _sub.cancel(); _controller.dispose(); super.dispose(); @@ -114,110 +184,116 @@ class _NavScaffoldState extends State { @override Widget build(BuildContext context) { Orientation orientation = MediaQuery.of(context).orientation; - final tabs = getTabs(context); - return Scaffold( - appBar: orientation == Orientation.portrait - ? AppTopBar( - title: Text( - tabs[_selectedTabIndex].title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), - ), - actions: [ - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - SettingGroupScreen(settingGroup: appSettings))); - }, - icon: - const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.8), + final tabs = getTabs(context, quickActionController); + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + ThemeSettingExtension themeSettings = + theme.extension()!; + ColorScheme colorScheme = theme.colorScheme; + + TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); + + Color materialNavColor = themeSettings.useMaterialYou + ? Color(tonalPalette + .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) + : colorScheme.surface; + + return WithForegroundTask( + child: Scaffold( + appBar: orientation == Orientation.portrait + ? AppTopBar( + titleWidget: Text( + tabs[_selectedTabIndex].title, + style: textTheme.titleMedium?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + ), ), - ], - ) - : null, - extendBody: false, - body: SafeArea( - child: Row( - children: [ - if (orientation == Orientation.landscape) - NavigationRail( - destinations: [ - for (final tab in tabs) - NavigationRailDestination( - icon: Icon(tab.icon), - label: Text(tab.title), - ) + systemNavBarColor: + themeSettings.useMaterialStyle ? materialNavColor : null, + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingGroupScreen( + settingGroup: appSettings))); + }, + icon: const Icon(FluxIcons.settings, + semanticLabel: "Settings"), + color: colorScheme.onBackground.withOpacity(0.8), + ), ], - leading: Text(tabs[_selectedTabIndex].title, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - )), - trailing: IconButton( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - SettingGroupScreen(settingGroup: appSettings))); - }, - icon: - const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.8), - ), - selectedIndex: _selectedTabIndex, - onDestinationSelected: _onTabSelected, - ), - Expanded( - child: PageView( - controller: _controller, - onPageChanged: _handlePageViewChanged, - physics: swipeActionSetting.value == SwipeAction.cardActions - ? const NeverScrollableScrollPhysics() - : null, - children: tabs.map((tab) => tab.widget).toList()), - ), - ], - ), - ), - bottomNavigationBar: orientation == Orientation.portrait - ? useMaterialNavBarSetting.value - ? NavigationBar( - labelBehavior: - NavigationDestinationLabelBehavior.onlyShowSelected, - selectedIndex: _selectedTabIndex, - onDestinationSelected: _onTabSelected, - destinations: [ + ) + : null, + bottomNavigationBar: orientation == Orientation.portrait + ? themeSettings.useMaterialStyle + ? NavigationBar( + labelBehavior: + NavigationDestinationLabelBehavior.onlyShowSelected, + selectedIndex: _selectedTabIndex, + backgroundColor: materialNavColor, + onDestinationSelected: _onTabSelected, + destinations: [ + for (final tab in tabs) + NavigationDestination( + icon: Icon(tab.icon), + label: tab.title, + ) + ], + ) + : AppNavigationBar( + selectedTabIndex: _selectedTabIndex, + onTabSelected: _onTabSelected, + ) + : null, + extendBody: false, + body: SafeArea( + child: Row( + children: [ + if (orientation == Orientation.landscape) + NavigationRail( + destinations: [ for (final tab in tabs) - NavigationDestination( + NavigationRailDestination( icon: Icon(tab.icon), - label: tab.title, + label: Text(tab.title), ) ], - ) - : AppNavigationBar( - selectedTabIndex: _selectedTabIndex, - onTabSelected: _onTabSelected, - ) - : null, + leading: Text(tabs[_selectedTabIndex].title, + style: textTheme.headlineSmall?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + )), + trailing: IconButton( + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingGroupScreen( + settingGroup: appSettings))); + }, + icon: const Icon(FluxIcons.settings, + semanticLabel: "Settings"), + color: colorScheme.onBackground.withOpacity(0.8), + ), + selectedIndex: _selectedTabIndex, + onDestinationSelected: _onTabSelected, + ), + Expanded( + child: PageView( + controller: _controller, + onPageChanged: _handlePageViewChanged, + physics: swipeActionSetting.value == SwipeAction.cardActions + ? const NeverScrollableScrollPhysics() + : null, + children: tabs.map((tab) => tab.widget).toList()), + ), + ], + ), + ), + ), ); } } diff --git a/lib/navigation/types/quick_action_controller.dart b/lib/navigation/types/quick_action_controller.dart new file mode 100644 index 00000000..778188f1 --- /dev/null +++ b/lib/navigation/types/quick_action_controller.dart @@ -0,0 +1,11 @@ +class QuickActionController { + Function(String name)? _action; + + void setAction(Function(String name)? action) { + _action = action; + } + + void callAction(String name) { + _action?.call(name); + } +} diff --git a/lib/navigation/types/tab.dart b/lib/navigation/types/tab.dart index 9f91654e..7fdfed3c 100644 --- a/lib/navigation/types/tab.dart +++ b/lib/navigation/types/tab.dart @@ -1,11 +1,13 @@ import 'package:flutter/widgets.dart'; class Tab { + final String id; final String title; final IconData icon; final Widget widget; Tab({ + required this.id, required this.title, required this.icon, required this.widget, diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index 95852f10..7c6a93d0 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -1,17 +1,23 @@ +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class AppTopBar extends StatelessWidget implements PreferredSizeWidget { - final Widget? title; + final Widget? titleWidget; + final String? title; final List? actions; + final Color? systemNavBarColor; @override Size get preferredSize => const Size(0, 56); const AppTopBar({ super.key, - this.title, + this.titleWidget, this.actions, + this.systemNavBarColor, + this.title, }); @override @@ -20,28 +26,73 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; + // final bool showBackButton = Navigator.of(context).canPop(); + + final systemNavigationBarColor = + systemNavBarColor ?? colorScheme.background; + + Brightness statusBarIconBrightness = + colorScheme.surface.computeLuminance() > 0.179 + ? Brightness.dark + : Brightness.light; + Brightness systemNavBarIconBrightness = + systemNavigationBarColor.computeLuminance() > 0.179 + ? Brightness.dark + : Brightness.light; + + Widget? barTitleWidget = titleWidget ?? + (title != null + ? Text( + title!, + style: textTheme.titleMedium?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + ), + ) + : null); + + final ModalRoute? parentRoute = ModalRoute.of(context); + final bool showBackButton = parentRoute?.impliesAppBarDismissal ?? false; + return PreferredSize( preferredSize: preferredSize, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: AppBar( systemOverlayStyle: SystemUiOverlayStyle( + systemNavigationBarColor: systemNavigationBarColor, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: systemNavBarIconBrightness, statusBarColor: colorScheme.background, statusBarIconBrightness: - colorScheme.background.computeLuminance() > 0.179 - ? Brightness.dark - : Brightness.light, // For Android (dark icons) + statusBarIconBrightness, // For Android (dark icons) ), scrolledUnderElevation: 0, toolbarHeight: preferredSize.height, - title: title, + titleSpacing: 0, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showBackButton) ...[ + IconButton( + icon: Icon(Icons.arrow_back, + color: colorScheme.onSurface.withOpacity(0.8)), + onPressed: () => Navigator.of(context).pop(), + padding: EdgeInsets.zero), + const SizedBox(width: 8) + ], + if (!showBackButton) const SizedBox(width: 16), + if (barTitleWidget != null) barTitleWidget, + ], + ), actions: [...?actions], elevation: 0, + automaticallyImplyLeading: false, iconTheme: IconThemeData( - color: colorScheme.onBackground.withOpacity(0.8), + color: colorScheme.onSurface.withOpacity(0.8), ), titleTextStyle: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground, + color: colorScheme.onSurface, ), backgroundColor: Colors.transparent, ), diff --git a/lib/navigation/widgets/search_top_bar.dart b/lib/navigation/widgets/search_top_bar.dart new file mode 100644 index 00000000..d30f3648 --- /dev/null +++ b/lib/navigation/widgets/search_top_bar.dart @@ -0,0 +1,131 @@ +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SearchParams { + final void Function(List filteredItems)? onSearch; + final List choices; + final String Function(T)? searchTermGetter; + final String placeholder; + + SearchParams( + {required this.onSearch, + required this.placeholder, + required this.choices, + required this.searchTermGetter}); +} + +class SearchTopBar extends StatefulWidget implements PreferredSizeWidget { + @override + Size get preferredSize => const Size(0, 56); + + const SearchTopBar({super.key, this.title, this.actions, this.searchParams}); + + final SearchParams? searchParams; + final List? actions; + final String? title; + + @override + State> createState() => _SearchTopBarState(); +} + +class _SearchTopBarState extends State> { + final TextEditingController _filterController = TextEditingController(); + bool _searching = false; + + _SearchTopBarState() { + _filterController.addListener(() async { + if (widget.searchParams == null) { + return; + } + if (_filterController.text.isEmpty) { + widget.searchParams!.onSearch?.call([]); + } else { + var results = extractTop( + query: _filterController.text, + choices: widget.searchParams!.choices, + // choices: [ + // ...appSettings.settings, + // ...appSettings.settingPageLinks, + // ...appSettings.settingActions + // ], + limit: 10, + cutoff: 50, + getter: widget.searchParams!.searchTermGetter, + // getter: (item) { + // // Search term includes the setting name, as well as the parent group names + // return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; + // }, + ); + + widget.searchParams!.onSearch + ?.call(results.map((result) => result.choice).toList()); + } + }); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + ColorScheme colorScheme = theme.colorScheme; + AppLocalizations localizations = AppLocalizations.of(context)!; + + if (_searching) { + return AppTopBar( + titleWidget: Expanded( + child: TextField( + autofocus: _filterController.text.isEmpty, + onTapOutside: ((event) { + FocusScope.of(context).unfocus(); + }), + controller: _filterController, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: + const OutlineInputBorder(borderSide: BorderSide.none), + fillColor: Colors.transparent, + hintText: widget.searchParams!.placeholder, + hintStyle: textTheme.bodyLarge, + ), + textAlignVertical: TextAlignVertical.center, + style: textTheme.bodyLarge, + ), + ), + actions: [ + IconButton( + onPressed: () { + _filterController.clear(); + setState(() { + _searching = false; + }); + }, + icon: const Icon(Icons.close), + ) + ], + ); + } else { + return AppTopBar( + title: widget.title, + actions: [ + ...?widget.actions, + if (widget.searchParams != null) + IconButton( + onPressed: () { + setState(() { + _searching = true; + }); + }, + icon: Icon( + Icons.search, + color: colorScheme.onBackground, + ), + ) + ], + ); + } + } +} diff --git a/lib/notifications/data/action_keys.dart b/lib/notifications/data/action_keys.dart new file mode 100644 index 00000000..8b660b65 --- /dev/null +++ b/lib/notifications/data/action_keys.dart @@ -0,0 +1,2 @@ +const String snoozeActionKey = "snooze"; +const String dismissActionKey = "dismiss"; diff --git a/lib/notifications/data/fullscreen_notification_data.dart b/lib/notifications/data/fullscreen_notification_data.dart new file mode 100644 index 00000000..72cda121 --- /dev/null +++ b/lib/notifications/data/fullscreen_notification_data.dart @@ -0,0 +1,15 @@ +import 'package:clock_app/common/types/notification_type.dart'; +import 'package:clock_app/navigation/types/routes.dart'; +import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; + +Map + alarmNotificationData = { + ScheduledNotificationType.alarm: FullScreenNotificationData( + id: 0, + route: Routes.alarmNotificationRoute, + ), + ScheduledNotificationType.timer: FullScreenNotificationData( + id: 1, + route: Routes.timerNotificationRoute, + ), +}; diff --git a/lib/notifications/data/notification_channel.dart b/lib/notifications/data/notification_channel.dart index 4c7c1282..16714689 100644 --- a/lib/notifications/data/notification_channel.dart +++ b/lib/notifications/data/notification_channel.dart @@ -1,6 +1,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/theme/theme.dart'; +const String foregroundNotificationChannelKey = 'foreground'; const String chronoNotificationChannelGroupKey = 'chrono'; const String reminderNotificationChannelKey = 'reminders'; const String stopwatchNotificationChannelKey = 'stopwatch'; @@ -22,6 +23,23 @@ final NotificationChannel alarmNotificationChannel = NotificationChannel( enableLights: false, ); + +// final NotificationChannel foregroundNotificationChannel = NotificationChannel( +// icon: 'resource://drawable/alarm_icon', +// // channelGroupKey: chronoNotificationChannelGroupKey, +// channelKey: foregroundNotificationChannelKey, +// channelName: 'Foreground Service', +// channelDescription: 'Notification channel for foreground service', +// defaultColor: defaultColorScheme.accent, +// locked: true, +// importance: NotificationImportance.Low, +// criticalAlerts: false, +// playSound: false, +// enableVibration: false, +// enableLights: false, +// ); + + final NotificationChannel reminderNotificationChannel = NotificationChannel( icon: 'resource://drawable/alarm_icon', // channelGroupKey: chronoNotificationChannelGroupKey, diff --git a/lib/notifications/logic/alarm_notifications.dart b/lib/notifications/logic/alarm_notifications.dart new file mode 100644 index 00000000..79513988 --- /dev/null +++ b/lib/notifications/logic/alarm_notifications.dart @@ -0,0 +1,241 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:awesome_notifications/android_foreground_service.dart'; +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/app.dart'; +import 'package:clock_app/common/types/notification_type.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/notifications/data/action_keys.dart'; +import 'package:clock_app/notifications/data/fullscreen_notification_data.dart'; +import 'package:clock_app/notifications/data/notification_channel.dart'; +import 'package:clock_app/alarm/logic/schedule_alarm.dart'; +import 'package:clock_app/navigation/types/routes.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; +import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; +import 'package:flutter_fgbg/flutter_fgbg.dart'; +import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; +import 'package:move_to_background/move_to_background.dart'; +import 'package:receive_intent/receive_intent.dart'; + +FGBGType appVisibilityWhenAlarmNotificationCreated = FGBGType.foreground; + +void showAlarmNotification({ + required ScheduledNotificationType type, + required List scheduleIds, + bool showSnoozeButton = true, + bool tasksRequired = false, + required String title, + required String body, + required String dismissActionLabel, + required String snoozeActionLabel, +}) { + logger.t("[showAlarmNotification]"); + FullScreenNotificationData data = alarmNotificationData[type]!; + + List actionButtons = []; + + if (scheduleIds.length > 1) { + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: dismissActionKey, + label: '$dismissActionLabel All', + actionType: ActionType.SilentAction, + autoDismissible: true, + )); + } else { + if (showSnoozeButton) { + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: snoozeActionKey, + label: snoozeActionLabel, + actionType: ActionType.SilentAction, + autoDismissible: true, + )); + } + + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: dismissActionKey, + label: "${tasksRequired ? "Solve tasks to " : ""}$dismissActionLabel", + actionType: tasksRequired ? ActionType.Default : ActionType.SilentAction, + autoDismissible: tasksRequired ? false : true, + )); + } + + AwesomeNotifications().createNotification( + content: NotificationContent( + id: data.id, + channelKey: alarmNotificationChannelKey, + title: title, + body: body, + payload: { + "scheduleIds": json.encode(scheduleIds), + "type": type.name, + "tasksRequired": tasksRequired.toString(), + }, + category: NotificationCategory.Alarm, + fullScreenIntent: true, + autoDismissible: false, + wakeUpScreen: true, + locked: true, + ), + actionButtons: actionButtons); +} + +Future removeAlarmNotification(ScheduledNotificationType type) async { + logger.t("[removeAlarmNotification]"); + FullScreenNotificationData data = alarmNotificationData[type]!; + + await AwesomeNotifications() + .cancelNotificationsByChannelKey(alarmNotificationChannelKey); + await AndroidForegroundService.stopForeground(data.id); +} + +Future closeAlarmNotification(ScheduledNotificationType type) async { + logger.t("[closeAlarmNotification]"); + final intent = await ReceiveIntent.getInitialIntent(); + + await removeAlarmNotification(type); + + await FlutterShowWhenLocked().hide(); + + // If app was launched from a notification, close the app when the notification + // is closed + if (intent?.action == "SELECT_NOTIFICATION") { + logger.t( + "[closeAlarmNotification] Moving app to background because it was launched from notification"); + await MoveToBackground.moveTaskToBack(); + // SystemNavigator.pop(); + } else { + // If notification was created while app was in background, move app back + // to background when we close the notification + if (appVisibilityWhenAlarmNotificationCreated == FGBGType.background) { + logger.t( + "[closeAlarmNotification] Moving app to background because notification moved it to foreground"); + appVisibilityWhenAlarmNotificationCreated = FGBGType.foreground; + await MoveToBackground.moveTaskToBack(); + } + } + // If we were on the alarm screen, pop it off the stack. Sometimes the system + // decides to show a heads up notification instead of a full screen one, so + // we can't always pop the top screen. + Routes.popIf(alarmNotificationData[type]?.route); + logger.t("[closeAlarmNotification] Notification closed"); +} + +Future snoozeAlarm(int scheduleId, ScheduledNotificationType type) async { + await stopAlarm(scheduleId, type, AlarmStopAction.snooze); +} + +Future dismissAlarm( + int scheduleId, ScheduledNotificationType type) async { + await stopAlarm(scheduleId, type, AlarmStopAction.dismiss); +} + +Future stopAlarm(int scheduleId, ScheduledNotificationType type, + AlarmStopAction action) async { + // Send a message to tell the alarm isolate to run the code to stop alarm + // See stopScheduledNotification in lib/alarm/logic/alarm_isolate.dart + IsolateNameServer.lookupPortByName(stopAlarmPortName) + ?.send([scheduleId, type.name, action.name]); +} + +Future dismissAlarmNotification(int scheduleId, + AlarmDismissType dismissType, ScheduledNotificationType type) async { + logger.t("[dismissAlarmNotification]"); + + switch (dismissType) { + case AlarmDismissType.dismiss: + await dismissAlarm(scheduleId, type); + break; + case AlarmDismissType.skip: + await updateAlarmById(scheduleId, (alarm) async { + alarm.setShouldSkip(true); + }); + break; + case AlarmDismissType.snooze: + await snoozeAlarm(scheduleId, type); + break; + + case AlarmDismissType.unsnooze: + await updateAlarmById(scheduleId, (alarm) async { + await alarm.cancelSnooze(); + await alarm.update("Skipped snooze"); + }); + break; + } + await closeAlarmNotification(type); +} + +Future openAlarmNotificationScreen( + FullScreenNotificationData data, + List scheduleIds, { + bool tasksOnly = false, + AlarmDismissType dismissType = AlarmDismissType.dismiss, +}) async { + logger.t("[openAlarmNotificationScreen]"); + await FlutterShowWhenLocked().show(); + // If we're already on the same notification screen, pop it off the + // stack so we don't have two of them on the stack. + if (Routes.currentRoute == data.route) { + logger.t( + "[openAlarmNotificationScreen] Popping current route because a new alarm notification needs to be pushed"); + Routes.pop(); + } + App.navigatorKey.currentState?.pushNamedAndRemoveUntil( + data.route, + (route) => (route.settings.name != data.route) || route.isFirst, + arguments: AlarmNotificationArguments( + scheduleIds: scheduleIds, + tasksOnly: tasksOnly, + dismissType: dismissType), + ); +} + +Future handleAlarmNotificationDismiss( + ReceivedAction action, AlarmDismissType dismissType) async { + logger.t("[handleAlarmNotificationDismiss]"); + + Payload payload = action.payload!; + final type = ScheduledNotificationType.values.byName((payload['type'])!); + FullScreenNotificationData data = alarmNotificationData[type]!; + bool tasksRequired = payload['tasksRequired'] == 'true'; + List scheduleIds = + (json.decode((payload['scheduleIds'])!) as List).cast(); + if (scheduleIds.isEmpty) return; + + if (tasksRequired && dismissType != AlarmDismissType.snooze) { + await openAlarmNotificationScreen(data, scheduleIds, + tasksOnly: true, dismissType: dismissType); + } else { + await dismissAlarmNotification(scheduleIds.first, dismissType, type); + } +} + +Future handleAlarmNotificationAction(ReceivedAction action) async { + logger.t("[handleAlarmNotificationAction]"); + Payload payload = action.payload!; + final type = ScheduledNotificationType.values.byName((payload['type'])!); + FullScreenNotificationData data = alarmNotificationData[type]!; + + List scheduleIds = + (json.decode((payload['scheduleIds'])!) as List).cast(); + + switch (action.buttonKeyPressed) { + case snoozeActionKey: + await handleAlarmNotificationDismiss(action, AlarmDismissType.snooze); + break; + + case dismissActionKey: + await handleAlarmNotificationDismiss(action, AlarmDismissType.dismiss); + break; + + // When notification is created or notification is clicked + default: + await openAlarmNotificationScreen(data, scheduleIds); + break; + } +} diff --git a/lib/notifications/logic/foreground_task.dart b/lib/notifications/logic/foreground_task.dart new file mode 100644 index 00000000..7cffd859 --- /dev/null +++ b/lib/notifications/logic/foreground_task.dart @@ -0,0 +1,34 @@ +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +void initForegroundTask() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: + 'This notification appears when the foreground service is running.', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + iconData: const NotificationIconData( + resType: ResourceType.drawable, + resPrefix: ResourcePrefix.ic, + name: 'alarm_icon', + ), + // buttons: [ + // const NotificationButton(id: 'sendButton', text: 'Send'), + // const NotificationButton(id: 'testButton', text: 'Test'), + // ], + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: true, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 1000 * 60, + isOnceEvent: false, + autoRunOnBoot: true, + allowWakeLock: true, + allowWifiLock: true, + ), + ); +} diff --git a/lib/notifications/logic/notification_callbacks.dart b/lib/notifications/logic/notification_callbacks.dart index 4aba2833..2901434c 100644 --- a/lib/notifications/logic/notification_callbacks.dart +++ b/lib/notifications/logic/notification_callbacks.dart @@ -1,7 +1,8 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; import 'package:clock_app/stopwatch/logic/update_stopwatch.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; @@ -17,7 +18,7 @@ Future onNotificationCreatedMethod( Payload payload = receivedNotification.payload!; int? scheduleId = int.tryParse(payload['scheduleId']); if (scheduleId == null) return; - AlarmNotificationManager.handleNotificationCreated(receivedNotification); + // AlarmNotificationManager.handleNotificationCreated(receivedNotification); break; } } @@ -35,7 +36,7 @@ Future onDismissActionReceivedMethod( switch (receivedAction.channelKey) { case alarmNotificationChannelKey: - AlarmNotificationManager.handleNotificationDismiss( + handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.dismiss); break; } @@ -48,16 +49,16 @@ Future onActionReceivedMethod(ReceivedAction receivedAction) async { switch (receivedAction.channelKey) { case alarmNotificationChannelKey: - AlarmNotificationManager.handleNotificationAction(receivedAction); + handleAlarmNotificationAction(receivedAction); break; case reminderNotificationChannelKey: switch (receivedAction.buttonKeyPressed) { case 'alarm_skip': - await AlarmNotificationManager.handleNotificationDismiss( + await handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.skip); break; case 'alarm_skip_snooze': - await AlarmNotificationManager.handleNotificationDismiss( + await handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.unsnooze); break; } diff --git a/lib/notifications/logic/notifications.dart b/lib/notifications/logic/notifications.dart index a4bd0bac..167ea376 100644 --- a/lib/notifications/logic/notifications.dart +++ b/lib/notifications/logic/notifications.dart @@ -25,7 +25,8 @@ Future initializeNotifications() async { alarmNotificationChannel, reminderNotificationChannel, stopwatchNotificationChannel, - timerNotificationChannel + timerNotificationChannel, + // foregroundNotificationChannel, ], // channelGroups: [alarmNotificationChannelGroup], debug: false, diff --git a/lib/notifications/types/notifications_controller.dart b/lib/notifications/logic/notifications_listeners.dart similarity index 89% rename from lib/notifications/types/notifications_controller.dart rename to lib/notifications/logic/notifications_listeners.dart index 3fe1870b..1cec1d33 100644 --- a/lib/notifications/types/notifications_controller.dart +++ b/lib/notifications/logic/notifications_listeners.dart @@ -1,8 +1,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/notifications/logic/notification_callbacks.dart'; -class NotificationController { - static void setListeners() { + void setNotificationListeners() { // Only after at least the action method is set, the notification events are delivered AwesomeNotifications().setListeners( onActionReceivedMethod: onActionReceivedMethod, @@ -10,4 +9,5 @@ class NotificationController { onNotificationDisplayedMethod: onNotificationDisplayedMethod, onDismissActionReceivedMethod: onDismissActionReceivedMethod); } -} + + diff --git a/lib/notifications/types/alarm_notification_arguments.dart b/lib/notifications/types/alarm_notification_arguments.dart new file mode 100644 index 00000000..0fb0b6a7 --- /dev/null +++ b/lib/notifications/types/alarm_notification_arguments.dart @@ -0,0 +1,17 @@ +class AlarmNotificationArguments { + final List scheduleIds; + final bool tasksOnly; + final AlarmDismissType dismissType; + + AlarmNotificationArguments( + {required this.scheduleIds, + required this.tasksOnly, + required this.dismissType}); +} + +enum AlarmDismissType { + dismiss, + skip, + snooze, + unsnooze, +} diff --git a/lib/notifications/types/fullscreen_notification_data.dart b/lib/notifications/types/fullscreen_notification_data.dart index 2e104834..78779836 100644 --- a/lib/notifications/types/fullscreen_notification_data.dart +++ b/lib/notifications/types/fullscreen_notification_data.dart @@ -1,6 +1,4 @@ import 'package:clock_app/common/types/json.dart'; -import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/navigation/types/routes.dart'; class FullScreenNotificationData { int id; @@ -14,14 +12,4 @@ class FullScreenNotificationData { typedef Payload = Json; -Map - alarmNotificationData = { - ScheduledNotificationType.alarm: FullScreenNotificationData( - id: 0, - route: Routes.alarmNotificationRoute, - ), - ScheduledNotificationType.timer: FullScreenNotificationData( - id: 1, - route: Routes.timerNotificationRoute, - ), -}; + diff --git a/lib/notifications/types/fullscreen_notification_manager.dart b/lib/notifications/types/fullscreen_notification_manager.dart deleted file mode 100644 index 2cd40418..00000000 --- a/lib/notifications/types/fullscreen_notification_manager.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'dart:convert'; -import 'dart:isolate'; -import 'dart:ui'; - -import 'package:awesome_notifications/android_foreground_service.dart'; -import 'package:awesome_notifications/awesome_notifications.dart'; -import 'package:clock_app/alarm/logic/alarm_isolate.dart'; -import 'package:clock_app/alarm/logic/update_alarms.dart'; -import 'package:clock_app/app.dart'; -import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/notifications/data/notification_channel.dart'; -import 'package:clock_app/alarm/logic/schedule_alarm.dart'; -import 'package:clock_app/navigation/types/routes.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_fgbg/flutter_fgbg.dart'; -import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; -import 'package:move_to_background/move_to_background.dart'; -import 'package:receive_intent/receive_intent.dart'; - -class AlarmNotificationManager { - static const String _snoozeActionKey = "snooze"; - static const String _dismissActionKey = "dismiss"; - - static FGBGType appVisibilityWhenCreated = FGBGType.foreground; - - static void showFullScreenNotification({ - required ScheduledNotificationType type, - required List scheduleIds, - bool showSnoozeButton = true, - bool tasksRequired = false, - required String title, - required String body, - required String dismissActionLabel, - required String snoozeActionLabel, - }) { - FullScreenNotificationData data = alarmNotificationData[type]!; - - List actionButtons = []; - - if (scheduleIds.length > 1) { - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _dismissActionKey, - label: '$dismissActionLabel All', - actionType: ActionType.SilentAction, - autoDismissible: true, - )); - } else { - if (showSnoozeButton) { - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _snoozeActionKey, - label: snoozeActionLabel, - actionType: ActionType.SilentAction, - autoDismissible: true, - )); - } - - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _dismissActionKey, - label: "${tasksRequired ? "Solve tasks to " : ""}$dismissActionLabel", - actionType: - tasksRequired ? ActionType.Default : ActionType.SilentAction, - autoDismissible: tasksRequired ? false : true, - )); - } - - AwesomeNotifications().createNotification( - content: NotificationContent( - id: data.id, - channelKey: alarmNotificationChannelKey, - title: title, - body: body, - payload: { - "scheduleIds": json.encode(scheduleIds), - "type": type.name, - "tasksRequired": tasksRequired.toString(), - }, - category: NotificationCategory.Alarm, - fullScreenIntent: true, - autoDismissible: false, - wakeUpScreen: true, - locked: true, - ), - actionButtons: actionButtons); - } - - static Future removeNotification(ScheduledNotificationType type) async { - FullScreenNotificationData data = alarmNotificationData[type]!; - - await AwesomeNotifications() - .cancelNotificationsByChannelKey(alarmNotificationChannelKey); - await AndroidForegroundService.stopForeground(data.id); - } - - static Future closeNotification(ScheduledNotificationType type) async { - final intent = await ReceiveIntent.getInitialIntent(); - - print(intent?.action); - - await removeNotification(type); - - await FlutterShowWhenLocked().hide(); - - // If app was launched from a notification, close the app when the notification - // is closed - if (intent?.action == "SELECT_NOTIFICATION") { - await MoveToBackground.moveTaskToBack(); - // SystemNavigator.pop(); - } else { - // If notification was created while app was in background, move app back - // to background when we close the notification - if (appVisibilityWhenCreated == FGBGType.background) { - appVisibilityWhenCreated = FGBGType.foreground; - await MoveToBackground.moveTaskToBack(); - } - } - // If we were on the alarm screen, pop it off the stack. Sometimes the system - // decides to show a heads up notification instead of a full screen one, so - // we can't always pop the top screen. - Routes.popIf(alarmNotificationData[type]?.route); - } - - static Future snoozeAlarm( - int scheduleId, ScheduledNotificationType type) async { - await stopAlarm(scheduleId, type, AlarmStopAction.snooze); - } - - static Future dismissAlarm( - int scheduleId, ScheduledNotificationType type) async { - await stopAlarm(scheduleId, type, AlarmStopAction.dismiss); - } - - static Future dismissNotification(int scheduleId, - AlarmDismissType dismissType, ScheduledNotificationType type) async { - switch (dismissType) { - case AlarmDismissType.dismiss: - await dismissAlarm(scheduleId, type); - break; - case AlarmDismissType.skip: - await updateAlarmById(scheduleId, (alarm) async { - alarm.setShouldSkip(true); - }); - break; - case AlarmDismissType.snooze: - await snoozeAlarm(scheduleId, type); - break; - - case AlarmDismissType.unsnooze: - await updateAlarmById(scheduleId, (alarm) async { - await alarm.cancelSnooze(); - await alarm.update("Skipped snooze"); - }); - break; - } - await closeNotification(type); - } - - static Future stopAlarm(int scheduleId, ScheduledNotificationType type, - AlarmStopAction action) async { - // Send a message to tell the alarm isolate to run the code to stop alarm - SendPort? sendPort = IsolateNameServer.lookupPortByName(stopAlarmPortName); - sendPort?.send([scheduleId, type.name, action.name]); - - // await closeNotification(type); - } - - static void handleNotificationCreated(ReceivedNotification notification) { - // _appVisibilityWhenCreated = AppVisibility.state; - } - - static Future openNotificationScreen( - FullScreenNotificationData data, - List scheduleIds, { - bool tasksOnly = false, - AlarmDismissType dismissType = AlarmDismissType.dismiss, - }) async { - await FlutterShowWhenLocked().show(); - // If we're already on the same notification screen, pop it off the - // stack so we don't have two of them on the stack. - if (Routes.currentRoute == data.route) { - Routes.pop(); - } - App.navigatorKey.currentState?.pushNamedAndRemoveUntil( - data.route, - (route) => (route.settings.name != data.route) || route.isFirst, - arguments: AlarmNotificationArguments( - scheduleIds: scheduleIds, - tasksOnly: tasksOnly, - dismissType: dismissType), - ); - } - - static Future handleNotificationDismiss( - ReceivedAction action, AlarmDismissType dismissType) async { - Payload payload = action.payload!; - final type = ScheduledNotificationType.values.byName((payload['type'])!); - FullScreenNotificationData data = alarmNotificationData[type]!; - bool tasksRequired = payload['tasksRequired'] == 'true'; - List scheduleIds = - (json.decode((payload['scheduleIds'])!) as List).cast(); - if (scheduleIds.isEmpty) return; - - if (tasksRequired && dismissType != AlarmDismissType.snooze) { - await openNotificationScreen(data, scheduleIds, - tasksOnly: true, dismissType: dismissType); - } else { - await dismissNotification(scheduleIds.first, dismissType, type); - } - } - - static Future handleNotificationAction(ReceivedAction action) async { - Payload payload = action.payload!; - final type = ScheduledNotificationType.values.byName((payload['type'])!); - FullScreenNotificationData data = alarmNotificationData[type]!; - - List scheduleIds = - (json.decode((payload['scheduleIds'])!) as List).cast(); - - switch (action.buttonKeyPressed) { - case _snoozeActionKey: - await handleNotificationDismiss(action, AlarmDismissType.snooze); - break; - - case _dismissActionKey: - await handleNotificationDismiss(action, AlarmDismissType.dismiss); - break; - - default: - await openNotificationScreen(data, scheduleIds); - break; - } - } -} - -class AlarmNotificationArguments { - final List scheduleIds; - final bool tasksOnly; - final AlarmDismissType dismissType; - - AlarmNotificationArguments( - {required this.scheduleIds, - required this.tasksOnly, - required this.dismissType}); -} - -enum AlarmDismissType { - dismiss, - skip, - snooze, - unsnooze, -} diff --git a/lib/settings/data/backup_options.dart b/lib/settings/data/backup_options.dart new file mode 100644 index 00000000..01c76d5a --- /dev/null +++ b/lib/settings/data/backup_options.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; + +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/app.dart'; +import 'package:clock_app/clock/types/city.dart'; +import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/backup_option.dart'; +import 'package:clock_app/theme/types/color_scheme.dart'; +import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:clock_app/timer/types/timer.dart'; +import 'package:clock_app/timer/types/timer_preset.dart'; +import 'package:clock_app/widgets/logic/update_widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Order of BackupOption matters +// tags should be before alarms and timers +// color_schemes and style_themes should be before settings +final backupOptions = [ + BackupOption( + "tags", + (context) => AppLocalizations.of(context)!.tagsSetting, + encode: () async { + return await loadTextFile("tags"); + }, + decode: (context, value) async { + final existingItems = await loadList("tags"); + final itemsToAdd = listFromString(value) + .where((tag) => + !existingItems.any((existingTag) => existingTag.isEqualTo(tag))) + .map((tag) => Tag.from(tag)); + + await saveList("tags", [...itemsToAdd, ...existingItems]); + }, + ), + BackupOption( + "color_schemes", + (context) => AppLocalizations.of(context)!.colorSchemeSetting, + encode: () async { + List colorSchemes = + await loadList("color_schemes"); + List customColorSchemes = + colorSchemes.where((scheme) => !scheme.isDefault).toList(); + return listToString(customColorSchemes); + }, + decode: (context, value) async { + final existingItems = await loadList("color_schemes"); + final itemsToAdd = listFromString(value) + .where((colorScheme) => !existingItems.any((existingColorScheme) => + existingColorScheme.isEqualTo(colorScheme))) + .map((scheme) => ColorSchemeData.from(scheme)); + + await saveList("color_schemes", [ + ...itemsToAdd, + ...existingItems, + ]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "style_themes", + (context) => AppLocalizations.of(context)!.styleThemeSetting, + encode: () async { + List styleThemes = await loadList("style_themes"); + List customThemes = + styleThemes.where((scheme) => !scheme.isDefault).toList(); + return listToString(customThemes); + }, + decode: (context, value) async { + final existingItems = await loadList("style_themes"); + final itemsToAdd = listFromString(value) + .where((theme) => !existingItems + .any((existingTheme) => existingTheme.isEqualTo(theme))) + .map((theme) => StyleTheme.from(theme)); + await saveList( + "style_themes", [...itemsToAdd, ...existingItems]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "settings", + (context) => AppLocalizations.of(context)!.settings, + encode: () async { + return json.encode(appSettings.valueToJson()); + }, + decode: (context, value) async { + appSettings.loadValueFromJson(json.decode(value)); + appSettings.callAllListeners(); + App.refreshTheme(context); + await appSettings.save(); + if (context.mounted) { + setDigitalClockWidgetData(context); + } + }, + ), + BackupOption( + "alarms", + (context) => AppLocalizations.of(context)!.alarmTitle, + encode: () async { + return await loadTextFile("alarms"); + }, + decode: (context, value) async { + final existingItems = await loadList("alarms"); + final itemsToAdd = listFromString(value) + .where((alarm) => !existingItems + .any((existingAlarm) => existingAlarm.isEqualTo(alarm))) + .map((alarm) => Alarm.fromAlarm(alarm)); + await saveList("alarms", [...itemsToAdd, ...existingItems]); + await updateAlarms("Updated alarms on importing backup"); + }, + ), + BackupOption( + "timers", + (context) => AppLocalizations.of(context)!.timerTitle, + encode: () async { + return await loadTextFile("timers"); + }, + decode: (context, value) async { + final existingItems = await loadList("timers"); + final itemsToAdd = listFromString(value) + .where((timer) => !existingItems + .any((existingTimer) => existingTimer.isEqualTo(timer))) + .map((timer) => ClockTimer.from(timer)); + await saveList("timers", [...itemsToAdd, ...existingItems]); + await updateTimers("Updated timers on importing backup"); + }, + ), + BackupOption( + "favorite_cities", + (context) => AppLocalizations.of(context)!.clockTitle, + encode: () async { + return await loadTextFile("favorite_cities"); + }, + decode: (context, value) async { + await saveList("favorite_cities", [ + ...listFromString(value), + // ...await loadList("favorite_cities") + ]); + // await updateTimers("Updated timers on importing backup"); + }, + ), + + // BackupOption( + // "stopwatches", + // (context) => AppLocalizations.of(context)!.stopwatchTitle, + // encode: () async { + // return await loadTextFile("stopwatches"); + // }, + // decode: (context, value) async { + // await saveList("stopwatches", [ + // ...listFromString(value), + // ]); + // }, + // ), + + BackupOption( + "timer_presets", + (context) => AppLocalizations.of(context)!.presetsSetting, + encode: () async { + return await loadTextFile("timer_presets"); + }, + decode: (context, value) async { + final existingItems = await loadList("timer_presets"); + final itemsToAdd = listFromString(value) + .where((preset) => !existingItems + .any((existingPreset) => existingPreset.isEqualTo(preset))) + .map((preset) => TimerPreset.from(preset)); + + await saveList( + "timer_presets", [...itemsToAdd, ...existingItems]); + }, + ), +]; diff --git a/lib/settings/data/backup_settings_schema.dart b/lib/settings/data/backup_settings_schema.dart index e8e1e6b5..41e6d1f0 100644 --- a/lib/settings/data/backup_settings_schema.dart +++ b/lib/settings/data/backup_settings_schema.dart @@ -1,14 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:clock_app/app.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/logic/backup.dart'; +import 'package:clock_app/settings/screens/backup_screen.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; -import 'package:clock_app/widgets/logic/update_widgets.dart'; +import 'package:clock_app/settings/types/setting_link.dart'; import 'package:flutter/material.dart'; -import 'package:pick_or_save/pick_or_save.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; SettingGroup backupSettingsSchema = SettingGroup( @@ -16,19 +11,13 @@ SettingGroup backupSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.backupSettingGroup, getDescription: (context) => AppLocalizations.of(context)!.backupSettingGroupDescription, +showExpandedView: false, icon: Icons.restore_rounded, [ - SettingGroup( - "Settings", - (context) => AppLocalizations.of(context)!.settingsTitle, - [ - SettingAction( + SettingPageLink( "Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, - (context) async { - saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); - }, - searchTags: ["settings", "export", "backup", "save"], + const BackupExportScreen(), getDescription: (context) => AppLocalizations.of(context)!.exportSettingsSettingDescription, ), @@ -36,45 +25,43 @@ SettingGroup backupSettingsSchema = SettingGroup( "Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, (context) async { - loadBackupFile( - (data) async { - appSettings.loadValueFromJson(json.decode(data)); - appSettings.callAllListeners(); - App.refreshTheme(context); - await appSettings.save(); - if (context.mounted) setDigitalClockWidgetData(context); - }, - ); + final data = await loadBackupFile(); + if(data == null) return; + if (context.mounted) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => BackupImportScreen(data: data))); + } }, - searchTags: ["settings", "import", "backup", "load"], getDescription: (context) => AppLocalizations.of(context)!.importSettingsSettingDescription, ), - ], - ), + // SettingAction( + // "Export", + // (context) => AppLocalizations.of(context)!.exportSettingsSetting, + // (context) async { + // saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); + // }, + // searchTags: ["settings", "export", "backup", "save"], + // getDescription: (context) => + // AppLocalizations.of(context)!.exportSettingsSettingDescription, + // ), + // SettingAction( + // "Import", + // (context) => AppLocalizations.of(context)!.importSettingsSetting, + // (context) async { + // loadBackupFile( + // (data) async { + // appSettings.loadValueFromJson(json.decode(data)); + // appSettings.callAllListeners(); + // App.refreshTheme(context); + // await appSettings.save(); + // if (context.mounted) setDigitalClockWidgetData(context); + // }, + // ); + // }, + // searchTags: ["settings", "import", "backup", "load"], + // getDescription: (context) => + // AppLocalizations.of(context)!.importSettingsSettingDescription, + // ), ], ); - -saveBackupFile(String data, String label) async { - await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: Uint8List.fromList(utf8.encode(data)), - fileName: "chrono_${label}_backup_${DateTime.now().toIso8601String()}", - ) - ], - )); -} - -loadBackupFile(Function(String) onSuccess) async { - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - getCachedFilePath: true, - ), - ); - if (result != null && result.isNotEmpty) { - File file = File(result[0]); - onSuccess(utf8.decode(file.readAsBytesSync())); - } -} diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 1843e658..1492892f 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:app_settings/app_settings.dart'; import 'package:auto_start_flutter/auto_start_flutter.dart'; import 'package:clock_app/app.dart'; +import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/clock/types/time.dart'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/types/weekday.dart'; @@ -12,12 +13,12 @@ import 'package:clock_app/common/utils/time_format.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/l10n/language_local.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; -import 'package:clock_app/settings/screens/ringtones_screen.dart'; import 'package:clock_app/settings/screens/tags_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; +import 'package:clock_app/system/logic/background_service.dart'; import 'package:clock_app/system/logic/permissions.dart'; import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/foundation.dart'; @@ -29,7 +30,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; enum TimePickerType { dial, input, spinner } -enum DurationPickerType { rings, spinner } +enum DurationPickerType { rings, spinner, numpad } SelectSettingOption _getDateSettingOption(String format) { return SelectSettingOption((context) { @@ -73,6 +74,11 @@ enum SwipeAction { switchTabs, } +enum LongPressAction { + reorder, + multiSelect, +} + final timeFormatOptions = [ SelectSettingOption( (context) => AppLocalizations.of(context)!.timeFormat12, TimeFormat.h12), @@ -107,7 +113,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Date Format", (context) => AppLocalizations.of(context)!.dateFormatSetting, dateFormatOptions, - getDescription: (context) => "How to display the dates", onChange: (context, index) async { // await HomeWidget.saveWidgetData( // "dateFormat", dateFormatOptions[index].value); @@ -118,7 +123,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Long Date Format", (context) => AppLocalizations.of(context)!.longDateFormatSetting, longDateFormatOptions, - getDescription: (context) => "How to display the dates", onChange: (context, index) async { setDigitalClockWidgetData(context); @@ -131,7 +135,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Time Format", (context) => AppLocalizations.of(context)!.timeFormatSetting, timeFormatOptions, - getDescription: (context) => "12 or 24 hour time", onChange: (context, index) async { String timeFormat = getTimeFormatString(context, timeFormatOptions[index].value); @@ -185,36 +188,58 @@ SettingGroup generalSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.pickerSpinner, DurationPickerType.spinner, ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.pickerNumpad, + DurationPickerType.numpad, + ), ], searchTags: [ "duration", "rings", "time", - "picker", + "numpad" + "picker", "dial", "input", "spinner", ]), ], ), - SelectSetting( - "Swipe Action", - (context) => AppLocalizations.of(context)!.swipeActionSetting, - [ - SelectSettingOption( - (context) => AppLocalizations.of(context)!.swipActionCardAction, - SwipeAction.cardActions, - getDescription: (context) => - AppLocalizations.of(context)!.swipeActionCardActionDescription, - ), - SelectSettingOption( - (context) => AppLocalizations.of(context)!.swipActionSwitchTabs, - SwipeAction.switchTabs, - getDescription: (context) => - AppLocalizations.of(context)!.swipeActionSwitchTabsDescription, - ) - ], - ), + SettingGroup("Interactions", + (context) => AppLocalizations.of(context)!.interactionsSettingGroup, [ + SelectSetting( + "Swipe Action", + (context) => AppLocalizations.of(context)!.swipeActionSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.swipActionCardAction, + SwipeAction.cardActions, + getDescription: (context) => + AppLocalizations.of(context)!.swipeActionCardActionDescription, + ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.swipActionSwitchTabs, + SwipeAction.switchTabs, + getDescription: (context) => + AppLocalizations.of(context)!.swipeActionSwitchTabsDescription, + ) + ], + ), + SelectSetting( + "Long Press Action", + (context) => AppLocalizations.of(context)!.longPressActionSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.longPressSelectAction, + LongPressAction.multiSelect, + ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.longPressReorderAction, + LongPressAction.reorder, + ), + ], + ), + ]), SettingPageLink( "Melodies", (context) => AppLocalizations.of(context)!.melodiesSetting, @@ -229,89 +254,120 @@ SettingGroup generalSettingsSchema = SettingGroup( searchTags: ["tags", "groups", "filter"], icon: Icons.label_outline_rounded, ), - SettingGroup("Reliability", - (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, [ - SettingAction( - "Ignore Battery Optimizations", - (context) => - AppLocalizations.of(context)!.ignoreBatteryOptimizationSetting, - (context) async { - requestBatteryOptimizationPermission( - onAlreadyGranted: () => { - showSnackBar( - context, - AppLocalizations.of(context)! - .ignoreBatteryOptimizationAlreadyGranted) - }); - }, - getDescription: (context) => - AppLocalizations.of(context)!.batteryOptimizationSettingDescription, - ), - SettingAction( - "Notifications", - (context) => - AppLocalizations.of(context)!.notificationPermissionSetting, - (context) async { - requestNotificationPermissions( - onAlreadyGranted: () => { - showSnackBar( - context, - AppLocalizations.of(context)! - .notificationPermissionAlreadyGranted) - }); - }, - ), - SettingAction( - "Vendor Specific", - (context) => AppLocalizations.of(context)!.vendorSetting, - (context) => launchUrl(Uri.parse("https://dontkillmyapp.com")), - getDescription: (context) => - AppLocalizations.of(context)!.vendorSettingDescription, - ), - SettingAction( - "Disable Battery Optimization", - (context) => AppLocalizations.of(context)!.batteryOptimizationSetting, - (context) async { - AppSettings.openAppSettings( - type: AppSettingsType.batteryOptimization); - }, - getDescription: (context) => - AppLocalizations.of(context)!.batteryOptimizationSettingDescription, - ), - SettingAction( - "Allow Notifications", - (context) => AppLocalizations.of(context)!.allowNotificationSetting, - (context) async { - AppSettings.openAppSettings(type: AppSettingsType.notification); - }, - getDescription: (context) => - AppLocalizations.of(context)!.allowNotificationSettingDescription, - ), - SettingAction( - "Auto Start", - (context) => AppLocalizations.of(context)!.autoStartSetting, - (context) async { - try { - //check auto-start availability. - var test = (await isAutoStartAvailable) ?? false; - //if available then navigate to auto-start setting page. - if (test) { - await getAutoStartPermission(); - } else { - // ignore: use_build_context_synchronously - if (context.mounted) { - showSnackBar( - context, "Auto Start is not available for your device"); + SettingGroup( + "Reliability", + (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, + [ + SwitchSetting( + "Show Foreground Notification", + (context) => AppLocalizations.of(context)!.showForegroundNotification, + false, + getDescription: (context) => AppLocalizations.of(context)! + .showForegroundNotificationDescription, + searchTags: ["foreground", "notification"], + ), + SliderSetting( + "backgroundServiceInterval", + (context) => + AppLocalizations.of(context)!.backgroundServiceIntervalSetting, + 15, + 300, + 60, + unit: "m", + snapLength: 15, + getDescription: (context) => AppLocalizations.of(context)! + .backgroundServiceIntervalSettingDescription, + searchTags: ["background", "service", "interval"], + onChange: (context, value) { + initBackgroundService(interval: value.toInt()); + }, + ), + SettingAction( + "Ignore Battery Optimizations", + (context) => + AppLocalizations.of(context)!.ignoreBatteryOptimizationSetting, + (context) async { + requestBatteryOptimizationPermission( + onAlreadyGranted: () => { + showSnackBar( + context, + AppLocalizations.of(context)! + .ignoreBatteryOptimizationAlreadyGranted) + }); + }, + getDescription: (context) => AppLocalizations.of(context)! + .batteryOptimizationSettingDescription, + ), + SettingAction( + "Notifications", + (context) => + AppLocalizations.of(context)!.notificationPermissionSetting, + (context) async { + requestNotificationPermissions( + onAlreadyGranted: () => { + showSnackBar( + context, + AppLocalizations.of(context)! + .notificationPermissionAlreadyGranted) + }); + }, + getDescription: (context) => + AppLocalizations.of(context)!.notificationPermissionDescription, + ), + SettingAction( + "Vendor Specific", + (context) => AppLocalizations.of(context)!.vendorSetting, + (context) => launchUrl(Uri.parse("https://dontkillmyapp.com")), + getDescription: (context) => + AppLocalizations.of(context)!.vendorSettingDescription, + ), + SettingAction( + "Disable Battery Optimization", + (context) => AppLocalizations.of(context)!.batteryOptimizationSetting, + (context) async { + AppSettings.openAppSettings( + type: AppSettingsType.batteryOptimization); + }, + getDescription: (context) => AppLocalizations.of(context)! + .batteryOptimizationSettingDescription, + + ), + SettingAction( + "Allow Notifications", + (context) => AppLocalizations.of(context)!.allowNotificationSetting, + (context) async { + AppSettings.openAppSettings(type: AppSettingsType.notification); + }, + getDescription: (context) => + AppLocalizations.of(context)!.allowNotificationSettingDescription, + ), + SettingAction( + "Auto Start", + (context) => AppLocalizations.of(context)!.autoStartSetting, + (context) async { + try { + //check auto-start availability. + var test = (await isAutoStartAvailable) ?? false; + //if available then navigate to auto-start setting page. + if (test) { + await getAutoStartPermission(); + } else { + // ignore: use_build_context_synchronously + if (context.mounted) { + showSnackBar( + context, "Auto Start is not available for your device"); + } } + } on PlatformException catch (e) { + if (kDebugMode) print(e.message); } - } on PlatformException catch (e) { - if (kDebugMode) print(e.message); - } - }, - getDescription: (context) => - AppLocalizations.of(context)!.autoStartSettingDescription, - ), - ]), + }, + getDescription: (context) => + AppLocalizations.of(context)!.autoStartSettingDescription, + ), + ], + searchTags: ["reliability", "battery", "optimization", "notifications"], + ), SelectSetting( "Default Tab", (context) => AppLocalizations.of(context)!.defaultPageSetting, @@ -334,26 +390,6 @@ SettingGroup generalSettingsSchema = SettingGroup( ), ], ), - SettingGroup("Animations", - (context) => AppLocalizations.of(context)!.animationSettingGroup, [ - SliderSetting( - "Animation Speed", - (context) => AppLocalizations.of(context)!.animationSpeedSetting, - 0.5, - 2, - 1, - // unit: 'm', - snapLength: 0.1, - // enableConditions: [ - // ValueCondition( - // ["Show Upcoming Alarm Notifications"], (value) => value), - // ], - ), - SwitchSetting( - "Extra Animations", - (context) => AppLocalizations.of(context)!.extraAnimationSetting, - false), - ]) ], icon: FluxIcons.settings, getDescription: (context) => diff --git a/lib/settings/data/localized_names.dart b/lib/settings/data/localized_names.dart deleted file mode 100644 index 06845ff6..00000000 --- a/lib/settings/data/localized_names.dart +++ /dev/null @@ -1,29 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// -// String getLocalizedSettingName(String name, BuildContext context) { -// switch (name) { -// case "General": -// return AppLocalizations.of(context)!.generalSettingGroup; -// case "Appearance": -// return AppLocalizations.of(context)!.appearanceSettingGroup; -// case "Alarm": -// return AppLocalizations.of(context)!.alarmTitle; -// case "Timer": -// return AppLocalizations.of(context)!.timerTitle; -// case "Stopwatch": -// return AppLocalizations.of(context)!.stopwatchTitle; -// case "Clock": -// return AppLocalizations.of(context)!.clockTitle; -// case "Developer Options" -// default: -// return name; -// } -// } -// -// String getLocalizedSettingDescription(String name, BuildContext context) { -// switch (name) { -// default: -// return name; -// } -// } diff --git a/lib/settings/data/settings_schema.dart b/lib/settings/data/settings_schema.dart index 8accc4b6..3df580f2 100644 --- a/lib/settings/data/settings_schema.dart +++ b/lib/settings/data/settings_schema.dart @@ -1,19 +1,22 @@ +import 'package:clock_app/alarm/data/alarm_app_settings_schema.dart'; +import 'package:clock_app/clock/data/clock_settings_schema.dart'; +import 'package:clock_app/developer/data/developer_settings_schema.dart'; import 'package:clock_app/settings/data/accessibility_settings_schema.dart'; -import 'package:clock_app/settings/data/alarm_app_settings_schema.dart'; -import 'package:clock_app/settings/data/appearance_settings_schema.dart'; import 'package:clock_app/settings/data/backup_settings_schema.dart'; -import 'package:clock_app/settings/data/developer_settings_schema.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; -import 'package:clock_app/settings/data/stopwatch_settings_schema.dart'; -import 'package:clock_app/settings/data/timer_app_settings_schema.dart'; -import 'package:clock_app/settings/data/widget_settings_schema.dart'; import 'package:clock_app/settings/screens/about_screen.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; +import 'package:clock_app/stopwatch/data/stopwatch_settings_schema.dart'; +import 'package:clock_app/theme/data/appearance_settings_schema.dart'; +import 'package:clock_app/timer/data/timer_app_settings_schema.dart'; +import 'package:clock_app/widgets/data/widget_settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -const int settingsSchemaVersion = 5; + +// Increment this after every schema change +const int settingsSchemaVersion = 7; SettingGroup appSettings = SettingGroup( "Settings", @@ -24,6 +27,7 @@ SettingGroup appSettings = SettingGroup( generalSettingsSchema, appearanceSettingsSchema, alarmAppSettingsSchema, + clockSettingsSchema, timerAppSettingsSchema, stopwatchSettingsSchema, widgetSettingSchema, @@ -39,5 +43,3 @@ SettingGroup appSettings = SettingGroup( ], ); - -// Settings appSettings = Settings(settingsItems); diff --git a/lib/settings/logic/backup.dart b/lib/settings/logic/backup.dart new file mode 100644 index 00000000..567964c5 --- /dev/null +++ b/lib/settings/logic/backup.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; + +Future saveBackupFile(String data) async { + return await FilePicker.platform.saveFile( + bytes: Uint8List.fromList(utf8.encode(data)), + fileName: + "chrono_backup_${DateTime.now().toIso8601String().split(".")[0]}.json", + ); +} + +Future loadBackupFile() async { + FilePickerResult? result = await FilePicker.platform + .pickFiles(type: FileType.any, allowMultiple: false); + + if (result != null && result.files.isNotEmpty) { + File file = File(result.files.single.path!); + return utf8.decode(file.readAsBytesSync()); + } + return null; +} diff --git a/lib/settings/logic/get_setting_widget.dart b/lib/settings/logic/get_setting_widget.dart index f1a70fd1..71ac63fc 100644 --- a/lib/settings/logic/get_setting_widget.dart +++ b/lib/settings/logic/get_setting_widget.dart @@ -32,12 +32,12 @@ List getSettingWidgets( bool isAppSettings = true, }) { bool showExtraAnimations = appSettings - .getGroup("General") + .getGroup("Appearance") .getGroup("Animations") .getSetting("Extra Animations") .value; double animationSpeed = appSettings - .getGroup("General") + .getGroup("Appearance") .getGroup("Animations") .getSetting("Animation Speed") .value; @@ -184,7 +184,7 @@ Widget? getSettingItemWidget( setting: item, showAsCard: showAsCard, ); - } else if (item is ListSetting) { + } else if (item is CustomizableListSetting) { return ListSettingCard( setting: item, showAsCard: showAsCard, diff --git a/lib/settings/logic/initialize_settings.dart b/lib/settings/logic/initialize_settings.dart index 5852ec47..b621cd3c 100644 --- a/lib/settings/logic/initialize_settings.dart +++ b/lib/settings/logic/initialize_settings.dart @@ -4,8 +4,9 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/alarm_event.dart'; -import 'package:clock_app/audio/logic/system_ringtones.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/clock/data/default_favorite_cities.dart'; +import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/common/data/default_tags.dart'; import 'package:clock_app/common/data/paths.dart'; @@ -26,7 +27,6 @@ import 'package:clock_app/timer/types/timer_preset.dart'; import 'package:flutter/foundation.dart'; import 'package:get_storage/get_storage.dart'; - Future _clearSettings() async { // List timers = await loadList('timers'); // List alarms = await loadList('alarms'); @@ -75,6 +75,8 @@ Future initializeStorage([bool clearSettingsOnDebug = true]) async { await initList("timer_presets", defaultTimerPresets); await initList("ringtones", await getSystemRingtones()); await initTextFile("time_format_string", "h:mm a"); + await initializeDatabases(); + // await initTextFile("", "0"); // await initTextFile("timers-sort-index", "0"); } diff --git a/lib/settings/screens/about_screen.dart b/lib/settings/screens/about_screen.dart index 4084948a..1fe416fb 100644 --- a/lib/settings/screens/about_screen.dart +++ b/lib/settings/screens/about_screen.dart @@ -24,10 +24,7 @@ class AboutScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.aboutSettingGroup, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.aboutSettingGroup, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart new file mode 100644 index 00000000..c6762786 --- /dev/null +++ b/lib/settings/screens/backup_screen.dart @@ -0,0 +1,197 @@ +import 'dart:convert'; + +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/data/backup_options.dart'; +import 'package:clock_app/settings/logic/backup.dart'; +import 'package:clock_app/settings/types/backup_option.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class BackupOptionCheckBox extends StatelessWidget { + const BackupOptionCheckBox( + {super.key, required this.option, required this.onChanged}); + + final BackupOption option; + final void Function(bool?) onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Checkbox( + // checkColor: Colors.white, + // fillColor: MaterialStateProperty.resolveWith(getColor), + value: option.selected, + onChanged: onChanged, + ), + Text( + option.getName(context), + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ); + } +} + +class BackupExportScreen extends StatefulWidget { + const BackupExportScreen({ + super.key, + }); + + @override + State createState() => _BackupExportScreenState(); +} + +class _BackupExportScreenState extends State { + @override + void initState() { + for (var option in backupOptions) { + option.selected = true; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppTopBar( + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TextButton( + onPressed: () async { + try { + final backupData = {}; + for (var option in backupOptions) { + if (option.selected) { + backupData[option.key] = await option.encode(); + } + } + final result = + await saveBackupFile(json.encode(backupData)); + if (result == null) return; + if (context.mounted) { + showSnackBar(context, "Export successful!"); + Navigator.pop(context); + } + } catch (e) { + logger.e("Error exporting: $e"); + } + }, + child: + Text(AppLocalizations.of(context)!.exportSettingsSetting)), + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + ...backupOptions.map( + (option) => BackupOptionCheckBox( + option: option, + onChanged: (bool? value) { + setState(() { + option.selected = value ?? false; + }); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} + +class BackupImportScreen extends StatefulWidget { + const BackupImportScreen({ + super.key, + required this.data, + }); + + final String data; + + @override + State createState() => _BackupImportScreenState(); +} + +class _BackupImportScreenState extends State { + late final List importOptions = []; + late final Json dataJson; + + @override + void initState() { + dataJson = json.decode(widget.data); + + if (dataJson != null) { + for (var option in backupOptions) { + option.selected = true; + if (dataJson!.keys.contains(option.key)) { + importOptions.add(option); + } + } + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: SearchTopBar( + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TextButton( + onPressed: () async { + try { + if (dataJson == null) return; + for (var option in importOptions) { + if (option.selected && context.mounted) { + await option.decode(context, dataJson![option.key]); + } + } + if (context.mounted) { + showSnackBar(context, "Import successful!"); + Navigator.pop(context); + } + } catch (e) { + logger.e("Error importing: $e"); + } + }, + child: + Text(AppLocalizations.of(context)!.importSettingsSetting)), + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + ...importOptions.map( + (option) => BackupOptionCheckBox( + option: option, + onChanged: (bool? value) { + setState(() { + option.selected = value ?? false; + }); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} diff --git a/lib/settings/screens/contributors.dart b/lib/settings/screens/contributors.dart index 81e751c1..dfbccce9 100644 --- a/lib/settings/screens/contributors.dart +++ b/lib/settings/screens/contributors.dart @@ -24,10 +24,7 @@ class ContributorsScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.contributorsSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.contributorsSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/donors.dart b/lib/settings/screens/donors.dart index 671c7a07..0ea29c02 100644 --- a/lib/settings/screens/donors.dart +++ b/lib/settings/screens/donors.dart @@ -24,10 +24,7 @@ class DonorsScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.donorsSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.donorsSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/licenses.dart b/lib/settings/screens/licenses.dart index e675d1b7..6a905462 100644 --- a/lib/settings/screens/licenses.dart +++ b/lib/settings/screens/licenses.dart @@ -16,10 +16,7 @@ class LicensesScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.openSourceLicensesSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.openSourceLicensesSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/list_filter_settings_screen.dart b/lib/settings/screens/list_filter_settings_screen.dart new file mode 100644 index 00000000..2d8cfb58 --- /dev/null +++ b/lib/settings/screens/list_filter_settings_screen.dart @@ -0,0 +1,67 @@ +import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/widgets/fab.dart'; +import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; +import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/widgets/tag_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ListFilterSettingsScreen extends StatefulWidget { + const ListFilterSettingsScreen({ + super.key, + }); + + @override + State createState() => + _ListFilterSettingsScreenState(); +} + +class _ListFilterSettingsScreenState extends State { + final _listController = PersistentListController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), + body: Stack( + children: [ + Column( + children: [ + Expanded( + child: PersistentListView( + saveTag: 'tags', + listController: _listController, + itemBuilder: (tag) => TagCard( + key: ValueKey(tag), + tag: tag, + onPressDelete: () => _listController.deleteItem(tag), + onPressDuplicate: () => _listController.duplicateItem(tag), + ), + onTapItem: (tag, index) async { + Tag? newTag = await showTagEditor(tag); + if (newTag == null) return; + tag.copyFrom(newTag); + _listController.changeItems((tags) {}); + }, + // onDeleteItem: _handleDeleteTimer, + placeholderText: "No tags created", + reloadOnPop: true, + isSelectable: true, + ), + ), + ], + ), + FAB( + bottomPadding: 8, + onPressed: () async { + Tag? tag = await showTagEditor(); + if (tag == null) return; + _listController.addItem(tag); + }, + ) + ], + ), + ); + } +} diff --git a/lib/settings/screens/restore_defaults_screen.dart b/lib/settings/screens/restore_defaults_screen.dart index 3c9ec2a1..b2322e5f 100644 --- a/lib/settings/screens/restore_defaults_screen.dart +++ b/lib/settings/screens/restore_defaults_screen.dart @@ -2,7 +2,7 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_item.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -65,7 +65,7 @@ class _RestoreDefaultScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar( + appBar: AppTopBar( title: AppLocalizations.of(context)!.restoreSettingGroup, ), body: SingleChildScrollView( diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart deleted file mode 100644 index c34549f3..00000000 --- a/lib/settings/screens/ringtones_screen.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'dart:io'; -import 'package:clock_app/audio/types/ringtone_player.dart'; -import 'package:clock_app/common/types/file_item.dart'; -import 'package:clock_app/common/utils/list_storage.dart'; -import 'package:clock_app/common/widgets/fab.dart'; -import 'package:clock_app/common/widgets/file_item_card.dart'; -import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; -import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/types/setting_item.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:path/path.dart'; -import 'package:pick_or_save/pick_or_save.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class RingtonesScreen extends StatefulWidget { - const RingtonesScreen({ - super.key, - }); - - @override - State createState() => _RingtonesScreenState(); -} - -class _RingtonesScreenState extends State { - final _listController = PersistentListController(); - List searchedItems = []; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - RingtonePlayer.stop(); - super.dispose(); - } - - void _onDeleteItem(FileItem fileItem) { - if (!fileItem.isDeletable) return; - final file = File(fileItem.uri); - file.deleteSync(); - RingtonePlayer.stop(); - } - - @override - Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - TextTheme textTheme = theme.textTheme; - - return Scaffold( - appBar: SettingsTopBar( - title: AppLocalizations.of(context)!.melodiesSetting, - ), - body: Stack( - children: [ - Column( - children: [ - Expanded( - flex: 1, - child: PersistentListView( - saveTag: 'ringtones', - listController: _listController, - itemBuilder: (fileItem) => FileItemCard( - key: ValueKey(fileItem), - fileItem: fileItem, - onPressDelete: () => _listController.deleteItem(fileItem), - ), - onTapItem: (fileItem, index) { - // widget.setting.setValue(context, themeItem); - // _listController.reload(); - }, - onDeleteItem: _onDeleteItem, - isDuplicateEnabled: false, - placeholderText: "No melodies", - reloadOnPop: true, - ), - ), - ], - ), - FAB( - // icon: Icons.music_note_rounded, - bottomPadding: 8, - onPressed: () async { - RingtonePlayer.stop(); - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - mimeTypesFilter: ['audio/*'], - getCachedFilePath: true, - enableMultipleSelection: true, - ), - ); - if (result != null && result.isNotEmpty) { - for (String uri in result) { - final metadata = await PickOrSave() - .fileMetaData(params: FileMetadataParams(filePath: uri)); - var name = metadata.displayName ?? "File"; - name = basenameWithoutExtension(name) - .replaceAll(RegExp(r"[0-9]+"), "") - .replaceAll(".", ""); - final fileItem = FileItem(name, uri, FileItemType.audio); - fileItem.uri = - await saveRingtone(fileItem.id.toString(), uri); - _listController.addItem(fileItem); - } - } - - // Item? themeItem = widget.createThemeItem(); - // await _openCustomizeItemScreen( - // themeItem, - // onSave: (newThemeItem) { - // _listController.addItem(newThemeItem); - // }, - // isNewItem: true, - // ); - }, - ), - // FAB( - // index: 1, - // icon: Icons.create_new_folder_rounded, - // bottomPadding: 8, - // onPressed: () async { - // RingtonePlayer.stop(); - // String? result = await PickOrSave() - // .directoryPicker(params: const DirectoryPickerParams()); - // - // if (result != null && result.isNotEmpty) { - // List? documentFiles = - // await PickOrSave().directoryDocumentsPicker( - // params: DirectoryDocumentsPickerParams( - // directoryUri: result, - // recurseDirectories: true, - // // allowedExtensions: [".pdf"], - // mimeTypesFilter: ["audio/*"], - // ), - // ); - // if (documentFiles != null) { - // DocumentFile documentFile = documentFiles[0]; - // for (var document in documentFiles) { - // print("${document.name} ${document.uri}"); - // } - // } - // - // // final directory = Directory(result); - // // String name = result.split("/").last; - // String name = basename( - // result.replaceAll("%3A", ":").replaceAll("%2F", "/")); - // // final metadata = await PickOrSave() - // // .fileMetaData(params: FileMetadataParams(filePath: result)); - // print("================ ${name}"); - // final fileItem = FileItem(name, result, FileItemType.directory); - // // fileItem.uri = - // // await saveRingtone(fileItem.id.toString(), result); - // _listController.addItem(fileItem); - // } - // }, - // ) - ], - ), - ); - } -} diff --git a/lib/settings/screens/settings_group_screen.dart b/lib/settings/screens/settings_group_screen.dart index e647bbcf..27737e21 100644 --- a/lib/settings/screens/settings_group_screen.dart +++ b/lib/settings/screens/settings_group_screen.dart @@ -1,3 +1,5 @@ +import 'package:clock_app/common/widgets/list/static_list_view.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/logic/get_setting_widget.dart'; import 'package:clock_app/settings/screens/restore_defaults_screen.dart'; @@ -7,7 +9,6 @@ import 'package:clock_app/settings/types/setting_link.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:clock_app/settings/widgets/search_setting_card.dart'; import 'package:clock_app/settings/widgets/setting_page_link_card.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; import 'package:flutter/material.dart'; class SettingGroupScreen extends StatefulWidget { @@ -22,7 +23,7 @@ class SettingGroupScreen extends StatefulWidget { } class _SettingGroupScreenState extends State { - List searchedItems = []; + List _searchedItems = []; @override void initState() { @@ -31,59 +32,65 @@ class _SettingGroupScreenState extends State { @override Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + List getSearchItemWidgets() { - return searchedItems.map((item) { + return _searchedItems.map((item) { return SearchSettingCard(settingItem: item); }).toList(); } return Scaffold( - appBar: SettingsTopBar( + appBar: SearchTopBar( title: widget.settingGroup.displayName(context), - onSearch: (settingItems) { - setState(() { - searchedItems = settingItems; - }); - }, - showSearch: widget.settingGroup.isSearchable, + searchParams: widget.settingGroup.isSearchable + ? SearchParams( + onSearch: (settingItems) { + setState(() { + _searchedItems = settingItems; + }); + }, + placeholder: localizations.searchSettingPlaceholder, + choices: [ + ...appSettings.settings, + ...appSettings.settingPageLinks, + ...appSettings.settingActions + ], + searchTermGetter: (item) { + // Search term includes the setting name, as well as the parent group names and the tags + return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; + }, + ) + : null, + // showSearch: widget.settingGroup.isSearchable, ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: searchedItems.isEmpty - ? [ - ...getSettingWidgets( - widget.settingGroup.settingItems, - checkDependentEnableConditions: () => setState(() {}), - onSettingChanged: () { - if (widget.isAppSettings) { - appSettings.save(); - } - }, - isAppSettings: widget.isAppSettings, - ), - if (widget.isAppSettings) - SettingPageLinkCard( - setting: SettingPageLink( - 'Restore default values', - (context) => AppLocalizations.of(context)! - .restoreSettingGroup, - RestoreDefaultScreen( - settingGroup: widget.settingGroup, - onRestore: () async { - await appSettings.save(); - setState(() {}); - }, - ))), - const SizedBox(height: 16), - ] - : [ - ...getSearchItemWidgets(), - const SizedBox(height: 16), - ], - ), - ), + body: StaticListView( + children: _searchedItems.isEmpty + ? [ + ...getSettingWidgets( + widget.settingGroup.settingItems, + checkDependentEnableConditions: () => setState(() {}), + onSettingChanged: () { + if (widget.isAppSettings) { + appSettings.save(); + } + }, + isAppSettings: widget.isAppSettings, + ), + if (widget.isAppSettings) + SettingPageLinkCard( + setting: SettingPageLink( + 'restore_default_values', + (context) => localizations.restoreSettingGroup, + RestoreDefaultScreen( + settingGroup: widget.settingGroup, + onRestore: () async { + await appSettings.save(); + setState(() {}); + }, + ))), + ] + : getSearchItemWidgets(), ), ); } diff --git a/lib/settings/screens/tags_screen.dart b/lib/settings/screens/tags_screen.dart index eddf2a98..1173c84c 100644 --- a/lib/settings/screens/tags_screen.dart +++ b/lib/settings/screens/tags_screen.dart @@ -3,7 +3,6 @@ import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; import 'package:clock_app/settings/widgets/tag_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -48,7 +47,7 @@ class _TagsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar(title: AppLocalizations.of(context)!.tagsSetting), + appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), body: Stack( children: [ Column( @@ -72,6 +71,7 @@ class _TagsScreenState extends State { // onDeleteItem: _handleDeleteTimer, placeholderText: "No tags created", reloadOnPop: true, + isSelectable: true, ), ), ], diff --git a/lib/settings/types/backup_option.dart b/lib/settings/types/backup_option.dart new file mode 100644 index 00000000..f038bcca --- /dev/null +++ b/lib/settings/types/backup_option.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class BackupOption { + final String Function(BuildContext context) getName; + final String key; + final Future Function() encode; + final Future Function(BuildContext context, dynamic value) decode; + bool selected = true; + + BackupOption(this.key, this.getName, + {required this.encode, required this.decode}); +} diff --git a/lib/settings/types/setting.dart b/lib/settings/types/setting.dart index a699cf64..1acbdae7 100644 --- a/lib/settings/types/setting.dart +++ b/lib/settings/types/setting.dart @@ -68,16 +68,17 @@ abstract class Setting extends SettingItem { } } -class ListSetting extends Setting> { +class CustomizableListSetting + extends Setting> { List possibleItems; Widget Function(T item, [VoidCallback?, VoidCallback?]) cardBuilder; Widget Function(T item) addCardBuilder; Widget Function(T item)? itemPreviewBuilder; // The widget that will be used to display the value of this setting. - Widget Function(BuildContext context, ListSetting setting) + Widget Function(BuildContext context, CustomizableListSetting setting) valueDisplayBuilder; - ListSetting( + CustomizableListSetting( String name, String Function(BuildContext) getLocalizedName, List defaultValue, @@ -104,8 +105,8 @@ class ListSetting extends Setting> { ); @override - ListSetting copy() { - return ListSetting( + CustomizableListSetting copy() { + return CustomizableListSetting( name, getLocalizedName, _value, @@ -151,6 +152,74 @@ class ListSetting extends Setting> { } } +class ListSetting extends Setting> { + List possibleItems; + Widget Function(T item, [VoidCallback?, VoidCallback?]) cardBuilder; + Widget Function(T item) addCardBuilder; + // The widget that will be used to display the value of this setting. + + ListSetting( + String name, + String Function(BuildContext) getLocalizedName, + List defaultValue, + this.possibleItems, { + required this.cardBuilder, + required this.addCardBuilder, + String Function(BuildContext) getDescription = defaultDescription, + void Function(BuildContext, List)? onChange, + bool isVisual = true, + List enableConditions = const [], + List searchTags = const [], + }) : super( + name, + getLocalizedName, + getDescription, + copyItemList(defaultValue), + onChange, + enableConditions, + searchTags, + isVisual, + valueCopyGetter: copyItemList, + ); + + @override + ListSetting copy() { + return ListSetting( + name, + getLocalizedName, + _value, + possibleItems, + cardBuilder: cardBuilder, + addCardBuilder: addCardBuilder, + getDescription: getDescription, + onChange: onChange, + enableConditions: enableConditions, + isVisual: isVisual, + searchTags: searchTags, + ); + } + + Widget getItemAddCard(T item) { + return addCardBuilder(item); + } + + Widget getItemCard(T item, + {VoidCallback? onDelete, VoidCallback? onDuplicate}) { + return cardBuilder(item, onDelete, onDuplicate); + } + + @override + dynamic valueToJson() { + return _value.map((e) => e.toJson()).toList(); + } + + @override + void loadValueFromJson(dynamic value) { + if (value == null) return; + _value = (value as List).map((e) => fromJsonFactories[T]!(e) as T).toList(); + } +} + class CustomSetting extends Setting { // The screen that will be navigated to when this setting is tapped. Widget Function(BuildContext, CustomSetting) screenBuilder; diff --git a/lib/settings/types/setting_enable_condition.dart b/lib/settings/types/setting_enable_condition.dart index 439ab64b..17248d12 100644 --- a/lib/settings/types/setting_enable_condition.dart +++ b/lib/settings/types/setting_enable_condition.dart @@ -4,13 +4,13 @@ import 'package:clock_app/settings/types/setting_item.dart'; // TODO: OMG ALL THESE NAMES ARE SO BAD, PLEASE THINK OF NEW ONES :( +// Allows us to check conditions for enabling settings abstract class EnableConditionParameter { void setupEnableSettings(SettingGroup group, SettingItem item); void setupChangesEnableCondition(SettingGroup group, SettingItem item); EnableConditionEvaluator getEvaluator(SettingGroup group); } - class GeneralCondition extends EnableConditionParameter { bool Function() condition; @@ -18,17 +18,16 @@ class GeneralCondition extends EnableConditionParameter { @override EnableConditionEvaluator getEvaluator(SettingGroup group) { - return GeneralConditionEvaluator (condition); + return GeneralConditionEvaluator(condition); } @override void setupEnableSettings(SettingGroup group, SettingItem item) { item.enableSettings.add(getEvaluator(group)); - } + } @override - void setupChangesEnableCondition(SettingGroup group, SettingItem item) { - } + void setupChangesEnableCondition(SettingGroup group, SettingItem item) {} } class ValueCondition extends EnableConditionParameter { diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart index 8d8987f0..c2a7cc41 100644 --- a/lib/settings/types/setting_group.dart +++ b/lib/settings/types/setting_group.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; @@ -100,7 +101,12 @@ class SettingGroup extends SettingItem { } SettingGroup getGroup(String name) { - return _settingGroups.firstWhere((item) => item.name == name); + try { + return _settingGroups.firstWhere((item) => item.name == name); + } catch (e) { + logger.e("Could not find setting group '$name'"); + rethrow; + } } Setting getSettingFromPath(List path) { @@ -130,7 +136,7 @@ class SettingGroup extends SettingItem { try { return _settingItems.firstWhere((item) => item.name == name); } catch (e) { - debugPrint("Could not find setting item $name: $e"); + logger.e("Could not find setting item $name: $e"); rethrow; } } @@ -139,7 +145,7 @@ class SettingGroup extends SettingItem { try { return _settings.firstWhere((item) => item.name == name); } catch (e) { - debugPrint("Could not find setting $name: $e"); + logger.e("Could not find setting $name: $e"); rethrow; } } @@ -165,6 +171,10 @@ class SettingGroup extends SettingItem { } } + bool isEqualTo(SettingGroup other) { + return json.encode(valueToJson()) == json.encode(other.valueToJson()); + } + @override dynamic valueToJson() { Json json = {}; @@ -226,7 +236,7 @@ class SettingGroup extends SettingItem { } } } catch (e) { - debugPrint( + logger.e( "Error migrating value in setting group ($name): ${e.toString()}"); } } @@ -234,7 +244,7 @@ class SettingGroup extends SettingItem { if (value != null) setting.loadValueFromJson(value[setting.name]); } } catch (e) { - debugPrint( + logger.e( "Error loading value from json in setting group ($name): ${e.toString()}"); } } @@ -249,7 +259,7 @@ class SettingGroup extends SettingItem { try { value = loadTextFileSync(id); } catch (e) { - debugPrint("Error loading $id: $e"); + logger.e("Error loading $id: $e"); value = GetStorage().read(id); } loadValueFromJson(json.decode(value)); diff --git a/lib/settings/types/setting_item.dart b/lib/settings/types/setting_item.dart index 8c34d768..606c8587 100644 --- a/lib/settings/types/setting_item.dart +++ b/lib/settings/types/setting_item.dart @@ -1,4 +1,3 @@ -import 'package:clock_app/settings/data/localized_names.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; import 'package:clock_app/settings/types/setting_group.dart'; @@ -14,16 +13,12 @@ abstract class SettingItem { List get settingListeners => _settingListeners; List searchTags = []; List enableConditions; - // List compoundEnableConditions; // Settings which influence whether this setting is enabled List enableSettings; - // List compoundEnableSettings; - String displayName(BuildContext context) => - getLocalizedName(context); + String displayName(BuildContext context) => getLocalizedName(context); - String displayDescription(BuildContext context) => - getDescription(context); + String displayDescription(BuildContext context) => getDescription(context); bool get isEnabled { for (var enableSetting in enableSettings) { @@ -51,8 +46,8 @@ abstract class SettingItem { return path.reversed.toList(); } - SettingItem( - this.name, this.getLocalizedName, this.getDescription, this.searchTags, this.enableConditions ) + SettingItem(this.name, this.getLocalizedName, this.getDescription, + this.searchTags, this.enableConditions) : id = name, _settingListeners = [], enableSettings = []; diff --git a/lib/settings/widgets/list_filter_setting_card.dart b/lib/settings/widgets/list_filter_setting_card.dart new file mode 100644 index 00000000..903d5a10 --- /dev/null +++ b/lib/settings/widgets/list_filter_setting_card.dart @@ -0,0 +1,49 @@ +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/utils/popup_action.dart'; +import 'package:clock_app/common/widgets/card_edit_menu.dart'; +import 'package:flutter/material.dart'; + +class ListFilterSettingCard extends StatefulWidget { + const ListFilterSettingCard({ + super.key, + required this.listFilter, + required this.onEnabledChange, + }); + + final ListFilterItem listFilter; + final void Function(bool) onEnabledChange; + + @override + State createState() => _ListFilterSettingCardState(); +} + +class _ListFilterSettingCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16.0, right: 4, top: 8, bottom: 8), + child: Row( + children: [ + Expanded( + flex: 999, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.listFilter.displayName(context), + style: Theme.of(context).textTheme.displaySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ], + ), + ), + Switch( + value: widget.listFilter.isEnabled, + onChanged: widget.onEnabledChange) + ], + )); + } +} diff --git a/lib/settings/widgets/list_setting_add_bottom_sheet.dart b/lib/settings/widgets/list_setting_add_bottom_sheet.dart index 347252d8..707457a9 100644 --- a/lib/settings/widgets/list_setting_add_bottom_sheet.dart +++ b/lib/settings/widgets/list_setting_add_bottom_sheet.dart @@ -2,14 +2,14 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; -class ListSettingAddBottomSheet +class CustomizableListSettingAddBottomSheet extends StatelessWidget { - const ListSettingAddBottomSheet({ + const CustomizableListSettingAddBottomSheet({ required this.setting, super.key, }); - final ListSetting setting; + final CustomizableListSetting setting; @override Widget build(BuildContext context) { diff --git a/lib/settings/widgets/list_setting_card.dart b/lib/settings/widgets/list_setting_card.dart index a5d4805f..2724a29a 100644 --- a/lib/settings/widgets/list_setting_card.dart +++ b/lib/settings/widgets/list_setting_card.dart @@ -11,7 +11,7 @@ class ListSettingCard extends StatefulWidget { this.showAsCard = true, }); - final ListSetting setting; + final CustomizableListSetting setting; final bool showAsCard; final void Function(BuildContext context) onChanged; @@ -28,7 +28,7 @@ class _ListSettingCardState extends State { onTap: () async { await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => ListSettingScreen( + builder: (context) => CustomizableListSettingScreen( setting: widget.setting, onChanged: widget.onChanged), ), ); diff --git a/lib/settings/widgets/list_setting_screen.dart b/lib/settings/widgets/list_setting_screen.dart index af301660..7963eff7 100644 --- a/lib/settings/widgets/list_setting_screen.dart +++ b/lib/settings/widgets/list_setting_screen.dart @@ -9,23 +9,23 @@ import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/widgets/list_setting_add_bottom_sheet.dart'; import 'package:flutter/material.dart'; -class ListSettingScreen +class CustomizableListSettingScreen extends StatefulWidget { - const ListSettingScreen({ + const CustomizableListSettingScreen({ super.key, required this.setting, required this.onChanged, }); - final ListSetting setting; + final CustomizableListSetting setting; final void Function(BuildContext context) onChanged; @override - State createState() => _ListSettingScreenState(); + State createState() => _CustomizableListSettingScreenState(); } -class _ListSettingScreenState - extends State> { +class _CustomizableListSettingScreenState + extends State> { final _listController = ListController(); Future _openAddBottomSheet() async { @@ -33,7 +33,7 @@ class _ListSettingScreenState return await showModalBottomSheet( context: context, - builder: (context) => ListSettingAddBottomSheet(setting: widget.setting), + builder: (context) => CustomizableListSettingAddBottomSheet(setting: widget.setting), ); } @@ -55,9 +55,7 @@ class _ListSettingScreenState @override Widget build(BuildContext context) { return Scaffold( - appBar: AppTopBar( - title: Text(widget.setting.displayName(context)), - ), + appBar: AppTopBar(title: widget.setting.displayName(context)), body: Stack( children: [ Column( @@ -76,6 +74,8 @@ class _ListSettingScreenState _handleCustomizeItem(task); }, onModifyList: () => widget.onChanged(context), + isReorderable: true, + isSelectable: true, placeholderText: "No ${widget.setting.displayName(context).toLowerCase()} added yet", ), diff --git a/lib/settings/widgets/settings_top_bar.dart b/lib/settings/widgets/settings_top_bar.dart deleted file mode 100644 index cbb12eee..00000000 --- a/lib/settings/widgets/settings_top_bar.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/types/setting_item.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class SettingsTopBar extends StatefulWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size(0, 56); - - const SettingsTopBar( - {super.key, - this.onSearch, - this.showSearch = false, - required this.title}); - - final void Function(List settings)? onSearch; - final String title; - final bool showSearch; - - @override - State createState() => _SettingsTopBarState(); -} - -class _SettingsTopBarState extends State { - final TextEditingController _filterController = TextEditingController(); - bool _searching = false; - - _SettingsTopBarState() { - _filterController.addListener(() async { - if (_filterController.text.isEmpty) { - widget.onSearch?.call([]); - } else { - var results = extractTop( - query: _filterController.text, - choices: [ - ...appSettings.settings, - ...appSettings.settingPageLinks, - ...appSettings.settingActions - ], - limit: 10, - cutoff: 50, - getter: (item) { - // Search term includes the setting name, as well as the parent group names - return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; - }); - - widget.onSearch?.call(results.map((result) => result.choice).toList()); - } - }); - } - - @override - Widget build(BuildContext context) { - if (_searching) { - return AppTopBar( - title: TextField( - autofocus: _filterController.text.isEmpty, - onTapOutside: ((event) { - FocusScope.of(context).unfocus(); - }), - controller: _filterController, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: - const OutlineInputBorder(borderSide: BorderSide.none), - fillColor: Colors.transparent, - hintText: AppLocalizations.of(context)!.searchSettingPlaceholder, - hintStyle: Theme.of(context).textTheme.bodyLarge, - ), - textAlignVertical: TextAlignVertical.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - actions: [ - IconButton( - onPressed: () { - _filterController.clear(); - setState(() { - _searching = false; - }); - }, - icon: const Icon(Icons.close), - ) - ], - ); - } else { - return AppTopBar( - title: Text( - widget.title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.6), - ), - ), - actions: [ - if (widget.showSearch) - IconButton( - onPressed: () { - setState(() { - _searching = true; - }); - }, - icon: Icon( - Icons.search, - color: - Theme.of(context).colorScheme.onBackground, - ), - ) - ], - ); - } - } -} diff --git a/lib/settings/widgets/slider_setting_card.dart b/lib/settings/widgets/slider_setting_card.dart index e2be84ff..3555e960 100644 --- a/lib/settings/widgets/slider_setting_card.dart +++ b/lib/settings/widgets/slider_setting_card.dart @@ -24,6 +24,7 @@ class _SliderSettingCardState extends State { Widget build(BuildContext context) { SliderField sliderCard = SliderField( title: widget.setting.displayName(context), + description: widget.setting.displayDescription(context), value: widget.setting.value, min: widget.setting.min, max: widget.setting.max, diff --git a/lib/settings/widgets/switch_setting_card.dart b/lib/settings/widgets/switch_setting_card.dart index 7203a373..212ad546 100644 --- a/lib/settings/widgets/switch_setting_card.dart +++ b/lib/settings/widgets/switch_setting_card.dart @@ -24,6 +24,7 @@ class _SwitchSettingCardState extends State { SwitchField switchCard = SwitchField( name: widget.setting.displayName(context), value: widget.setting.value, + description: widget.setting.displayDescription(context), onChanged: (value) { setState(() { widget.setting.setValue(context, value); diff --git a/lib/settings/data/stopwatch_settings_schema.dart b/lib/stopwatch/data/stopwatch_settings_schema.dart similarity index 100% rename from lib/settings/data/stopwatch_settings_schema.dart rename to lib/stopwatch/data/stopwatch_settings_schema.dart diff --git a/lib/stopwatch/logic/stopwatch_notification.dart b/lib/stopwatch/logic/stopwatch_notification.dart index 3ec7bb35..0ec6cf10 100644 --- a/lib/stopwatch/logic/stopwatch_notification.dart +++ b/lib/stopwatch/logic/stopwatch_notification.dart @@ -8,9 +8,9 @@ Future updateStopwatchNotification(ClockStopwatch stopwatch) async { content: NotificationContent( id: stopwatch.id, channelKey: stopwatchNotificationChannelKey, - title: 'Stopwatch', - body: - "${TimeDuration.fromMilliseconds(stopwatch.elapsedMilliseconds).toTimeString(showMilliseconds: false)} - LAP ${stopwatch.laps.length + 1}", + title: + "${TimeDuration.fromMilliseconds(stopwatch.elapsedMilliseconds).toTimeString(showMilliseconds: false)} - LAP ${stopwatch.laps.length}", + body: "Stopwatch", category: NotificationCategory.StopWatch, ), actionButtons: [ diff --git a/lib/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index a4645316..2d17acea 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; @@ -17,8 +18,6 @@ import 'package:clock_app/stopwatch/widgets/stopwatch_ticker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - class StopwatchScreen extends StatefulWidget { const StopwatchScreen({super.key}); @@ -30,9 +29,8 @@ class _StopwatchScreenState extends State { final _listController = ListController(); late Setting _showNotificationSetting; - - late final ClockStopwatch _stopwatch; + late final ClockStopwatch _stopwatch; void update(dynamic value) { setState(() {}); @@ -41,8 +39,14 @@ class _StopwatchScreenState extends State { @override void initState() { super.initState(); - _stopwatch = loadListSync('stopwatches').first; - + final stopwatches = loadListSync('stopwatches'); + if (stopwatches.isEmpty) { + _stopwatch = ClockStopwatch(); + saveList('stopwatches', [_stopwatch]); + } else { + _stopwatch = stopwatches.first; + } + _showNotificationSetting = appSettings.getGroup("Stopwatch").getSetting("Show Notification"); @@ -55,20 +59,23 @@ class _StopwatchScreenState extends State { void _handleStopwatchChange() { final newList = loadListSync('stopwatches'); + _stopwatch.copyFrom(newList.first); + if (mounted) { - newList.first.laps - .where((lap) => !_stopwatch.laps.contains(lap)) - .forEach((lap) => _listController.addItem(lap)); + // // If there are any new laps, tell the listcontroller to update the ui with them + // newList.first.laps + // .where((lap) => + // !_stopwatch.laps.map((l) => l.number).contains(lap.number)) + // .forEach((lap) => _listController.addItem(lap)); + _listController.reload(_stopwatch.laps); setState(() {}); } - _stopwatch.copyFrom(newList.first); showProgressNotification(); } @override void dispose() { - // updateNotificationInterval?.cancel(); // updateNotificationInterval = null; @@ -88,18 +95,23 @@ class _StopwatchScreenState extends State { void _handleAddLap() { if (_stopwatch.currentLapTime.inMilliseconds == 0) return; + _stopwatch.finishLap(_stopwatch.laps.first); + _listController.changeItems((laps) => {}); _listController.addItem(_stopwatch.getLap()); saveList('stopwatches', [_stopwatch]); showProgressNotification(); } void _handleToggleState() { + if (_stopwatch.isStopped) { + _listController.addItem(_stopwatch.getLap()); + } setState(() { _stopwatch.toggleState(); }); + saveList('stopwatches', [_stopwatch]); if (_stopwatch.isRunning) { - // ticker!.start(); showProgressNotification(); } else { stopwatchNotificationInterval?.cancel(); @@ -137,16 +149,18 @@ class _StopwatchScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StopwatchTicker(stopwatch:_stopwatch), - const SizedBox(height: 8), + StopwatchTicker(stopwatch: _stopwatch), + const SizedBox(height: 8), Expanded( child: CustomListView( items: _stopwatch.laps, listController: _listController, - itemBuilder: (lap) => LapCard( - key: ValueKey(lap), - lap: lap, - ), + itemBuilder: (lap) => lap.isActive + ? ActiveLapCard(stopwatch: _stopwatch) + : LapCard( + key: ValueKey(lap), + lap: lap, + ), placeholderText: AppLocalizations.of(context)!.noLapsMessage, isDeleteEnabled: false, isDuplicateEnabled: false, @@ -182,5 +196,3 @@ class _StopwatchScreenState extends State { ); } } - - diff --git a/lib/stopwatch/types/lap.dart b/lib/stopwatch/types/lap.dart index e5f3a7d3..803c96c2 100644 --- a/lib/stopwatch/types/lap.dart +++ b/lib/stopwatch/types/lap.dart @@ -3,27 +3,45 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/timer/types/time_duration.dart'; class Lap extends ListItem { - late int number; - late TimeDuration lapTime; - late TimeDuration elapsedTime; + late int _number; + late TimeDuration _lapTime; + late TimeDuration _elapsedTime; + late bool _isActive; + int get number => _number; + bool get isActive => _isActive; + set isActive(bool value) => _isActive = value; + set lapTime(TimeDuration value) => _lapTime = value; + set elapsedTime(TimeDuration value) => _elapsedTime = value; + TimeDuration get lapTime => _lapTime; + TimeDuration get elapsedTime => _elapsedTime; @override int get id => number; @override bool get isDeletable => false; - Lap({required this.elapsedTime, required this.number, required this.lapTime}); + Lap( + {required int number, + TimeDuration elapsedTime = const TimeDuration(), + TimeDuration lapTime = const TimeDuration(), + bool isActive = false}) + : _lapTime = lapTime, + _number = number, + _elapsedTime = elapsedTime, + _isActive = isActive; Lap.fromJson(Json? json) { if (json == null) { - number = 0; - lapTime = TimeDuration.zero; - elapsedTime = TimeDuration.zero; + _number = 0; + _lapTime = TimeDuration.zero; + _elapsedTime = TimeDuration.zero; + _isActive = false; return; } - number = json['number'] ?? 0; - lapTime = TimeDuration.fromJson(json['lapTime']); - elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _number = json['number'] ?? 0; + _lapTime = TimeDuration.fromJson(json['lapTime']); + _elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _isActive = json['isActive'] ?? false; } @override @@ -31,17 +49,23 @@ class Lap extends ListItem { 'number': number, 'lapTime': lapTime.toJson(), 'elapsedTime': elapsedTime.toJson(), + 'isActive': _isActive, }; @override copy() { - return Lap(elapsedTime: elapsedTime, number: number, lapTime: lapTime); + return Lap( + elapsedTime: elapsedTime, + number: number, + lapTime: lapTime, + isActive: _isActive); } @override void copyFrom(other) { - number = other.number; - lapTime = TimeDuration.from(other.lapTime); - elapsedTime = TimeDuration.from(other.elapsedTime); + _number = other.number; + _lapTime = TimeDuration.from(other.lapTime); + _elapsedTime = TimeDuration.from(other.elapsedTime); + _isActive = other._isActive; } } diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index 2f1701e2..d1b34568 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -1,10 +1,12 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; +import 'package:clock_app/common/utils/id.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; -import 'package:flutter/material.dart'; +// All time units are in milliseconds class ClockStopwatch extends JsonSerializable { int _elapsedMillisecondsOnPause = 0; DateTime _startTime = DateTime(0); @@ -16,33 +18,42 @@ class ClockStopwatch extends JsonSerializable { int get id => _id; List get laps => _laps; + List get finishedLaps => _laps.where((lap) => !lap.isActive).toList(); int get elapsedMilliseconds => _state == TimerState.running ? DateTime.now().difference(_startTime).toTimeDuration().inMilliseconds : _elapsedMillisecondsOnPause; + TimeDuration get elapsedTime => + TimeDuration.fromMilliseconds(elapsedMilliseconds); bool get isRunning => _state == TimerState.running; + bool get isStopped => _state == TimerState.stopped; bool get isStarted => _state == TimerState.running || _state == TimerState.paused; TimerState get state => _state; TimeDuration get currentLapTime => - TimeDuration.fromMilliseconds(elapsedMilliseconds - - (_laps.isNotEmpty ? _laps.first.elapsedTime.inMilliseconds : 0)); - Lap? get previousLap => _laps.isNotEmpty ? _laps.first : null; + TimeDuration.fromMilliseconds(elapsedMilliseconds - lastLapElapsedTime); + int get lastLapElapsedTime { + if (finishedLaps.isEmpty) return 0; + return finishedLaps.first.elapsedTime.inMilliseconds; + } + + Lap? get previousLap => finishedLaps.isNotEmpty ? finishedLaps.first : null; Lap? get fastestLap => _fastestLap; Lap? get slowestLap => _slowestLap; Lap? get averageLap { - if (_laps.isEmpty) return null; - var totalMilliseconds = _laps.fold( + if (finishedLaps.isEmpty) return null; + var totalMilliseconds = finishedLaps.fold( 0, (previousValue, lap) => previousValue + lap.lapTime.inMilliseconds); return Lap( - elapsedTime: - TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + elapsedTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), number: _laps.length + 1, - lapTime: TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + lapTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), ); } ClockStopwatch() - : _id = UniqueKey().hashCode, + : _id = getId(), _elapsedMillisecondsOnPause = 0, _startTime = DateTime(0), _state = TimerState.stopped; @@ -51,7 +62,7 @@ class ClockStopwatch extends JsonSerializable { : _elapsedMillisecondsOnPause = 0, _startTime = DateTime(0), _state = TimerState.stopped, - _id = UniqueKey().hashCode; + _id = getId(); copyFrom(ClockStopwatch stopwatch) { _elapsedMillisecondsOnPause = stopwatch._elapsedMillisecondsOnPause; @@ -94,31 +105,37 @@ class ClockStopwatch extends JsonSerializable { } } - void updateFastestAndSlowestLap() { - if(laps.isEmpty) return; - _fastestLap = _laps.reduce((value, element) => + if (finishedLaps.isEmpty) return; + _fastestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds < element.lapTime.inMilliseconds ? value : element); - _slowestLap = _laps.reduce((value, element) => + _slowestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds > element.lapTime.inMilliseconds ? value : element); } void addLap() { - if (currentLapTime.inMilliseconds == 0) return; + if (_laps.isNotEmpty) { + if (currentLapTime.inMilliseconds == 0) return; + finishLap(_laps.first); + updateFastestAndSlowestLap(); + } _laps.insert(0, getLap()); - updateFastestAndSlowestLap(); } + void finishLap(Lap lap) { + // This needs to be set before elapsedTime and isActive + lap.lapTime = currentLapTime; + lap.elapsedTime = TimeDuration.fromMilliseconds(elapsedMilliseconds); + lap.isActive = false; + } + + // Lap getLap() { - return Lap( - elapsedTime: TimeDuration.fromMilliseconds(elapsedMilliseconds), - number: _laps.length + 1, - lapTime: currentLapTime, - ); + return Lap(number: finishedLaps.length + 1, isActive: true); } @override @@ -128,13 +145,13 @@ class ClockStopwatch extends JsonSerializable { 'elapsedMillisecondsOnPause': _elapsedMillisecondsOnPause, 'startTime': _startTime.toIso8601String(), 'state': _state.toString(), - 'laps': _laps.map((e) => e.toJson()).toList(), + 'laps': listToString(_laps), }; } ClockStopwatch.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } _elapsedMillisecondsOnPause = json['elapsedMillisecondsOnPause'] ?? 0; @@ -144,8 +161,9 @@ class ClockStopwatch extends JsonSerializable { _state = TimerState.values.firstWhere( (e) => e.toString() == (json['state'] ?? ''), orElse: () => TimerState.stopped); - _id = json['id'] ?? UniqueKey().hashCode; - _laps = ((json['laps'] ?? []) as List).map((e) => Lap.fromJson(e)).toList(); + _id = json['id'] ?? getId(); + // _finishedLaps = []; + _laps = listFromString(json['laps'] ?? '[]'); updateFastestAndSlowestLap(); } } diff --git a/lib/stopwatch/widgets/lap_card.dart b/lib/stopwatch/widgets/lap_card.dart index 5ce040a5..bb279d7d 100644 --- a/lib/stopwatch/widgets/lap_card.dart +++ b/lib/stopwatch/widgets/lap_card.dart @@ -1,23 +1,67 @@ import 'package:clock_app/stopwatch/types/lap.dart'; +import 'package:clock_app/stopwatch/types/stopwatch.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class LapCard extends StatefulWidget { - const LapCard({super.key, required this.lap, this.onInit}); +class LapCard extends StatelessWidget { + const LapCard({super.key, required this.lap}); final Lap lap; - final VoidCallback? onInit; @override - State createState() => _LapCardState(); + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Text('${lap.number}'), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(lap.lapTime.toTimeString(showMilliseconds: true), + style: Theme.of(context).textTheme.displaySmall), + Text( + '${AppLocalizations.of(context)!.elapsedTime}: ${lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + ], + ), + ], + ), + ); + } +} + +class ActiveLapCard extends StatefulWidget { + const ActiveLapCard({ + super.key, + required this.stopwatch, + }); + + final ClockStopwatch stopwatch; + + @override + State createState() => _ActiveLapCardState(); } -class _LapCardState extends State { +class _ActiveLapCardState extends State { + late Ticker ticker; + + void tick(Duration elapsed) { + setState(() {}); + } + @override void initState() { + ticker = Ticker(tick); + ticker.start(); super.initState(); - // widget.onInit?.call(); + } + + @override + void dispose() { + ticker.dispose(); + super.dispose(); } @override @@ -26,15 +70,17 @@ class _LapCardState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Text('${widget.lap.number}'), + Text('${widget.stopwatch.laps.length}'), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.lap.lapTime.toTimeString(showMilliseconds: true), + Text( + widget.stopwatch.currentLapTime + .toTimeString(showMilliseconds: true), style: Theme.of(context).textTheme.displaySmall), Text( - '${AppLocalizations.of(context)!.elapsedTime}: ${widget.lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + '${AppLocalizations.of(context)!.elapsedTime}: ${widget.stopwatch.elapsedTime.toTimeString(showMilliseconds: true)}'), ], ), ], diff --git a/lib/stopwatch/widgets/stopwatch_ticker.dart b/lib/stopwatch/widgets/stopwatch_ticker.dart index 5a3bae65..9bbe4a5f 100644 --- a/lib/stopwatch/widgets/stopwatch_ticker.dart +++ b/lib/stopwatch/widgets/stopwatch_ticker.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - class StopwatchTicker extends StatefulWidget { const StopwatchTicker({super.key, required this.stopwatch}); @@ -31,10 +30,6 @@ class _StopwatchTickerState extends State { } void tick(Duration elapsed) { - // var t = elapsed.inMicroseconds * 1e-6; - // double radius = 100; - // drawState.x = radius * math.sin(t); - // drawState.y = radius * math.cos(t); setState(() {}); } @@ -77,9 +72,6 @@ class _StopwatchTickerState extends State { ticker.stop(); ticker.dispose(); - // updateNotificationInterval?.cancel(); - // updateNotificationInterval = null; - super.dispose(); } diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart new file mode 100644 index 00000000..cd73e2f0 --- /dev/null +++ b/lib/system/logic/background_service.dart @@ -0,0 +1,70 @@ +import 'package:background_fetch/background_fetch.dart'; +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/system/logic/initialize_isolate.dart'; +import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:flutter/material.dart'; + +Future initBackgroundService({int interval = 60}) async { + assert( + interval >= 15, "Interval must be greater than or equal to 15 minutes."); + await BackgroundFetch.configure( + BackgroundFetchConfig( + minimumFetchInterval: interval, + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.NONE), (String taskId) async { + // <-- Event handler + // This is the fetch-event callback. + logger.t("[initBackgroundService] Event received $taskId"); + + // await initializeIsolate(); + + await updateAlarms( + "[initBackgroundService] Update alarms in background service"); + await updateTimers( + "[initBackgroundService] Update timers in background service"); + // IMPORTANT: You must signal completion of your task or the OS can punish your app + // for taking too long in the background. + BackgroundFetch.finish(taskId); + }, (String taskId) async { + // <-- Task timeout handler. + // This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId) + logger.t("[initBackgroundService] Task timed-out taskId: $taskId"); + BackgroundFetch.finish(taskId); + }); +} + +// [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` +@pragma('vm:entry-point') +void handleBackgroundServiceTask(HeadlessTask task) async { + FlutterError.onError = (FlutterErrorDetails details) { + logger.f( + "Error in handleBackgroundServiceTask isolate: ${details.exception.toString()}"); + }; + String taskId = task.taskId; + bool isTimeout = task.timeout; + if (isTimeout) { + // This task has exceeded its allowed running-time. + // You must stop what you're doing and immediately .finish(taskId) + logger.t("[handleBackgroundServiceTask] Headless task timed-out: $taskId"); + BackgroundFetch.finish(taskId); + return; + } + await initializeIsolate(); + logger.t('[handleBackgroundServiceTask] Headless event received.'); + await updateAlarms( + "[handleBackgroundServiceTask] Update alarms in background service"); + await updateTimers( + "[handleBackgroundServiceTask] Update timers in background service"); + + BackgroundFetch.finish(taskId); +} + +void registerHeadlessBackgroundService() { + BackgroundFetch.registerHeadlessTask(handleBackgroundServiceTask); +} diff --git a/lib/system/logic/handle_boot.dart b/lib/system/logic/handle_boot.dart index 742c589b..67c16986 100644 --- a/lib/system/logic/handle_boot.dart +++ b/lib/system/logic/handle_boot.dart @@ -1,6 +1,8 @@ import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:flutter/material.dart'; @pragma('vm:entry-point') void handleBoot() async { @@ -11,6 +13,10 @@ void handleBoot() async { // File('$appDataDirectory/log-dart.txt') // .writeAsStringSync(message, mode: FileMode.append); // + FlutterError.onError = (FlutterErrorDetails details) { + logger.f("Error in handleBoot isolate: ${details.exception.toString()}"); + }; + await initializeIsolate(); await updateAlarms("handleBoot(): Update alarms on system boot"); diff --git a/lib/system/logic/handle_intents.dart b/lib/system/logic/handle_intents.dart index 718a0a68..f794e656 100644 --- a/lib/system/logic/handle_intents.dart +++ b/lib/system/logic/handle_intents.dart @@ -4,20 +4,17 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/schedules/weekly_alarm_schedule.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:flutter/material.dart' hide Intent; import 'package:receive_intent/receive_intent.dart'; -// void navigateToTab(BuildContext context, int tab) { -// Navigator.of(context) -// .pushNamedAndRemoveUntil(Routes.rootRoute, (Route route) => false, arguments: {'tabIndex': tab});} - void handleIntent(Intent? receivedIntent, BuildContext context, Function(Alarm) onSetAlarm, Function(int) setTab) async { if (receivedIntent != null) { - print( + logger.i( "Intent received ${receivedIntent.action} ${receivedIntent.data} ${receivedIntent.extra}"); switch (receivedIntent.action) { case "android.intent.action.MAIN": @@ -96,8 +93,7 @@ void handleIntent(Intent? receivedIntent, BuildContext context, case "android.intent.action.VIEW_TIMERS": break; case "SELECT_NOTIFICATION": - AlarmNotificationManager.appVisibilityWhenCreated = AppVisibility.state; - print("************Select************************** ${AppVisibility.state}"); + appVisibilityWhenAlarmNotificationCreated = AppVisibility.state; break; default: break; diff --git a/lib/system/logic/initialize_isolate.dart b/lib/system/logic/initialize_isolate.dart index cda95c0d..12035168 100644 --- a/lib/system/logic/initialize_isolate.dart +++ b/lib/system/logic/initialize_isolate.dart @@ -7,9 +7,12 @@ import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; import 'package:clock_app/settings/logic/initialize_settings.dart'; import 'package:clock_app/system/data/device_info.dart'; +import 'package:flutter/widgets.dart'; Future initializeIsolate() async { DartPluginRegistrant.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + await initializeAndroidInfo(); await initializeAppDataDirectory(); await initializeStorage(false); diff --git a/lib/system/logic/initialize_isolate_ports.dart b/lib/system/logic/initialize_isolate_ports.dart new file mode 100644 index 00000000..0722ca35 --- /dev/null +++ b/lib/system/logic/initialize_isolate_ports.dart @@ -0,0 +1,22 @@ +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/settings/types/listener_manager.dart'; + +void initializeIsolatePorts(){ + ReceivePort receivePort = ReceivePort(); + IsolateNameServer.removePortNameMapping(updatePortName); + IsolateNameServer.registerPortWithName(receivePort.sendPort, updatePortName); + printIsolateInfo(); + receivePort.listen((message) { + if (message == "updateAlarms") { + ListenerManager.notifyListeners("alarms"); + } else if (message == "updateTimers") { + ListenerManager.notifyListeners("timers"); + } else if (message == "updateStopwatches") { + ListenerManager.notifyListeners("stopwatch"); + } + }); +} diff --git a/lib/system/logic/quick_actions.dart b/lib/system/logic/quick_actions.dart new file mode 100644 index 00000000..b2e1aeee --- /dev/null +++ b/lib/system/logic/quick_actions.dart @@ -0,0 +1,28 @@ +import 'package:clock_app/navigation/data/tabs.dart'; +import 'package:flutter/material.dart'; +import 'package:quick_actions/quick_actions.dart'; + +Future initializeQuickActions( + BuildContext context, Function(int, [String?]) setTab) async { + const QuickActions quickActions = QuickActions(); + await quickActions.initialize((shortcutType) { + if (shortcutType == 'action_add_alarm') { + setTab(getTabs(context).indexWhere((tab) => tab.id == "alarm"), "add_alarm"); + } + if (shortcutType == 'action_add_timer') { + setTab(getTabs(context).indexWhere((tab) => tab.id == "timer"), "add_timer"); + } + // More handling code... + }); + + await quickActions.setShortcutItems([ + const ShortcutItem( + type: 'action_add_alarm', + localizedTitle: 'Add alarm', + icon: 'alarm_icon'), + const ShortcutItem( + type: 'action_add_timer', + localizedTitle: 'Add timer', + icon: 'timer_icon') + ]); +} diff --git a/lib/system/types/android_platform_file.dart b/lib/system/types/android_platform_file.dart new file mode 100644 index 00000000..e9a9af4b --- /dev/null +++ b/lib/system/types/android_platform_file.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart'; + +/// Android-specific. uses a platform channel which calls Scoped Storage APIs +/// so that we can list directory contents and read files from a selected folder +class AndroidFile { + String uri; + String name; + int size; + DateTime modifiedDate; + + AndroidFile( + {required this.uri, + required this.name, + required this.size, + required this.modifiedDate}); + AndroidFile.fromFile(File file) + : this( + uri: file.path, + name: file.path.split('/').last, + size: file.lengthSync(), + modifiedDate: file.lastModifiedSync()); + + static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + + Stream> openRead() async* { + const int chunkSize = 1024 * 1024; // 1MB + int offset = 0; + bool moreData = true; + + while (moreData) { + try { + final Map arguments = { + 'uri': uri, + 'offset': offset, + 'chunkSize': chunkSize, + }; + final List? chunk = + await methodChannel.invokeMethod('getFileChunk', arguments); + if (chunk == null || chunk.isEmpty) { + moreData = false; + } else { + yield Uint8List.fromList(chunk); + offset += chunk.length; + } + } on PlatformException catch (e) { + logger.e("Failed to get file chunk: ${e.message}"); + moreData = false; + } + } + } +} + +class AndroidFolder { + final String? uri; + final String? path; + + AndroidFolder({this.uri, this.path}); + + static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + + Future> files() async { + if (path != null) { + final directory = Directory(path!); + return (await directory.list().toList()) + .whereType() + .map((file) => AndroidFile.fromFile(file)) + .toList(); + } else { + // Android + return (await methodChannel.invokeMethod('listFiles', {'uri': uri!})) + .map((file) => AndroidFile( + uri: file['uri'], + name: file['name'], + size: file['size'], + modifiedDate: + DateTime.fromMillisecondsSinceEpoch(file['modified']))) + .cast() + .toList(); + } + } + + Future> folders() async { + if (path != null) { + final directory = Directory(uri!); + return (await directory.list().toList()) + .whereType() + .map((directory) => AndroidFolder(uri: directory.path)) + .toList(); + } else { + // Android + return (await methodChannel + .invokeMethod('listDirectories', {'uri': uri!})) + .map((folder) => AndroidFolder(uri: folder)) + .cast() + .toList(); + } + } + + String get name { + if (path != null) return basename(path!); + return Uri.decodeFull(uri!).split(RegExp(r'[/:]')).last; // Android + } +} diff --git a/lib/settings/data/appearance_settings_schema.dart b/lib/theme/data/appearance_settings_schema.dart similarity index 92% rename from lib/settings/data/appearance_settings_schema.dart rename to lib/theme/data/appearance_settings_schema.dart index b4a8bd38..9057c042 100644 --- a/lib/settings/data/appearance_settings_schema.dart +++ b/lib/theme/data/appearance_settings_schema.dart @@ -7,13 +7,11 @@ import 'package:clock_app/theme/screens/themes_screen.dart'; import 'package:clock_app/theme/theme.dart'; import 'package:clock_app/theme/types/color_scheme.dart'; import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/theme/types/theme_brightness.dart'; import 'package:clock_app/theme/utils/color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -enum ThemeBrightness { light, dark, system } - -enum DarkMode { user, system, nightDay } SettingGroup appearanceSettingsSchema = SettingGroup( "Appearance", @@ -198,6 +196,24 @@ SettingGroup appearanceSettingsSchema = SettingGroup( ), ], ), + SettingGroup("Animations", + (context) => AppLocalizations.of(context)!.animationSettingGroup, [ + SliderSetting( + "Animation Speed", + (context) => AppLocalizations.of(context)!.animationSpeedSetting, + 0.5, + 2, + 1, + snapLength: 0.1, + ), + SwitchSetting( + "Extra Animations", + (context) => AppLocalizations.of(context)!.extraAnimationSetting, + false, + getDescription: (context) => + AppLocalizations.of(context)!.extraAnimationSettingDescription, + ), + ]) ], icon: Icons.palette_outlined, getDescription: (context) => diff --git a/lib/theme/logic/theme_extension.dart b/lib/theme/logic/theme_extension.dart new file mode 100644 index 00000000..2581ed35 --- /dev/null +++ b/lib/theme/logic/theme_extension.dart @@ -0,0 +1,6 @@ +// import 'package:flutter/material.dart'; +// +// T getThemeExtension (ThemeData theme) { +// return theme.extension() ?? const T(); +// +// } diff --git a/lib/theme/screens/themes_screen.dart b/lib/theme/screens/themes_screen.dart index 89ca65e7..eba91cae 100644 --- a/lib/theme/screens/themes_screen.dart +++ b/lib/theme/screens/themes_screen.dart @@ -79,7 +79,7 @@ class _ThemesScreenState Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: Text(widget.setting.displayName(context)), + titleWidget: Text(widget.setting.displayName(context)), ), body: Stack( children: [ diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 81a390e5..8355f2b8 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -39,6 +39,6 @@ ThemeData defaultTheme = ThemeData( sliderTheme: getSliderTheme(defaultColorScheme), bottomSheetTheme: getBottomSheetTheme(defaultColorScheme, defaultStyleTheme), toggleButtonsTheme: toggleButtonsTheme, - extensions: const >[ThemeStyleExtension()], + extensions: const >[ThemeStyleExtension(), ThemeSettingExtension()], popupMenuTheme: getPopupMenuTheme(defaultColorScheme, defaultStyleTheme), ); diff --git a/lib/theme/types/color_scheme.dart b/lib/theme/types/color_scheme.dart index 24be3a5c..ece52d7e 100644 --- a/lib/theme/types/color_scheme.dart +++ b/lib/theme/types/color_scheme.dart @@ -97,6 +97,22 @@ class ColorSchemeData extends ThemeItem { ColorSchemeData.fromJson(Json json) : super.fromJson(json, colorSchemeSettingsSchema.copy()); + + bool isEqualTo(ColorSchemeData other) { + return background == other.background && + error == other.error && + accent == other.accent && + onError == other.onError && + card == other.card && + onCard == other.onCard && + onAccent == other.onAccent && + onBackground == other.onBackground && + shadow == other.shadow && + outline == other.outline && + useAccentAsShadow == other.useAccentAsShadow && + useAccentAsOutline == other.useAccentAsOutline && + name == other.name; + } } ColorScheme getColorScheme(ColorSchemeData colorSchemeData) { diff --git a/lib/theme/types/dark_mode.dart b/lib/theme/types/dark_mode.dart new file mode 100644 index 00000000..94647f6a --- /dev/null +++ b/lib/theme/types/dark_mode.dart @@ -0,0 +1 @@ +enum DarkMode { user, system, nightDay } diff --git a/lib/theme/types/style_theme.dart b/lib/theme/types/style_theme.dart index c4775334..f0be1244 100644 --- a/lib/theme/types/style_theme.dart +++ b/lib/theme/types/style_theme.dart @@ -40,7 +40,7 @@ class StyleTheme extends ThemeItem { .setValueWithoutNotify(borderWidth); } - StyleTheme.from(StyleTheme colorSchemeData) : super.from(colorSchemeData); + StyleTheme.from(StyleTheme super.colorSchemeData) : super.from(); @override String get name => settings.getSetting("Name").value; @@ -65,4 +65,14 @@ class StyleTheme extends ThemeItem { StyleTheme.fromJson(Json json) : super.fromJson(json, styleThemeSettingsSchema.copy()); + + bool isEqualTo(StyleTheme other) { + return name == other.name && + shadowElevation == other.shadowElevation && + shadowOpacity == other.shadowOpacity && + shadowBlurRadius == other.shadowBlurRadius && + shadowSpreadRadius == other.shadowSpreadRadius && + borderRadius == other.borderRadius && + borderWidth == other.borderWidth; + } } diff --git a/lib/theme/types/theme_brightness.dart b/lib/theme/types/theme_brightness.dart new file mode 100644 index 00000000..4660cd26 --- /dev/null +++ b/lib/theme/types/theme_brightness.dart @@ -0,0 +1 @@ +enum ThemeBrightness { light, dark, system } diff --git a/lib/theme/types/theme_extension.dart b/lib/theme/types/theme_extension.dart index cfbdad4f..87088f8c 100644 --- a/lib/theme/types/theme_extension.dart +++ b/lib/theme/types/theme_extension.dart @@ -55,3 +55,34 @@ class ThemeStyleExtension extends ThemeExtension { ); } } + +class ThemeSettingExtension extends ThemeExtension { + final bool useMaterialYou; + final bool useMaterialStyle; + + const ThemeSettingExtension({ + this.useMaterialYou = false, + this.useMaterialStyle = false, + }); + + @override + ThemeExtension copyWith({ + bool? useMaterialYou, + bool? useMaterialStyle, + }) { + return ThemeSettingExtension( + useMaterialYou: useMaterialYou ?? this.useMaterialYou, + useMaterialStyle: useMaterialStyle ?? this.useMaterialStyle); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, double t) { + if (other is! ThemeSettingExtension) return this; + + return ThemeSettingExtension( + useMaterialYou: t < 0.5 ? useMaterialYou : other.useMaterialYou, + useMaterialStyle: t < 0.5 ? useMaterialStyle : other.useMaterialStyle, + ); + } +} diff --git a/lib/theme/types/theme_item.dart b/lib/theme/types/theme_item.dart index e33fe42f..c8a26861 100644 --- a/lib/theme/types/theme_item.dart +++ b/lib/theme/types/theme_item.dart @@ -1,7 +1,7 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/settings/types/setting_group.dart'; -import 'package:flutter/material.dart'; abstract class ThemeItem extends CustomizableListItem { late int _id; @@ -9,12 +9,12 @@ abstract class ThemeItem extends CustomizableListItem { bool _isDefault = false; ThemeItem(SettingGroup defaultSettings, bool isDefault, [int? id]) - : _id = id ?? UniqueKey().hashCode, + : _id = id ?? getId(), _settings = defaultSettings, _isDefault = isDefault; ThemeItem.from(ThemeItem themeItem) - : _id = UniqueKey().hashCode, + : _id = getId(), _isDefault = false, _settings = themeItem.settings.copy(); @@ -45,10 +45,10 @@ abstract class ThemeItem extends CustomizableListItem { ThemeItem.fromJson(Json json, SettingGroup settings) : _settings = settings { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); _isDefault = json['isDefault'] ?? false; settings.loadValueFromJson(json['settings']); } diff --git a/lib/theme/utils/color_scheme.dart b/lib/theme/utils/color_scheme.dart index 3d3cf4a9..e4319abb 100644 --- a/lib/theme/utils/color_scheme.dart +++ b/lib/theme/utils/color_scheme.dart @@ -1,4 +1,5 @@ import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/theme/bottom_sheet.dart'; import 'package:clock_app/theme/popup_menu.dart'; import 'package:clock_app/theme/slider.dart'; @@ -38,20 +39,25 @@ ThemeData getTheme( {ColorScheme? colorScheme, ColorSchemeData? colorSchemeData, StyleTheme? styleTheme}) { - styleTheme ??= appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Style Theme") +SettingGroup appearanceSettings = appSettings + .getGroup("Appearance"); + SettingGroup colorSettings = appearanceSettings.getGroup("Colors"); + SettingGroup styleSettings = appearanceSettings.getGroup("Style"); + + styleTheme ??= styleSettings.getSetting("Style Theme") .value; colorSchemeData ??= colorScheme != null ? getColorSchemeData(colorScheme) - : appSettings - .getGroup("Appearance") - .getGroup("Colors") - .getSetting("Color Scheme") + : colorSettings.getSetting("Color Scheme") .value; + bool useMaterialYou = colorSettings.getSetting("Use Material You") + .value; + bool useMaterialStyle = styleSettings.getSetting("Use Material Style") + .value; + + if (styleTheme == null || colorSchemeData == null) { return defaultTheme; } @@ -95,6 +101,11 @@ ThemeData getTheme( borderWidth: styleTheme.borderWidth, ) ?? const ThemeStyleExtension(), + defaultTheme.extension()?.copyWith( + useMaterialYou: useMaterialYou, + useMaterialStyle: useMaterialStyle, + ) ?? + const ThemeSettingExtension(), ], ); } diff --git a/lib/settings/data/timer_app_settings_schema.dart b/lib/timer/data/timer_app_settings_schema.dart similarity index 100% rename from lib/settings/data/timer_app_settings_schema.dart rename to lib/timer/data/timer_app_settings_schema.dart index d7fbfdb4..b5a7b546 100644 --- a/lib/settings/data/timer_app_settings_schema.dart +++ b/lib/timer/data/timer_app_settings_schema.dart @@ -39,7 +39,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ], [ SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionSlide, + (context) => AppLocalizations.of(context)!.dismissActionAreaButtons, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => AreaNotificationAction( @@ -51,7 +51,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ), ), SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionButtons, + (context) => AppLocalizations.of(context)!.dismissActionSlide, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => SlideNotificationAction( @@ -63,7 +63,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ), ), SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionAreaButtons, + (context) => AppLocalizations.of(context)!.dismissActionButtons, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => ButtonsNotificationAction( diff --git a/lib/timer/data/timer_settings_schema.dart b/lib/timer/data/timer_settings_schema.dart index 2b189dfd..8bbfe1da 100644 --- a/lib/timer/data/timer_settings_schema.dart +++ b/lib/timer/data/timer_settings_schema.dart @@ -1,12 +1,12 @@ import 'package:audio_session/audio_session.dart'; import 'package:clock_app/audio/audio_channels.dart'; +import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/logic/tags.dart'; import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/popup_action.dart'; import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/utils/ringtones.dart'; -import 'package:clock_app/settings/screens/ringtones_screen.dart'; import 'package:clock_app/settings/screens/tags_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; diff --git a/lib/timer/logic/get_duration_picker.dart b/lib/timer/logic/get_duration_picker.dart index fd697e3a..672575ae 100644 --- a/lib/timer/logic/get_duration_picker.dart +++ b/lib/timer/logic/get_duration_picker.dart @@ -5,19 +5,13 @@ import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer_preset.dart'; import 'package:clock_app/timer/widgets/dial_duration_picker.dart'; +import 'package:clock_app/timer/widgets/numpad_duration_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -Widget getDurationPicker(BuildContext context, TimeDuration duration, +Widget getDurationPicker(BuildContext context,DurationPickerType type, TimeDuration duration, void Function(TimeDuration) onDurationChange, {TimerPreset? preset}) { - Orientation orientation = MediaQuery.of(context).orientation; - - DurationPickerType type = appSettings - .getGroup("General") - .getGroup("Display") - .getSetting("Duration Picker") - .value; Widget picker; @@ -83,6 +77,15 @@ Widget getDurationPicker(BuildContext context, TimeDuration duration, ), ); + case DurationPickerType.numpad: + picker = NumpadDurationPicker( + duration: duration, + onChange: (TimeDuration newDuration) { + onDurationChange(newDuration); + }, + + ); + break; } diff --git a/lib/timer/logic/timer_notification.dart b/lib/timer/logic/timer_notification.dart index fe6d9729..3cecb357 100644 --- a/lib/timer/logic/timer_notification.dart +++ b/lib/timer/logic/timer_notification.dart @@ -4,8 +4,6 @@ import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer.dart'; Future updateTimerNotification(ClockTimer timer, int count) async { - // print("------------ ${timer.remainingSeconds.toDouble() / - // timer.currentDuration.inSeconds.toDouble()}"); List actionButtons = []; if (count == 1) { @@ -46,9 +44,8 @@ Future updateTimerNotification(ClockTimer timer, int count) async { content: NotificationContent( id: 2, channelKey: timerNotificationChannelKey, - title: - "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}", - body: TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), + title: "${TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString()} - ${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}", + body: "Timer" , category: NotificationCategory.Progress, notificationLayout: NotificationLayout.ProgressBar, payload: { diff --git a/lib/timer/screens/presets_screen.dart b/lib/timer/screens/presets_screen.dart index 16fcab77..f928aff1 100644 --- a/lib/timer/screens/presets_screen.dart +++ b/lib/timer/screens/presets_screen.dart @@ -23,7 +23,7 @@ class _PresetsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: Text( + titleWidget: Text( AppLocalizations.of(context)!.editPresetsTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: @@ -57,6 +57,7 @@ class _PresetsScreenState extends State { // onDeleteItem: _handleDeleteTimer, placeholderText: "No presets created", reloadOnPop: true, + isSelectable: true, ), ), ], diff --git a/lib/timer/screens/timer_fullscreen.dart b/lib/timer/screens/timer_fullscreen.dart index ea6a0a5d..1e37b4c0 100644 --- a/lib/timer/screens/timer_fullscreen.dart +++ b/lib/timer/screens/timer_fullscreen.dart @@ -66,7 +66,7 @@ class _TimerFullscreenState extends State { // Orientation orientation = MediaQuery.of(context).orientation; return Scaffold( appBar: AppTopBar( - title: Text(timer.label, + titleWidget: Text(timer.label, style: textTheme.titleMedium?.copyWith( color: colorScheme.onBackground.withOpacity(0.6), )), diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 4a291c79..1886e3a5 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -1,8 +1,9 @@ -import 'package:clock_app/alarm/logic/schedule_alarm.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/routes.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/notifications/widgets/notification_actions/slide_notification_action.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/types/time_duration.dart'; @@ -36,12 +37,12 @@ class _TimerNotificationScreenState extends State { ); void _addTime() { - AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + dismissAlarmNotification(widget.scheduleIds[0], AlarmDismissType.snooze, ScheduledNotificationType.timer); } void _stop() { - AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + dismissAlarmNotification(widget.scheduleIds[0], AlarmDismissType.dismiss, ScheduledNotificationType.timer); } @@ -67,7 +68,7 @@ class _TimerNotificationScreenState extends State { '+${getTimerById(widget.scheduleIds.last)?.addLength.floor()}:00', ); - debugPrint(e.toString()); + logger.e(e.toString()); } super.initState(); diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 71f7e56e..01b8f48b 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -1,10 +1,15 @@ import 'dart:async'; +import 'dart:isolate'; import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/common/logic/customize_screen.dart'; +import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; @@ -17,21 +22,97 @@ import 'package:clock_app/timer/screens/timer_fullscreen.dart'; import 'package:clock_app/timer/widgets/timer_duration_picker.dart'; import 'package:clock_app/timer/widgets/timer_picker.dart'; import 'package:flutter/material.dart'; -import 'package:great_list_view/great_list_view.dart'; +// import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:clock_app/timer/widgets/timer_card.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -typedef TimerCardBuilder = Widget Function( - BuildContext context, - int index, - AnimatedWidgetBuilderData data, -); +// Future updateForegroundTask(List timers) async { +// final runningTimers = timers.where((timer) => !timer.isStopped).toList(); +// if (runningTimers.isEmpty) { +// FlutterForegroundTask.stopService(); +// // timerNotificationInterval?.cancel(); +// return false; +// } +// // Get timer with lowest remaining time +// final timer = runningTimers +// .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); +// final count = runningTimers.length; +// +// if (await FlutterForegroundTask.isRunningService) { +// return FlutterForegroundTask.updateService( +// notificationTitle: +// "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count - 1} timers' : ''}", +// notificationText: +// TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), +// callback: startCallback, +// ); +// } else { +// return FlutterForegroundTask.startService( +// notificationTitle: +// "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count - 1} timers' : ''}", +// notificationText: +// TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), +// callback: startCallback, +// ); +// } +// } +// +// // The callback function should always be a top-level function. +// @pragma('vm:entry-point') +// void startCallback() async { +// await initializeIsolate(); +// // The setTaskHandler function must be called to handle the task in the background. +// FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +// } +// +// class FirstTaskHandler extends TaskHandler { +// // SendPort? _sendPort; +// +// // Called when the task is started. +// @override +// void onStart(DateTime timestamp, SendPort? sendPort) async {} +// +// // Called every [interval] milliseconds in [ForegroundTaskOptions]. +// @override +// void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { +// // Send data to the main isolate. +// // sendPort?.send(timestamp); +// final timers = await loadList('timers'); +// updateForegroundTask(timers); +// } +// +// // Called when the notification button on the Android platform is pressed. +// @override +// void onDestroy(DateTime timestamp, SendPort? sendPort) async {} +// +// // Called when the notification button on the Android platform is pressed. +// @override +// void onNotificationButtonPressed(String id) { +// // print('onNotificationButtonPressed >> $id'); +// } +// +// // Called when the notification itself on the Android platform is pressed. +// // +// // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for +// // this function to be called. +// @override +// void onNotificationPressed() { +// // Note that the app will only route to "/resume-route" when it is exited so +// // it will usually be necessary to send a message through the send port to +// // signal it to restore state when the app is already started. +// FlutterForegroundTask.launchApp("/"); +// // _sendPort?.send('onNotificationPressed'); +// } +// } + class TimerScreen extends StatefulWidget { - const TimerScreen({super.key}); + const TimerScreen({super.key, this.actionController}); + + final QuickActionController? actionController; @override State createState() => _TimerScreenState(); @@ -48,13 +129,42 @@ class _TimerScreenState extends State { _listController.changeItems((timers) => {}); } + void _updateTimerNotification() { + // updateForegroundTask(_listController.getItems()); + if (!_showNotification.value) { + AwesomeNotifications() + .cancelNotificationsByChannelKey(timerNotificationChannelKey); + timerNotificationInterval?.cancel(); + return; + } + final runningTimers = + _listController.getItems().where((timer) => !timer.isStopped).toList(); + if (runningTimers.isEmpty) { + AwesomeNotifications() + .cancelNotificationsByChannelKey(timerNotificationChannelKey); + timerNotificationInterval?.cancel(); + return; + } + // Get timer with lowest remaining time + final timer = runningTimers + .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); + + updateTimerNotification(timer, runningTimers.length); + timerNotificationInterval?.cancel(); + timerNotificationInterval = Timer.periodic(const Duration(seconds: 1), (t) { + updateTimerNotification(timer, runningTimers.length); + }); + } + void onTimerUpdate() async { if (mounted) { _listController.reload(); setState(() {}); // _listController.changeItems((timers) => {}); } - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } @override @@ -75,7 +185,13 @@ class _TimerScreenState extends State { _showSort.addListener(update); _showNotification.addListener(update); ListenerManager.addOnChangeListener("timers", onTimerUpdate); - showProgressNotification(); + widget.actionController?.setAction((action) { + logger.i("Received action: $action"); + if (action == "add_timer") { + handleAddTimerAction(); + } + }); + // showProgressNotification(); } @override @@ -90,21 +206,27 @@ class _TimerScreenState extends State { Future _onDeleteTimer(ClockTimer deletedTimer) async { await deletedTimer.reset(); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); // _listController.deleteItem(deletedTimer); } Future _handleToggleState(ClockTimer timer) async { await timer.toggleState(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleStartTimer(ClockTimer timer) async { if (timer.isRunning) return; await timer.start(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleStartMultipleTimers(List timers) async { @@ -113,14 +235,17 @@ class _TimerScreenState extends State { await timer.start(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handlePauseTimer(ClockTimer timer) async { if (timer.isPaused) return; await timer.pause(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + // showProgressNotification(); } Future _handlePauseMultipleTimers(List timers) async { @@ -129,13 +254,17 @@ class _TimerScreenState extends State { await timer.pause(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleResetTimer(ClockTimer timer) async { await timer.reset(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleResetMultipleTimers(List timers) async { @@ -143,13 +272,17 @@ class _TimerScreenState extends State { await timer.reset(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleAddTimeToTimer(ClockTimer timer) async { await timer.addTime(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _openCustomizeTimerScreen( @@ -170,6 +303,28 @@ class _TimerScreenState extends State { ); } + Future handleAddTimerAction() async { + PickerResult? pickerResult = await showTimerPicker(context); + if (pickerResult != null) { + ClockTimer timer = ClockTimer.from(pickerResult.value); + if (pickerResult.isCustomize) { + await _openCustomizeTimerScreen( + timer, + onSave: (timer) async { + await timer.start(); + _listController.addItem(timer); + }, + isNewTimer: true, + ); + } else { + await timer.start(); + _listController.addItem(timer); + } + _updateTimerNotification(); + // showProgressNotification(); + } + } + Future _handleCustomizeTimer(ClockTimer timer) async { await _openCustomizeTimerScreen(timer, onSave: (newTimer) async { // Timer id gets reset after copyFrom, so we have to cancel the old one @@ -178,34 +333,40 @@ class _TimerScreenState extends State { await timer.start(); _listController.changeItems((timers) {}); }); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); return timer; } - Future showProgressNotification() async { - if (!_showNotification.value) { - AwesomeNotifications() - .cancelNotificationsByChannelKey(timerNotificationChannelKey); - timerNotificationInterval?.cancel(); - return; - } - final runningTimers = - _listController.getItems().where((timer) => !timer.isStopped).toList(); - if (runningTimers.isEmpty) { - AwesomeNotifications() - .cancelNotificationsByChannelKey(timerNotificationChannelKey); - timerNotificationInterval?.cancel(); - return; - } - // Get timer with lowest remaining time - final timer = runningTimers - .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); - - updateTimerNotification(timer, runningTimers.length); - timerNotificationInterval?.cancel(); - timerNotificationInterval = Timer.periodic(const Duration(seconds: 1), (t) { - updateTimerNotification(timer, runningTimers.length); - }); + List> _getCustomActions() { + if (!_showFilters.value) return []; + return [ + ListFilterCustomAction( + name: "Reset all filtered timers", + icon: Icons.timer_off_rounded, + action: (timers) => _handleResetMultipleTimers(timers)), + ListFilterCustomAction( + name: "Play all filtered timers", + icon: Icons.play_arrow_rounded, + action: (timers) => _handleStartMultipleTimers(timers)), + ListFilterCustomAction( + name: "Pause all filtered timers", + icon: Icons.pause_rounded, + action: (timers) => _handlePauseMultipleTimers(timers)), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.shuffleAlarmMelodiesAction, + icon: Icons.shuffle_rounded, + action: (timers) async { + List randomIndices = + await getNRandomRingtoneIndices(timers.length); + for (var timer in timers) { + final setting = timer.settings.getSetting("Melody") + as DynamicSelectSetting; + setting.setIndex(context, randomIndices.removeAt(0)); + } + }), + ]; } @override @@ -223,8 +384,8 @@ class _TimerScreenState extends State { onToggleState: () => _handleToggleState(timer), onPressDelete: () => _listController.deleteItem(timer), onPressDuplicate: () => _listController.duplicateItem(timer), - onPressReset: ()=> _handleResetTimer(timer), - onPressAddTime: ()=> _handleAddTimeToTimer(timer), + onPressReset: () => _handleResetTimer(timer), + onPressAddTime: () => _handleAddTimeToTimer(timer), ), onTapItem: (timer, index) async { await Navigator.push( @@ -243,55 +404,18 @@ class _TimerScreenState extends State { // _listController.changeItems((item) {}); }, onDeleteItem: _onDeleteTimer, + isSelectable: true, placeholderText: AppLocalizations.of(context)!.noTimerMessage, reloadOnPop: true, listFilters: _showFilters.value ? timerListFilters : [], sortOptions: _showSort.value ? timerSortOptions : [], - customActions: _showFilters.value - ? [ - ListFilterCustomAction( - name: "Reset all filtered timers", - icon: Icons.timer_off_rounded, - action: (timers) => - _handleResetMultipleTimers(timers)), - ListFilterCustomAction( - name: "Play all filtered timers", - icon: Icons.play_arrow_rounded, - action: (timers) => - _handleStartMultipleTimers(timers)), - ListFilterCustomAction( - name: "Pause all filtered timers", - icon: Icons.pause_rounded, - action: (timers) => - _handlePauseMultipleTimers(timers)), - ] - : [], + customActions: _getCustomActions(), ), ), ], ), FAB( - onPressed: () async { - PickerResult? pickerResult = - await showTimerPicker(context); - if (pickerResult != null) { - ClockTimer timer = ClockTimer.from(pickerResult.value); - if (pickerResult.isCustomize) { - await _openCustomizeTimerScreen( - timer, - onSave: (timer) async { - await timer.start(); - _listController.addItem(timer); - }, - isNewTimer: true, - ); - } else { - await timer.start(); - _listController.addItem(timer); - } - showProgressNotification(); - } - }, + onPressed: handleAddTimerAction, ) ]); } diff --git a/lib/timer/types/time_duration.dart b/lib/timer/types/time_duration.dart index fd7bc61f..7deeecf1 100644 --- a/lib/timer/types/time_duration.dart +++ b/lib/timer/types/time_duration.dart @@ -112,4 +112,18 @@ class TimeDuration extends JsonSerializable { minutes = json != null ? json['minutes'] ?? 0 : 0, seconds = json != null ? json['seconds'] ?? 0 : 0, milliseconds = json != null ? json['milliseconds'] ?? 0 : 0; + + @override + bool operator ==(Object other) { + if (other is TimeDuration) { + return hours == other.hours && + minutes == other.minutes && + seconds == other.seconds && + milliseconds == other.milliseconds; + } + return false; + } + + @override + int get hashCode => Object.hash(hours, minutes, seconds, milliseconds); } diff --git a/lib/timer/types/timer.dart b/lib/timer/types/timer.dart index 1578490c..6cd2b9e7 100644 --- a/lib/timer/types/timer.dart +++ b/lib/timer/types/timer.dart @@ -5,6 +5,7 @@ import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; @@ -60,7 +61,10 @@ class ClockTimer extends CustomizableListItem { if (isRunning) { return math.max( _milliSecondsRemainingOnPause - - DateTime.now().difference(_startTime).toTimeDuration().inMilliseconds, + DateTime.now() + .difference(_startTime) + .toTimeDuration() + .inMilliseconds, 0); } else { return _milliSecondsRemainingOnPause; @@ -77,7 +81,7 @@ class ClockTimer extends CustomizableListItem { TimerState get state => _state; ClockTimer(this._duration) - : _id = UniqueKey().hashCode, + : _id = getId(), _currentDuration = TimeDuration.from(_duration), _milliSecondsRemainingOnPause = _duration.inSeconds * 1000, _startTime = DateTime(0), @@ -90,7 +94,7 @@ class ClockTimer extends CustomizableListItem { _startTime = DateTime(0), _state = TimerState.stopped, _settings = timer._settings.copy(), - _id = UniqueKey().hashCode; + _id = getId(); void setSetting(BuildContext context, String name, dynamic value) { _settings.getSetting(name).setValue(context, value); @@ -148,6 +152,13 @@ class ClockTimer extends CustomizableListItem { } } + Future snooze() async { + TimeDuration addedDuration = TimeDuration(minutes: addLength.floor()); + _currentDuration = addedDuration; + _milliSecondsRemainingOnPause = addedDuration.inSeconds * 1000; + await start(); + } + Future pause() async { await cancelAlarm(_id, ScheduledNotificationType.timer); _milliSecondsRemainingOnPause -= @@ -211,7 +222,7 @@ class ClockTimer extends CustomizableListItem { ClockTimer.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } _duration = TimeDuration.fromSeconds(json['duration'] ?? 0); @@ -223,7 +234,7 @@ class ClockTimer extends CustomizableListItem { : DateTime.now(); _state = TimerState.values.firstWhere((e) => e.toString() == json['state'], orElse: () => TimerState.stopped); - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); _settings = SettingGroup( "Timer Settings", (context) => "Timer Settings", @@ -245,11 +256,14 @@ class ClockTimer extends CustomizableListItem { _state = other._state; _settings = other._settings.copy(); _id = other._id; - } @override copy() { return ClockTimer.from(this); } + + bool isEqualTo(ClockTimer other) { + return _duration == other._duration && _settings.isEqualTo(other._settings); + } } diff --git a/lib/timer/types/timer_preset.dart b/lib/timer/types/timer_preset.dart index 95fe5e94..aef325c5 100644 --- a/lib/timer/types/timer_preset.dart +++ b/lib/timer/types/timer_preset.dart @@ -1,13 +1,14 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/timer/types/time_duration.dart'; -import 'package:flutter/foundation.dart'; class TimerPreset extends ListItem { late int _id; String name = "Preset"; TimeDuration duration = const TimeDuration(minutes: 5); - TimerPreset(this.name, this.duration) : _id = UniqueKey().hashCode; + TimerPreset(this.name, this.duration) : _id = getId(); @override int get id => _id; @@ -15,7 +16,7 @@ class TimerPreset extends ListItem { bool get isDeletable => true; TimerPreset.from(TimerPreset preset) - : _id = UniqueKey().hashCode, + : _id = getId(), name = preset.name, duration = TimeDuration.from(preset.duration); @@ -28,10 +29,10 @@ class TimerPreset extends ListItem { TimerPreset.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); name = json['name'] ?? "Preset"; duration = TimeDuration.fromJson(json['duration']); } @@ -45,6 +46,10 @@ class TimerPreset extends ListItem { } } + bool isEqualTo(TimerPreset other) { + return name == other.name && duration == other.duration; + } + @override copy() { return TimerPreset(name, duration); diff --git a/lib/timer/widgets/duration_picker.dart b/lib/timer/widgets/duration_picker.dart index ddc71e1c..2641a25f 100644 --- a/lib/timer/widgets/duration_picker.dart +++ b/lib/timer/widgets/duration_picker.dart @@ -1,19 +1,21 @@ import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/edit_duration_picker_mode.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - Future showDurationPicker( BuildContext context, { TimeDuration initialTimeDuration = - const TimeDuration(hours: 0, minutes: 5, seconds: 0), + const TimeDuration(hours: 0, minutes: 0, seconds: 0), bool showHours = true, }) async { final theme = Theme.of(context); final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; return showDialog( context: context, @@ -30,13 +32,19 @@ Future showDurationPicker( // Get available height and width of the build area of this widget. Make a choice depending on the size. Orientation orientation = MediaQuery.of(context).orientation; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + Widget title() => Row( children: [ // const SizedBox(width: 8), Text( AppLocalizations.of(context)!.durationPickerTitle, style: TimePickerTheme.of(context).helpTextStyle ?? - Theme.of(context).textTheme.labelSmall, + textTheme.labelSmall, ), const Spacer(), TextButton( @@ -44,12 +52,9 @@ Future showDurationPicker( context, () => setState(() {})), child: Text( AppLocalizations.of(context)!.timePickerModeButton, - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), + style: textTheme.titleSmall?.copyWith( + color: colorScheme.primary, + ), ), ) ], @@ -62,6 +67,7 @@ Future showDurationPicker( Widget durationPicker() => getDurationPicker( context, + type, timeDuration, (TimeDuration newDuration) { setState(() { @@ -74,8 +80,9 @@ Future showDurationPicker( ? Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 8), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), const SizedBox(height: 16), @@ -89,8 +96,9 @@ Future showDurationPicker( // mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 16), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), ], diff --git a/lib/timer/widgets/numpad_duration_picker.dart b/lib/timer/widgets/numpad_duration_picker.dart new file mode 100644 index 00000000..cea39231 --- /dev/null +++ b/lib/timer/widgets/numpad_duration_picker.dart @@ -0,0 +1,175 @@ +import 'package:clock_app/theme/text.dart'; +import 'package:clock_app/timer/types/time_duration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class NumpadDurationPicker extends StatefulWidget { + const NumpadDurationPicker( + {super.key, required this.duration, required this.onChange}); + + final TimeDuration duration; + final void Function(TimeDuration) onChange; + + @override + State createState() => _NumpadDurationPickerState(); +} + +class _NumpadDurationPickerState extends State { + // String hours = "00"; + // String minutes = "00"; + // String seconds = "00"; + // List timeInput = ["0", "0", "0", "0", "0", "0"]; + + @override + void initState() { + super.initState(); + } + + List getTimeInput() { + final hours = widget.duration.hours.toString().padLeft(2, "0"); + final minutes = widget.duration.minutes.toString().padLeft(2, "0"); + final seconds = widget.duration.seconds.toString().padLeft(2, "0"); + return [hours[0], hours[1], minutes[0], minutes[1], seconds[0], seconds[1]]; + } + + void _addDigit(String digit, [int amount = 1]) { + setState(() { + final timeInput = getTimeInput(); + for (int i = 0; i < amount; i++) { + timeInput.removeAt(0); + timeInput.add(digit); + } + _update(timeInput); + }); + } + + void _removeDigit() { + setState(() { + final timeInput = getTimeInput(); + timeInput.removeAt(5); + timeInput.insert(0, "0"); + _update(timeInput); + }); + } + + void _update(List timeInput) { + widget.onChange(TimeDuration( + hours: int.parse("${timeInput[0]}${timeInput[1]}"), + minutes: int.parse("${timeInput[2]}${timeInput[3]}"), + seconds: int.parse("${timeInput[4]}${timeInput[5]}"), + )); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + final labelStyle = textTheme.headlineLarge + ?.copyWith(color: colorScheme.onSurface, height: 1); + final labelUnitStyle = + textTheme.headlineMedium?.copyWith(color: colorScheme.onSurface); + + double originalWidth = MediaQuery.of(context).size.width; + + final hours = widget.duration.hours.toString().padLeft(2, "0"); + final minutes = widget.duration.minutes.toString().padLeft(2, "0"); + final seconds = widget.duration.seconds.toString().padLeft(2, "0"); + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(hours, style: labelStyle), + Text("h", style: labelUnitStyle), + const SizedBox(width: 10), + Text(minutes, style: labelStyle), + Text("m", style: labelUnitStyle), + const SizedBox(width: 10), + Text(seconds, style: labelStyle), + Text("s", style: labelUnitStyle), + ], + ), + const SizedBox(height: 4), + SizedBox( + width: originalWidth * 0.76, + height: originalWidth * 1.1, + child: GridView.builder( + padding: const EdgeInsets.symmetric(vertical: 12), + shrinkWrap: true, + itemCount: 12, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + ), + itemBuilder: (context, index) { + if (index < 9) { + return TimerButton( + label: (index + 1).toString(), + onTap: () => _addDigit((index + 1).toString()), + ); + } else if (index == 9) { + return TimerButton( + isHighlighted: true, + label: "00", + onTap: () { + _addDigit("0", 2); + }); + } else if (index == 10) { + return TimerButton( + label: "0", + onTap: () => _addDigit("0"), + ); + } else { + return TimerButton( + isHighlighted: true, + icon: Icons.backspace_outlined, + onTap: _removeDigit, + ); + } + }, + ), + ), + ], + ); + } +} + +class TimerButton extends StatelessWidget { + final String? label; + final IconData? icon; + final VoidCallback onTap; + final bool isHighlighted; + + const TimerButton( + {super.key, this.label, required this.onTap, this.icon, this.isHighlighted = false}); + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(100), + child: Container( + decoration: BoxDecoration( + color: isHighlighted ? colorScheme.primary.withOpacity(0.2) : colorScheme.onBackground.withOpacity(0.1), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: label != null + ? Text( + label!, + style: textTheme.titleMedium + ?.copyWith(color: colorScheme.onSurface), + ) + : icon != null + ? Icon(icon, color: colorScheme.onSurface) + : Container()), + ), + ); + } +} diff --git a/lib/timer/widgets/timer_picker.dart b/lib/timer/widgets/timer_picker.dart index a04e61a8..d99ea7e4 100644 --- a/lib/timer/widgets/timer_picker.dart +++ b/lib/timer/widgets/timer_picker.dart @@ -2,6 +2,8 @@ import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/edit_duration_picker_mode.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/screens/presets_screen.dart'; @@ -23,7 +25,7 @@ Future?> showTimerPicker( context: context, builder: (BuildContext context) { ClockTimer timer = ClockTimer.from( - initialTimer ?? ClockTimer(const TimeDuration(minutes: 5))); + initialTimer ?? ClockTimer(const TimeDuration(minutes: 0))); TimerPreset? selectedPreset; List presets = loadListSync("timer_presets"); @@ -47,6 +49,12 @@ Future?> showTimerPicker( builder: (context) { var width = MediaQuery.of(context).size.width; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + Widget presetChips(double width) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -118,6 +126,7 @@ Future?> showTimerPicker( Widget durationPicker(double width) => getDurationPicker( context, + type, timer.duration, (TimeDuration newDuration) { setState(() { @@ -126,9 +135,10 @@ Future?> showTimerPicker( }, preset: selectedPreset, ); - - Widget label() => Text(timer.duration.toString(), - style: textTheme.displayMedium); + Widget label() => Text( + timer.duration.toString(), + style: textTheme.displayMedium, + ); Widget title() => Row( children: [ @@ -161,8 +171,9 @@ Future?> showTimerPicker( mainAxisSize: MainAxisSize.min, children: [ title(), - const SizedBox(height: 16), - label(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) label(), const SizedBox(height: 16), durationPicker(width), const SizedBox(height: 16), @@ -176,8 +187,9 @@ Future?> showTimerPicker( // mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 16), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), const SizedBox(height: 16), diff --git a/lib/timer/widgets/timer_preset_picker.dart b/lib/timer/widgets/timer_preset_picker.dart index 999ae50b..1da3804d 100644 --- a/lib/timer/widgets/timer_preset_picker.dart +++ b/lib/timer/widgets/timer_preset_picker.dart @@ -1,4 +1,6 @@ import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer_preset.dart'; @@ -14,7 +16,7 @@ Future showTimerPresetPicker(BuildContext context, context: context, builder: (BuildContext context) { TimerPreset timerPreset = TimerPreset.from(initialTimerPreset ?? - TimerPreset("New Preset", const TimeDuration(minutes: 5))); + TimerPreset("New Preset", const TimeDuration(minutes: 0))); TextEditingController controller = TextEditingController( text: timerPreset.name, @@ -29,6 +31,12 @@ Future showTimerPresetPicker(BuildContext context, // Get available height and width of the build area of this widget. Make a choice depending on the size. var width = MediaQuery.of(context).size.width; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -53,7 +61,7 @@ Future showTimerPresetPicker(BuildContext context, Text(timerPreset.duration.toString(), style: textTheme.displayMedium), const SizedBox(height: 16), - getDurationPicker(context, timerPreset.duration, + getDurationPicker(context, type, timerPreset.duration, (TimeDuration newDuration) { setState(() { timerPreset.duration = newDuration; diff --git a/lib/settings/data/widget_settings_schema.dart b/lib/widgets/data/widget_settings_schema.dart similarity index 100% rename from lib/settings/data/widget_settings_schema.dart rename to lib/widgets/data/widget_settings_schema.dart diff --git a/lib/widgets/logic/update_widgets.dart b/lib/widgets/logic/update_widgets.dart index a60ee62b..982b7534 100644 --- a/lib/widgets/logic/update_widgets.dart +++ b/lib/widgets/logic/update_widgets.dart @@ -1,4 +1,5 @@ import 'package:clock_app/common/utils/time_format.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; @@ -58,7 +59,7 @@ void setDigitalClockWidgetData(BuildContext context) async { updateDigitalClockWidget(); } catch (e) { - debugPrint("Couldn't update Digital Clock Widget: $e"); + logger.e("Couldn't update Digital Clock Widget: $e"); } } diff --git a/pubspec.lock b/pubspec.lock index e8de28dc..a3c9c2ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "61.0.0" + analog_clock: + dependency: "direct main" + description: + name: analog_clock + sha256: "6550dc371c5834d76295ec7b50c44b96c52f60e9bb2dd49568a3cb100e4b289e" + url: "https://pub.dev" + source: hosted + version: "0.1.1" analyzer: dependency: transitive description: @@ -30,10 +38,18 @@ packages: description: path: "packages/android_alarm_manager_plus" ref: alarm_show_intent - resolved-ref: "2b9a1f38f0f5f647153507cae3a0f00c896fd06a" + resolved-ref: ae6c11c3eccd66ff79827424cbf8c208f727282e url: "https://github.com/AhsanSarwar45/plus_plugins" source: git version: "4.0.1" + animated_analog_clock: + dependency: "direct main" + description: + name: animated_analog_clock + sha256: "00ef12809ad0a95d1f6e50264e5b109a65448d97ba677be23d766261fb3378ce" + url: "https://pub.dev" + source: hosted + version: "0.1.0" ansicolor: dependency: transitive description: @@ -90,6 +106,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + background_fetch: + dependency: "direct main" + description: + name: background_fetch + sha256: e9f26ae54d88310b7ac2a68f2f9fcee0081a4d5f11100f233a70702021e7ac4f + url: "https://pub.dev" + source: hosted + version: "1.3.7" boolean_selector: dependency: transitive description: @@ -131,7 +155,7 @@ packages: source: hosted version: "2.0.3" clock: - dependency: transitive + dependency: "direct main" description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf @@ -154,6 +178,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -226,14 +258,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - diffutil_dart: - dependency: transitive - description: - name: diffutil_dart - sha256: e0297e4600b9797edff228ed60f4169a778ea357691ec98408fa3b72994c7d06 - url: "https://pub.dev" - source: hosted - version: "3.0.0" dots_indicator: dependency: transitive description: @@ -282,6 +306,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" + url: "https://pub.dev" + source: hosted + version: "8.0.7" flex_color_picker: dependency: "direct main" description: @@ -304,6 +336,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + url: "https://pub.dev" + source: hosted + version: "4.5.0" flutter_boot_receiver: dependency: "direct main" description: @@ -320,6 +360,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + flutter_foreground_task: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "6ab85aadb67e68377ec14beefe7ba7ea7fb34caa" + url: "https://github.com/vicolo-dev/flutter_foreground_task" + source: git + version: "6.5.0" flutter_html: dependency: "direct main" description: @@ -397,6 +446,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + url: "https://pub.dev" + source: hosted + version: "2.0.22" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + url: "https://pub.dev" + source: hosted + version: "0.1.2" flutter_show_when_locked: dependency: "direct main" description: @@ -463,24 +528,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - great_list_view: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: "6fb91f6f3b2480c490bfa57cb7a8b18e4db2c117" - url: "https://github.com/AhsanSarwar45/great_list_view" - source: git - version: "0.2.3" home_widget: dependency: "direct main" description: - path: "." + path: "packages/home_widget" ref: main - resolved-ref: "5788ac45f62bef72ff44c52cbd77dc1f9258f633" + resolved-ref: "28bf6db2761467209a5244971ccb803a50a8cbb0" url: "https://github.com/AhsanSarwar45/home_widget" source: git - version: "0.5.0" + version: "0.7.0" html: dependency: transitive description: @@ -565,26 +621,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -609,6 +665,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: transitive description: @@ -637,10 +701,18 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" + mime: + dependency: "direct main" + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" move_to_background: dependency: "direct main" description: @@ -685,10 +757,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -701,10 +773,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -785,14 +857,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" - pick_or_save: - dependency: "direct main" - description: - name: pick_or_save - sha256: "5e562e714e8486000b1144e580dfbd6db888a0b4dd02bf4c28501b244dd22fd3" - url: "https://pub.dev" - source: hosted - version: "2.2.4" platform: dependency: transitive description: @@ -849,6 +913,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+2" + quick_actions: + dependency: "direct main" + description: + name: quick_actions + sha256: b17da113df7a7005977f64adfa58ccc49c829d3ccc6e8e770079a8c7fbf2da9e + url: "https://pub.dev" + source: hosted + version: "1.0.7" + quick_actions_android: + dependency: transitive + description: + name: quick_actions_android + sha256: "54a581491b90ff2e1be94af84a40c05e806e232184bb32afa2df57b07c4d6882" + url: "https://pub.dev" + source: hosted + version: "1.0.15" + quick_actions_ios: + dependency: transitive + description: + name: quick_actions_ios + sha256: "402596dea62a1028960b93f7651ec22be0e2a91e4fbf92a1c62d3b95f8ff95a5" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + quick_actions_platform_interface: + dependency: transitive + description: + name: quick_actions_platform_interface + sha256: "81a1e40c519bb3cacfec38b3008b13cef665a75bd270da94f40091b57f0f9236" + url: "https://pub.dev" + source: hosted + version: "1.0.6" receive_intent: dependency: "direct main" description: @@ -865,6 +961,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" simple_gesture_detector: dependency: transitive description: @@ -954,10 +1106,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timer_builder: dependency: "direct main" description: @@ -970,10 +1122,10 @@ packages: dependency: "direct main" description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.4" typed_data: dependency: transitive description: @@ -1074,10 +1226,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: "direct main" description: @@ -1098,10 +1250,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.5.4" win32_registry: dependency: transitive description: @@ -1110,14 +1262,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - worker_manager: - dependency: transitive - description: - name: worker_manager - sha256: "42501e49ee0acad9eeda562984e3dcfe6fe3d26f2d8dc410bd76308a86447eb5" - url: "https://pub.dev" - source: hosted - version: "5.0.3" xdg_directories: dependency: transitive description: @@ -1143,5 +1287,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.1 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 068c8a30..88a1fd13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,10 +2,10 @@ name: clock_app description: An alarm, clock, timer and stowatch app. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.5.1+24 +version: 0.6.0-beta1+26 environment: - sdk: ">=2.18.6 <4.0.0" + sdk: '>=3.4.0 <4.0.0' dependencies: flutter: @@ -24,7 +24,7 @@ dependencies: sqflite: ^2.2.2 flutter_slidable: ^3.1.0 flutter_system_ringtones: ^0.0.6 - # android_alarm_manager_plus: ^4.0.1 + # android_alarm_manager_plus: ^4.0.4 android_alarm_manager_plus: # path: "../plus_plugins/packages/android_alarm_manager_plus/" git: @@ -33,14 +33,12 @@ dependencies: ref: alarm_show_intent just_audio: ^0.9.31 awesome_notifications: ^0.9.3 + # awesome_notifications: + # path: "../awesome_notifications" audio_session: ^0.1.13 flutter_fgbg: ^0.3.0 move_to_background: ^1.0.2 vibration: ^1.7.6 - great_list_view: - git: - url: https://github.com/AhsanSarwar45/great_list_view - ref: master get_storage: ^2.1.1 queue: ^3.1.0+2 table_calendar: ^3.0.8 @@ -61,7 +59,6 @@ dependencies: introduction_screen: ^3.1.12 app_settings: ^5.1.1 auto_start_flutter: ^0.1.1 - pick_or_save: ^2.2.4 package_info_plus: ^6.0.0 receive_intent: ^0.2.5 watcher: ^1.1.0 @@ -74,9 +71,27 @@ dependencies: git: url: https://github.com/AhsanSarwar45/home_widget ref: main + path: packages/home_widget/ permission_handler: ^11.3.1 device_info_plus: ^10.1.0 - + flutter_foreground_task: + # path: "../flutter_foreground_task" + git: + url: https://github.com/vicolo-dev/flutter_foreground_task + ref: master + logger: ^2.4.0 + flutter_animate: ^4.5.0 + quick_actions: ^1.0.7 + file_picker: ^8.0.7 + background_fetch: ^1.3.7 + clock: ^1.1.1 + mime: ^1.0.6 + analog_clock: ^0.1.1 + animated_analog_clock: ^0.1.0 + # animated_reorderable_list: ^1.1.1 + # animated_reorderable_list: + # path: "../animated_reorderable_list" + # dev_dependencies: flutter_test: diff --git a/test/alarm/logic/alarm_time.dart b/test/alarm/logic/alarm_time.dart new file mode 100644 index 00000000..5d365a94 --- /dev/null +++ b/test/alarm/logic/alarm_time.dart @@ -0,0 +1,305 @@ +import 'package:clock/clock.dart'; +import 'package:clock_app/alarm/logic/alarm_time.dart'; +import 'package:clock_app/common/types/time.dart'; +import 'package:flutter_test/flutter_test.dart'; + +DateTime currentDate = DateTime(2000, 1, 10, 10, 0); +DateTime futureScheduleStartDate = DateTime(2000, 1, 20, 10, 0); +DateTime pastScheduleStartDate = DateTime(2000, 1, 1, 10, 0); + +void testGetScheduleDateForTime(Time scheduleTime, DateTime expectedDate, + {DateTime? scheduleStartDate, int interval = 1}) { + withClock( + Clock.fixed(currentDate), + () { + DateTime scheduledDateTime = getScheduleDateForTime( + scheduleTime, + interval: interval, + scheduleStartDate: scheduleStartDate, + ); + expect( + scheduledDateTime, + DateTime( + expectedDate.year, + expectedDate.month, + expectedDate.day, + scheduleTime.hour, + scheduleTime.minute, + scheduleTime.second, + )); + }, + ); +} + +void main() async { + group('getScheduleDateForTime()', () { + group('with interval = 1', () { + group('without scheduleStartDate', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + )); + }, + ); + test( + "returns tomorrow's date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + )); + }, + ); + test( + "returns tomorrow's date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + )); + }, + ); + }); + group('with scheduleStartDate in the future', () { + test( + "returns scheduleStartDate when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + test( + "returns day after scheduleStartDate when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + test( + "returns day after scheduleStartDate when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + }); + group('with scheduleStartDate in the past', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + test( + "returns tomorrow's date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + test( + "returns tomorrow's date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + }); + }); + group('with interval = 7', () { + group('without scheduleStartDate', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + ), + interval: 7, + ); + }, + ); + test( + "returns next week date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 7, + ), + interval: 7, + ); + }, + ); + test( + "returns next week date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 7, + ), + interval: 7, + ); + }, + ); + }); + group('with scheduleStartDate in the future', () { + test( + "returns scheduleStartDate when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns scheduleStartDate when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns scheduleStartDate when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + }); + group('with scheduleStartDate in the past', () { + test( + "returns correctly when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns correctly when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns returns correctly when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + }); + }); + }); +} diff --git a/test/alarm/logic/schedule_description_test.dart b/test/alarm/logic/schedule_description_test.dart index 5c717db5..70dcc197 100644 --- a/test/alarm/logic/schedule_description_test.dart +++ b/test/alarm/logic/schedule_description_test.dart @@ -26,8 +26,8 @@ void testDescription(String name, Function(BuildContext) callback) { } void main() async { - group('getAlarmScheduleDescription', () { - testDescription('when alarm is snoozed', (context) async { + group('getAlarmScheduleDescription()', () { + testDescription('returns correctly when alarm is snoozed', (context) async { final alarm = Alarm(const Time(hour: 8, minute: 30)); await alarm.snooze(); @@ -52,7 +52,8 @@ void main() async { // expect(result, 'No future dates'); // }); - testDescription('when alarm is not enabled', (context) async { + testDescription('returns correctly when alarm is not enabled', + (context) async { final alarm = Alarm(const Time(hour: 8, minute: 30)); await alarm.disable(); @@ -63,7 +64,8 @@ void main() async { expect(result, 'Not scheduled'); }); - testDescription('when alarm has once schedule', (context) { + testDescription('returns correctly when alarm has once schedule', + (context) { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 0); @@ -76,7 +78,8 @@ void main() async { ); }); - testDescription('when alarm has daily schedule', (context) { + testDescription('returns correctly when alarm has daily schedule', + (context) { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 1); @@ -86,7 +89,7 @@ void main() async { expect(result, 'Every day'); }); - group('when alarm has weekly schedule', () { + group('returns correctly when alarm has weekly schedule', () { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 2); testDescription("with all week days", (context) { @@ -99,7 +102,7 @@ void main() async { expect(result, 'Every day'); }); - testDescription("with only weekends", (context) { + testDescription("returns correctly with only weekends", (context) { alarm.setSettingWithoutNotify( "Week Days", [false, false, false, false, false, true, true]); @@ -108,7 +111,7 @@ void main() async { expect(result, 'Every weekend'); }); - testDescription("with only weekdays", (context) { + testDescription("returns correctly with only weekdays", (context) { alarm.setSettingWithoutNotify( "Week Days", [true, true, true, true, true, false, false]); @@ -117,7 +120,7 @@ void main() async { expect(result, 'Every weekday'); }); - testDescription("with other week days", (context) { + testDescription("returns correctly with other week days", (context) { alarm.setSettingWithoutNotify( "Week Days", [true, false, false, false, false, false, true]);