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