From cfce5be27a2345dea49d5b80dd16c087870dc08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20F?= Date: Thu, 28 Mar 2024 01:15:22 +0000 Subject: [PATCH 001/177] Translated using Weblate (Spanish) Currently translated at 12.5% (1 of 8 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/es/ --- fastlane/metadata/android/es-ES/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/es-ES/short_description.txt 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..e558d9ef --- /dev/null +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +¡Una moderna y potente aplicación de reloj, alarmas, temporizador y cronómetro para Android! From 9b9d4c4db2d21c3e3e28e7183dc25a013638dbd6 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:03:03 +0100 Subject: [PATCH 002/177] Translated using Weblate (English) Currently translated at 100.0% (8 of 8 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/101.txt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/101.txt 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 - From 1302c3b99b2711b6c06d752aba1c186482aacd7a Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:03:18 +0100 Subject: [PATCH 003/177] Translated using Weblate (English) Currently translated at 100.0% (7 of 7 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/102.txt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/102.txt 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 - From 693c07b5f130d3d7818fb592cd5d20913c2a4baa Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:03:38 +0100 Subject: [PATCH 004/177] Translated using Weblate (English) Currently translated at 100.0% (6 of 6 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/103.txt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/103.txt 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 - From ad26155969068aa0a06f5704570f13939416b8b5 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:03:51 +0100 Subject: [PATCH 005/177] Translated using Weblate (English) Currently translated at 100.0% (5 of 5 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/91.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/91.txt 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 From 582f59ad49b3d6506094157f8c273f9bc8b44f50 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:04:40 +0100 Subject: [PATCH 006/177] Translated using Weblate (English) Currently translated at 100.0% (4 of 4 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/92.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/92.txt 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 From 5a7319d1dc683470ef00b5133a50aba244425b7d Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 12:04:47 +0100 Subject: [PATCH 007/177] Translated using Weblate (English) Currently translated at 100.0% (3 of 3 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/changelogs/93.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 fastlane/metadata/android/en-US/changelogs/93.txt 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 From ba5340553d1ad7ba68c1e0ac5120fd3ac5768ed1 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Thu, 28 Mar 2024 14:49:21 +0000 Subject: [PATCH 008/177] Translated using Weblate (English) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/full_description.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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)
From 844d245c284b9d61dec68224a161ce275a8c2453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Thu, 28 Mar 2024 13:55:30 +0000 Subject: [PATCH 009/177] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/nb_NO/ --- .../android/no-NO/full_description.txt | 35 +++++++++++++++++++ .../android/no-NO/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/no-NO/full_description.txt create mode 100644 fastlane/metadata/android/no-NO/short_description.txt 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. From 82423f2cd56e792c9cc1e287c6749fee6fe1ac8e Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 28 Mar 2024 15:28:23 +0000 Subject: [PATCH 010/177] Translated using Weblate (Spanish) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/es/ --- .../android/es-ES/full_description.txt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 fastlane/metadata/android/es-ES/full_description.txt 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
  • +
From a9799b754c11e71e36ff3bd2a3e3e1ba41e26be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 29 Mar 2024 16:30:16 +0000 Subject: [PATCH 011/177] Translated using Weblate (English) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index b03b9d5c..26cf23fa 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 From 123c65663f281252fed20c534a12c70fd3b283b5 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Fri, 29 Mar 2024 16:31:44 +0000 Subject: [PATCH 012/177] Translated using Weblate (English) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/en/ --- fastlane/metadata/android/en-US/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 26cf23fa..7b7984cb 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Modern and powerful clock, alarms, timer and stopwatch +Modern and powerful clock, alarms, timer and stopwatch. From a32a9d443c96236d27ef9b45bc88d2c78e620ab6 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 29 Mar 2024 20:29:06 +0000 Subject: [PATCH 013/177] Translated using Weblate (Spanish) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/es/ --- fastlane/metadata/android/es-ES/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt index e558d9ef..62c625cc 100644 --- a/fastlane/metadata/android/es-ES/short_description.txt +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -¡Una moderna y potente aplicación de reloj, alarmas, temporizador y cronómetro para Android! +Reloj, alarmas, cronómetro y cronómetro moderno y potente. From 5d636bc970000f6216941eba175d504cae2b96aa Mon Sep 17 00:00:00 2001 From: Muha Aliss Date: Fri, 12 Apr 2024 15:48:53 +0000 Subject: [PATCH 014/177] Translated using Weblate (Turkish) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/tr/ --- .../android/tr-TR/full_description.txt | 35 +++++++++++++++++++ .../android/tr-TR/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/tr-TR/full_description.txt create mode 100644 fastlane/metadata/android/tr-TR/short_description.txt 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. From 270fb1e74856d1b2a83595ead47d069d4833d8b3 Mon Sep 17 00:00:00 2001 From: UngoIiant Date: Sun, 14 Apr 2024 13:36:23 +0000 Subject: [PATCH 015/177] Translated using Weblate (German) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/de/ --- .../android/de-DE/full_description.txt | 35 +++++++++++++++++++ .../android/de-DE/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/full_description.txt create mode 100644 fastlane/metadata/android/de-DE/short_description.txt diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt new file mode 100644 index 00000000..cfec22b2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -0,0 +1,35 @@ +

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. From b6b0bc2937d85869e491c7f78657ebe0099634c6 Mon Sep 17 00:00:00 2001 From: STVGecko Date: Sat, 13 Apr 2024 16:47:52 +0000 Subject: [PATCH 016/177] Translated using Weblate (French) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/fr/ --- .../android/fr-FR/full_description.txt | 35 +++++++++++++++++++ .../android/fr-FR/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 fastlane/metadata/android/fr-FR/short_description.txt 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. From 2a2fe20e81f0ad8c1c8f8d6a8a0663b1a9cc1dc7 Mon Sep 17 00:00:00 2001 From: ngocanhtve Date: Thu, 18 Apr 2024 13:20:21 +0000 Subject: [PATCH 017/177] Translated using Weblate (Vietnamese) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/vi/ --- .../metadata/android/vi/full_description.txt | 35 +++++++++++++++++++ .../metadata/android/vi/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt 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ẽ. From 018c526bf502771722a1ca76dcf9359e82ac1847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8D=E4=BA=88?= Date: Fri, 10 May 2024 08:30:33 +0000 Subject: [PATCH 018/177] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/zh_Hans/ --- .../android/zh-CN/full_description.txt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/full_description.txt 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 主题
  • +
  • 高度可定制的颜色主题
  • +
  • 高度可定制的样式主题
  • +
From bb460db794aaf658c14b1a6421801c832150a403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=94?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D1=8E=D0=B4?= <79773329+spiderVS@users.noreply.github.com> Date: Thu, 16 May 2024 07:18:42 +0000 Subject: [PATCH 019/177] Translated using Weblate (Russian) Currently translated at 50.0% (1 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/ru/ --- fastlane/metadata/android/ru-RU/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/ru-RU/short_description.txt 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 @@ +Современные и многофункциональные часы, будильник, таймер и секундомер. From 0cf59251bd791ec99b4ed1b8dce46275bc522c88 Mon Sep 17 00:00:00 2001 From: Ahsan Sarwar Date: Fri, 24 May 2024 14:44:08 +0000 Subject: [PATCH 020/177] Translated using Weblate (Turkish) Currently translated at 99.7% (333 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/tr/ --- lib/l10n/app_tr.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 0b5daa01..712c6fb1 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -225,7 +225,7 @@ "@showSecondsSetting": {}, "pickerInput": "Girdi", "@pickerInput": {}, - "timePickerSetting": "https://hosted.weblate.org/translate/flathub/frontend/tr/?checksum=f9ea9ac0139d213e", + "timePickerSetting": "", "@timePickerSetting": {}, "ignoreBatteryOptimizationSetting": "Batarya İyileştirmesini Yoksay", "@ignoreBatteryOptimizationSetting": {}, From 4db867639da9f24193ea3655153ade7709f2e3fd Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sat, 25 May 2024 06:36:26 +0200 Subject: [PATCH 021/177] Added translation using Weblate (Ukrainian) --- fastlane/metadata/android/zh-CN/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/zh-CN/short_description.txt 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 @@ +时尚又强大的时钟,闹钟,计时器和秒表应用。 From cb1a1ee293e9d5c7bca349afe0dc5908bf427132 Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sat, 25 May 2024 07:05:33 +0200 Subject: [PATCH 022/177] Added translation using Weblate (Ukrainian) --- lib/l10n/app_uk.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/app_uk.arb diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lib/l10n/app_uk.arb @@ -0,0 +1 @@ +{} From 4d44132adca8d76c27b8e76fef1711dd6aa4937d Mon Sep 17 00:00:00 2001 From: Unacceptium Date: Sat, 25 May 2024 08:06:55 +0200 Subject: [PATCH 023/177] Added translation using Weblate (Hungarian) --- lib/l10n/app_hu.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/app_hu.arb diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lib/l10n/app_hu.arb @@ -0,0 +1 @@ +{} From 273189a63213e22619a8778a5417ad974882b3e6 Mon Sep 17 00:00:00 2001 From: Stzyxh Date: Sat, 25 May 2024 08:51:04 +0000 Subject: [PATCH 024/177] Translated using Weblate (German) Currently translated at 100.0% (334 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/de/ --- lib/l10n/app_de.arb | 484 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 477 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ff4d5477..7df8e5a5 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -91,7 +91,7 @@ "@cancelButton": {}, "customizeButton": "Anpassen", "@customizeButton": {}, - "saveButton": "Speichern Button", + "saveButton": "Speichern", "@saveButton": {}, "labelField": "Bezeichnung", "@labelField": {}, @@ -117,7 +117,7 @@ "@scheduleTypeRange": {}, "settingGroupMore": "Mehr", "@settingGroupMore": {}, - "soundSettingGroup": "Ton und Vibration", + "soundSettingGroup": "Ton", "@soundSettingGroup": {}, "vibrationSetting": "Vibration", "@vibrationSetting": {}, @@ -177,7 +177,7 @@ "@system": {}, "languageSetting": "Sprache", "@languageSetting": {}, - "timeFormat24": "24 Stunden Format", + "timeFormat24": "24 Stunden", "@timeFormat24": {}, "timeFormatDevice": "Geräte Einstellungen", "@timeFormatDevice": {}, @@ -187,7 +187,7 @@ "@pickerInput": {}, "pickerSpinner": "Wählrad", "@pickerSpinner": {}, - "durationPickerSetting": "Dauer Auswahl", + "durationPickerSetting": "Auswahl der Dauer", "@durationPickerSetting": {}, "pickerRings": "Ringe", "@pickerRings": {}, @@ -201,8 +201,478 @@ "@dateFormatSetting": {}, "timeFormatSetting": "Zeit Format", "@timeFormatSetting": {}, - "timeFormat12": "12 Stunden Format", + "timeFormat12": "12 Stunden", "@timeFormat12": {}, - "timePickerSetting": "Uhrzeit Auswähler", - "@timePickerSetting": {} + "timePickerSetting": "Zeitauswahl", + "@timePickerSetting": {}, + "translateDescription": "Helfe die App zu übersetzen", + "@translateDescription": {}, + "batteryOptimizationSetting": "Manuelles Deaktivieren der Akku-Optimierung", + "@batteryOptimizationSetting": {}, + "styleThemeShapeSettingGroup": "Form", + "@styleThemeShapeSettingGroup": {}, + "styleThemeRadiusSetting": "Rundheit der Ecken", + "@styleThemeRadiusSetting": {}, + "styleThemeElevationSetting": "Erhebung", + "@styleThemeElevationSetting": {}, + "errorLabel": "Fehler", + "@errorLabel": {}, + "styleThemeOpacitySetting": "Deckkraft", + "@styleThemeOpacitySetting": {}, + "previewLabel": "Vorschau", + "@previewLabel": {}, + "cardLabel": "Karte", + "@cardLabel": {}, + "accentLabel": "Akzent", + "@accentLabel": {}, + "materialBrightnessSystem": "System", + "@materialBrightnessSystem": {}, + "materialBrightnessDark": "Dunkel", + "@materialBrightnessDark": {}, + "accentColorSetting": "Akzent Farbe", + "@accentColorSetting": {}, + "alarmRangeSetting": "Datumsbereich", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Intervall", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Täglich", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Nach Beendigung löschen", + "@alarmDeleteAfterFinishingSetting": {}, + "audioChannelAlarm": "Alarm", + "@audioChannelAlarm": {}, + "audioChannelRingtone": "Klingelton", + "@audioChannelRingtone": {}, + "mathEasyDifficulty": "Einfach (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Mittel (X × Y)", + "@mathMediumDifficulty": {}, + "saveReminderAlert": "Wollen Sie schliesen, ohne zu speichern?", + "@saveReminderAlert": {}, + "noTagsMessage": "Keine Stichwörter erstellt", + "@noTagsMessage": {}, + "createdDateFilterGroup": "Erstellungsdatum", + "@createdDateFilterGroup": {}, + "todayFilter": "Heute", + "@todayFilter": {}, + "scheduleDateFilterGroup": "Zeitplan Datum", + "@scheduleDateFilterGroup": {}, + "durationAsc": "Kürzeste", + "@durationAsc": {}, + "enableAllFilteredAlarmsAction": "Alle gefilterten Alarme aktivieren", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Alle gefilterten Alarme deaktivieren", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Alle gefilterten Alarme überspringen", + "@skipAllFilteredAlarmsAction": {}, + "openSourceLicensesSetting": "Open Source Lizenzen", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Mitwirkende", + "@contributorsSetting": {}, + "donorsSetting": "Spendende", + "@donorsSetting": {}, + "editButton": "Bearbeiten", + "@editButton": {}, + "elapsedTime": "Verstrichene Zeit", + "@elapsedTime": {}, + "alarmDeleteAfterRingingSetting": "Löschen nach verpassten Weckruf", + "@alarmDeleteAfterRingingSetting": {}, + "noLapsMessage": "Noch keine Runden", + "@noLapsMessage": {}, + "wednesdayFull": "Mittwoch", + "@wednesdayFull": {}, + "mondayLetter": "M", + "@mondayLetter": {}, + "tuesdayLetter": "D", + "@tuesdayLetter": {}, + "thursdayLetter": "D", + "@thursdayLetter": {}, + "fridayLetter": "F", + "@fridayLetter": {}, + "sundayLetter": "S", + "@sundayLetter": {}, + "horizontalAlignmentSetting": "Horizontale Ausrichtung", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Vertikale Ausrichtung", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Oben", + "@alignmentTop": {}, + "alignmentBottom": "Unten", + "@alignmentBottom": {}, + "alignmentJustify": "Begründen", + "@alignmentJustify": {}, + "dateSettingGroup": "Datum", + "@dateSettingGroup": {}, + "timeSettingGroup": "Zeit", + "@timeSettingGroup": {}, + "sizeSetting": "Größe", + "@sizeSetting": {}, + "fontWeightSetting": "Schriftstärke", + "@fontWeightSetting": {}, + "showMeridiemSetting": "AM/PM anzeigen", + "@showMeridiemSetting": {}, + "firstDayOfWeekSetting": "Erster Tag der Woche", + "@firstDayOfWeekSetting": {}, + "separatorSetting": "Trennzeichen", + "@separatorSetting": {}, + "editTagLabel": "Stichwort bearbeiten", + "@editTagLabel": {}, + "tagNamePlaceholder": "Stichwort Name", + "@tagNamePlaceholder": {}, + "timePickerModeButton": "Modus", + "@timePickerModeButton": {}, + "maxSnoozesSetting": "Maximale Betätigung des Slummermoduses", + "@maxSnoozesSetting": {}, + "audioChannelMedia": "Medien", + "@audioChannelMedia": {}, + "timeToFullVolumeSetting": "Zeit bis zur vollen Lautstärke", + "@timeToFullVolumeSetting": {}, + "snoozePreventDeletionSetting": "Löschung verhindern", + "@snoozePreventDeletionSetting": {}, + "noButton": "Nein", + "@noButton": {}, + "noAlarmMessage": "Keine Alarme erstellt", + "@noAlarmMessage": {}, + "noTimerMessage": "Keine Timer erstellt", + "@noTimerMessage": {}, + "noStopwatchMessage": "Keine Stoppuhren erstellt", + "@noStopwatchMessage": {}, + "noTaskMessage": "Keine Aufgaben erstellt", + "@noTaskMessage": {}, + "noLogsMessage": "Keine Alarm Protokolle", + "@noLogsMessage": {}, + "noPresetsMessage": "Keine Voreinstellungen erstellt", + "@noPresetsMessage": {}, + "cancelSkipAlarmButton": "Überspringen abbrechen", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Ablehnen", + "@dismissAlarmButton": {}, + "allFilter": "Alle", + "@allFilter": {}, + "dateFilterGroup": "Datum", + "@dateFilterGroup": {}, + "logTypeFilterGroup": "Typ", + "@logTypeFilterGroup": {}, + "deleteButton": "Löschen", + "@deleteButton": {}, + "duplicateButton": "Duplizieren", + "@duplicateButton": {}, + "skipAlarmButton": "Nächsten Alarm überspringen", + "@skipAlarmButton": {}, + "tomorrowFilter": "Morgen", + "@tomorrowFilter": {}, + "stateFilterGroup": "Zustand", + "@stateFilterGroup": {}, + "activeFilter": "Aktiv", + "@activeFilter": {}, + "inactiveFilter": "Inaktiv", + "@inactiveFilter": {}, + "runningTimerFilter": "läuft", + "@runningTimerFilter": {}, + "pausedTimerFilter": "pausiert", + "@pausedTimerFilter": {}, + "sortGroup": "Sortieren", + "@sortGroup": {}, + "stoppedTimerFilter": "Gestoppt", + "@stoppedTimerFilter": {}, + "defaultLabel": "Standard", + "@defaultLabel": {}, + "filterActions": "Filter Aktionen", + "@filterActions": {}, + "clearFiltersAction": "Alle Filter löschen", + "@clearFiltersAction": {}, + "durationDesc": "Längste", + "@durationDesc": {}, + "nameAsc": "Bezeichnung A-Z", + "@nameAsc": {}, + "nameDesc": "Bezeichnung Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Die frühen Stunden zuerst", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Die späten Stunden zuerst", + "@timeOfDayDesc": {}, + "cancelSkipAllFilteredAlarmsAction": "Abbrechen der Überspringung aller gefilterten Alarme", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Alle gefilterten Elemente löschen", + "@deleteAllFilteredAction": {}, + "alarmDescriptionWeekly": "Jeden {days}", + "@alarmDescriptionWeekly": {}, + "defaultSettingGroup": "Standardeinstellungen", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Standardwerte für neue Alarme festlegen", + "@alarmsDefaultSettingGroupDescription": {}, + "notificationsSettingGroup": "Benachrichtigungen", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Anstehende Alarmbenachrichtigungen anzeigen", + "@showUpcomingAlarmNotificationSetting": {}, + "timerDefaultSettingGroupDescription": "Standardwerte für neue Timer festlegen", + "@timerDefaultSettingGroupDescription": {}, + "upcomingLeadTimeSetting": "Zeit vor dem Alarm", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Wenn Schlummernd, Benachrichtigungen anzeigen", + "@showSnoozeNotificationSetting": {}, + "filtersSettingGroup": "Filter", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Filter anzeigen", + "@showFiltersSetting": {}, + "showSortSetting": "Sortierung anzeigen", + "@showSortSetting": {}, + "showNotificationSetting": "Benachrichtigungen anzeigen", + "@showNotificationSetting": {}, + "stopwatchShowMillisecondsSetting": "Millisekunden anzeigen", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Vergleich Lap Bars", + "@comparisonLapBarsSettingGroup": {}, + "packageNameLabel": "Name des Pakets", + "@packageNameLabel": {}, + "licenseLabel": "Lizenz", + "@licenseLabel": {}, + "emailLabel": "E-mail", + "@emailLabel": {}, + "viewOnGithubLabel": "Auf Github anzeigen", + "@viewOnGithubLabel": {}, + "dismissActionSetting": "Aktionstyp, wenn der Alarm verpasst wird", + "@dismissActionSetting": {}, + "dismissActionSlide": "Schieben", + "@dismissActionSlide": {}, + "presetsSetting": "Voreinstellungen", + "@presetsSetting": {}, + "newPresetPlaceholder": "Neue Voreinstellung", + "@newPresetPlaceholder": {}, + "dismissActionButtons": "Buttons", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Bereich Buttons", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Zeit Format", + "@stopwatchTimeFormatSettingGroup": {}, + "showPreviousLapSetting": "Vorherige Runde anzeigen", + "@showPreviousLapSetting": {}, + "sameTime": "Gleiche Zeit", + "@sameTime": {}, + "searchCityPlaceholder": "Nach einer Stadt suchen", + "@searchCityPlaceholder": {}, + "relativeTime": "{hours}h {relative, select, ahead{voraus} behind{hinter} other{Sonstiges}}", + "@relativeTime": {}, + "tuesdayShort": "Die", + "@tuesdayShort": {}, + "wednesdayShort": "Mit", + "@wednesdayShort": {}, + "thursdayShort": "Don", + "@thursdayShort": {}, + "fridayShort": "Fre", + "@fridayShort": {}, + "saturdayFull": "Samstag", + "@saturdayFull": {}, + "sundayFull": "Sonntag", + "@sundayFull": {}, + "mondayShort": "Mon", + "@mondayShort": {}, + "donorsDescription": "Unsere großzügigen Patreons", + "@donorsDescription": {}, + "contributorsDescription": "Menschen, die diese App möglich machen", + "@contributorsDescription": {}, + "thursdayFull": "Donnerstag", + "@thursdayFull": {}, + "fridayFull": "Freitag", + "@fridayFull": {}, + "saturdayShort": "Sam", + "@saturdayShort": {}, + "donateDescription": "Spenden, um die Entwicklung der App zu unterstützen", + "@donateDescription": {}, + "sundayShort": "Son", + "@sundayShort": {}, + "longDateFormatSetting": "Langes Datumsformat", + "@longDateFormatSetting": {}, + "batteryOptimizationSettingDescription": "Deaktivieren Sie die Batterieoptimierung für diese Anwendung, um zu verhindern, dass Alarme verzögert werden.", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Sperrbildschirm Benachrichtigungen für Alarme und Timer zulassen", + "@allowNotificationSettingDescription": {}, + "pickerDial": "Kreise", + "@pickerDial": {}, + "swipActionCardAction": "Kartenaktionen", + "@swipActionCardAction": {}, + "swipeActionSwitchTabsDescription": "Zwischen Registerkarten wischen", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Melodien", + "@melodiesSetting": {}, + "tagsSetting": "Stichworte", + "@tagsSetting": {}, + "vendorSetting": "Anbieter Einstellungen", + "@vendorSetting": {}, + "vendorSettingDescription": "Manuelle Deaktivierung herstellerspezifischer Optimierungen", + "@vendorSettingDescription": {}, + "colorSchemeBackgroundSettingGroup": "Hintergrund", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Akzent", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Fehler", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Karte", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Schatten", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsShadowSetting": "Aktzent als Schatten nutzen", + "@colorSchemeUseAccentAsShadowSetting": {}, + "colorSchemeUseAccentAsOutlineSetting": "Akzent als Außenlinie nutzen", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSetting": "Farbe", + "@colorSetting": {}, + "textColorSetting": "Text", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Farbschema", + "@colorSchemeNamePlaceholder": {}, + "styleThemeNamePlaceholder": "Stil-Thema", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Schatten", + "@styleThemeShadowSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Umriss", + "@colorSchemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Breite", + "@styleThemeOutlineWidthSetting": {}, + "showIstantAlarmButtonSetting": "Sofortalarm Button anzeigen", + "@showIstantAlarmButtonSetting": {}, + "styleThemeOutlineSettingGroup": "Umriss", + "@styleThemeOutlineSettingGroup": {}, + "showIstantTimerButtonSetting": "„Sofortigen Timer\" Button anzeigen", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Protokolle", + "@logsSettingGroup": {}, + "maxLogsSetting": "Maximale Protokolle", + "@maxLogsSetting": {}, + "alarmLogSetting": "Alarm Protokolle", + "@alarmLogSetting": {}, + "resetButton": "Zurücketzen", + "@resetButton": {}, + "alarmWeekdaysSetting": "Wochen Tage", + "@alarmWeekdaysSetting": {}, + "exportSettingsSetting": "Exportieren", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Einstellungen in eine lokale Datei exportieren", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Importieren", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importieren von Einstellungen aus einer lokalen Datei", + "@importSettingsSettingDescription": {}, + "showFastestLapSetting": "Schnellste Runde anzeigen", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Durchschnittliche Runde anzeigen", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Langsamste Runde anzeigen", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Linkshändermodus", + "@leftHandedSetting": {}, + "versionLabel": "Version", + "@versionLabel": {}, + "autoStartSettingDescription": "Bei einigen Geräten muss der automatische Start aktiviert sein, damit die Alarme klingeln, wenn die App geschlossen ist", + "@autoStartSettingDescription": {}, + "allowNotificationSetting": "Manuelles Zulassen aller Benachrichtigungen", + "@allowNotificationSetting": {}, + "autoStartSetting": "Auto Start", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Berechtigungen", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Batterie Optimierung ignorieren", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Benachrichtigung Erlaubnis", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Benachrichtigungen Erlaubnis bereits erteilt", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "gnore Batterieoptimierung Erlaubnis bereits erteilt", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "animationSettingGroup": "Animationen", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Animations Geschwindigkeit", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Extra Animationen", + "@extraAnimationSetting": {}, + "nameField": "Name", + "@nameField": {}, + "styleThemeBlurSetting": "Unschärfe", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Verbreitung", + "@styleThemeSpreadSetting": {}, + "materialBrightnessLight": "Hell", + "@materialBrightnessLight": {}, + "alarmDatesSetting": "Daten", + "@alarmDatesSetting": {}, + "alarmIntervalWeekly": "Wöchentlich", + "@alarmIntervalWeekly": {}, + "scheduleTypeDate": "Zu bestimmten Daten", + "@scheduleTypeDate": {}, + "mathHardDifficulty": "Schwer (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Sehr schwer (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeNumberChars": "Anzahl der Zeichen", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Zahlen einbeziehen", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Kleinbuchstaben einbeziehen", + "@retypeLowercaseSetting": {}, + "soundAndVibrationSettingGroup": "Ton und Vibration", + "@soundAndVibrationSettingGroup": {}, + "sequenceLengthSetting": "Länge der Sequenz", + "@sequenceLengthSetting": {}, + "audioChannelNotification": "Benachrichtigung", + "@audioChannelNotification": {}, + "sequenceGridSizeSetting": "Rastergröße", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Anzahl der Probleme", + "@numberOfProblemsSetting": {}, + "widgetsSettingGroup": "Widgets", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Digitale Uhr", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Layout", + "@layoutSettingGroup": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Alarm kann nicht deaktiviert werden, wenn er im Schlummerzustand ist", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "yesButton": "Ja", + "@yesButton": {}, + "completedFilter": "Abgeschlossen", + "@completedFilter": {}, + "snoozedFilter": "Schlummernd", + "@snoozedFilter": {}, + "remainingTimeDesc": "Wenig Zeit übrig", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Die meiste Zeit übrig", + "@remainingTimeAsc": {}, + "disabledFilter": "Deaktiviert", + "@disabledFilter": {}, + "searchSettingPlaceholder": "Nach einer Einstellung suchen", + "@searchSettingPlaceholder": {}, + "mondayFull": "Montag", + "@mondayFull": {}, + "tuesdayFull": "Dienstag", + "@tuesdayFull": {}, + "editPresetsTitle": "Voreinstellungen bearbeiten", + "@editPresetsTitle": {}, + "wednesdayLetter": "M", + "@wednesdayLetter": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "alignmentLeft": "Links", + "@alignmentLeft": {}, + "alignmentCenter": "Zentrum", + "@alignmentCenter": {}, + "alignmentRight": "Rechts", + "@alignmentRight": {}, + "defaultPageSetting": "Standard Registerkarte", + "@defaultPageSetting": {}, + "translateLink": "Übersetzen", + "@translateLink": {}, + "textSettingGroup": "Text", + "@textSettingGroup": {}, + "showDateSetting": "Datum anzeigen", + "@showDateSetting": {}, + "settingsTitle": "Einstellungen", + "@settingsTitle": {}, + "donateButton": "Spenden", + "@donateButton": {}, + "addLengthSetting": "Länge hinzufügen", + "@addLengthSetting": {}, + "cityAlreadyInFavorites": "Diese Stadt ist bereits in deinen Favoriten", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Dauer wählen", + "@durationPickerTitle": {} } From a88ba3d6b5ae757f9b3b2221d1285e209239716f Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sat, 25 May 2024 05:11:16 +0000 Subject: [PATCH 025/177] Translated using Weblate (Ukrainian) Currently translated at 94.9% (317 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/uk/ --- lib/l10n/app_uk.arb | 661 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 660 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 0967ef42..2c956097 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1 +1,660 @@ -{} +{ + "generalSettingGroupDescription": "Налаштування загальних налаштувань програми, таких як формат часу", + "@generalSettingGroupDescription": {}, + "languageSetting": "Мова", + "@languageSetting": {}, + "dateFormatSetting": "Формат дати", + "@dateFormatSetting": {}, + "longDateFormatSetting": "Довгий формат дати", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Формат часу", + "@timeFormatSetting": {}, + "timePickerSetting": "Вибір часу", + "@timePickerSetting": {}, + "pickerDial": "Циферблат", + "@pickerDial": {}, + "timeFormat12": "12-ти годинний", + "@timeFormat12": {}, + "pickerInput": "Ввід", + "@pickerInput": {}, + "timeFormat24": "24 годинний", + "@timeFormat24": {}, + "timeFormatDevice": "Налаштування пристрою", + "@timeFormatDevice": {}, + "showSecondsSetting": "Показувати секунди", + "@showSecondsSetting": {}, + "durationPickerSetting": "Вибір тривалості", + "@durationPickerSetting": {}, + "pickerRings": "Кільця", + "@pickerRings": {}, + "swipeActionSetting": "Дія при свайпі", + "@swipeActionSetting": {}, + "swipeActionCardActionDescription": "Проведіть по картці вліво або вправо, щоб виконати дію", + "@swipeActionCardActionDescription": {}, + "clockTitle": "Годинник", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "timerTitle": "Таймер", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Секундомір", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "system": "Система", + "@system": {}, + "swipActionSwitchTabs": "Перемикання вкладок", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Перемикатись між вкладками", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Мелодії", + "@melodiesSetting": {}, + "tagsSetting": "Мітки", + "@tagsSetting": {}, + "pickerSpinner": "Спінер", + "@pickerSpinner": {}, + "vendorSetting": "Налаштування постачальника", + "@vendorSetting": {}, + "vendorSettingDescription": "Вручну вимкнути оптимізацію для конкретного постачальника", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Вручну вимкнути оптимізацію батареї", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Вимкніть оптимізацію батареї для цієї програми, щоб запобігти затримці будильників", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Увімкнути сповіщення на екрані блокування для будильників і таймерів", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "На деяких пристроях потрібно увімкнути автоматичний запуск, щоб будильники дзвонили, коли програму закрито", + "@autoStartSettingDescription": {}, + "allowNotificationSetting": "Вручну дозволити всі сповіщення", + "@allowNotificationSetting": {}, + "autoStartSetting": "Автоматичний запуск", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Дозволи", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Ігнорувати оптимізацію батареї", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Дозвіл на сповіження", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Дозвіл на сповіження успішно надано", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Ігнорувати вже наданий дозвіл на оптимізацію батареї", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "animationSettingGroup": "Анімації", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Швидкість анімації", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Додаткові анімації", + "@extraAnimationSetting": {}, + "appearanceSettingGroup": "Зовнішній вигляд", + "@appearanceSettingGroup": {}, + "appearanceSettingGroupDescription": "Встановлюйте теми, кольори та змінюйте макет", + "@appearanceSettingGroupDescription": {}, + "nameField": "Назва", + "@nameField": {}, + "colorSetting": "Колір", + "@colorSetting": {}, + "textColorSetting": "Текст", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Кольорова схема", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Фон", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Акцент", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Помилка", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Картка", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Завнішня лінія", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Тінь", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "Використовувати акцент як зовнішню лінію", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Використовувати акцент як тінь", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeShapeSettingGroup": "Форма", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Висота", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "Заокруглення кутів", + "@styleThemeRadiusSetting": {}, + "styleThemeBlurSetting": "Розмиття", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Розподіл", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineSettingGroup": "Зовнішня лінія", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Ширина", + "@styleThemeOutlineWidthSetting": {}, + "accessibilitySettingGroup": "Доступність", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "Резервне копіювання", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Параметри розробника", + "@developerOptionsSettingGroup": {}, + "logsSettingGroup": "Журнали", + "@logsSettingGroup": {}, + "maxLogsSetting": "Максимальний журнал", + "@maxLogsSetting": {}, + "alarmLogSetting": "Журнал будильників", + "@alarmLogSetting": {}, + "aboutSettingGroup": "Про", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Відновити стандартні значення", + "@restoreSettingGroup": {}, + "resetButton": "Скинути", + "@resetButton": {}, + "previewLabel": "Попередній перегляд", + "@previewLabel": {}, + "cardLabel": "Картка", + "@cardLabel": {}, + "accentLabel": "Акцент", + "@accentLabel": {}, + "errorLabel": "Помилка", + "@errorLabel": {}, + "reliabilitySettingGroup": "Надійність", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Кольори", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Стиль", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "Використовувати Material You", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "Яскравість", + "@materialBrightnessSetting": {}, + "materialBrightnessDark": "Темна", + "@materialBrightnessDark": {}, + "overrideAccentSetting": "Перевизначити колір акценту", + "@overrideAccentSetting": {}, + "accentColorSetting": "Акцентний колір", + "@accentColorSetting": {}, + "useMaterialStyleSetting": "Використовувати стиль Material", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "Тема стилю", + "@styleThemeSetting": {}, + "systemDarkModeSetting": "Темний режим системи", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "Кольорова схема", + "@colorSchemeSetting": {}, + "clockSettingGroup": "Годинник", + "@clockSettingGroup": {}, + "timerSettingGroup": "Таймер", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Секундомір", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Експортуйте або імпортуйте налаштування локально", + "@backupSettingGroupDescription": {}, + "alarmRangeSetting": "Діапазон дат", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Проміжок", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Щодня", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Видалити після завершення", + "@alarmDeleteAfterFinishingSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Неможливо вимкнути будильник, коли він відкладений", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "selectTime": "Вибрати час", + "@selectTime": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "cancelButton": "Скасувати", + "@cancelButton": {}, + "customizeButton": "Налаштувати", + "@customizeButton": {}, + "saveButton": "Зберегти", + "@saveButton": {}, + "labelField": "Мітка", + "@labelField": {}, + "labelFieldPlaceholder": "Додати мітку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Розклад", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Одноразово", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Щодня", + "@scheduleTypeDaily": {}, + "scheduleTypeOnceDescription": "Дзвонитиме при наступному настанні часу", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Дзвонитиме щодня", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "У визначені дні тижня", + "@scheduleTypeWeek": {}, + "scheduleTypeDate": "У конкретні дати", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "Діапазон дат", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Повторюватиметься у зазначені дати", + "@scheduleTypeDateDescription": {}, + "soundAndVibrationSettingGroup": "Звук і вібрація", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Більше", + "@settingGroupMore": {}, + "melodySetting": "Мелодія", + "@melodySetting": {}, + "vibrationSetting": "Вібрація", + "@vibrationSetting": {}, + "audioChannelSetting": "Аудіо канал", + "@audioChannelSetting": {}, + "audioChannelNotification": "Сповіщення", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Рингтон", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Медіа", + "@audioChannelMedia": {}, + "volumeSetting": "Гучність", + "@volumeSetting": {}, + "risingVolumeSetting": "Зростання гучності", + "@risingVolumeSetting": {}, + "timeToFullVolumeSetting": "Час до повної гучності", + "@timeToFullVolumeSetting": {}, + "snoozeSettingGroup": "Відкласти", + "@snoozeSettingGroup": {}, + "snoozeLengthSetting": "Довжина", + "@snoozeLengthSetting": {}, + "maxSnoozesSetting": "Максимальне відкладення", + "@maxSnoozesSetting": {}, + "whileSnoozedSettingGroup": "Поки відкладений", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "Запобігти відключенню", + "@snoozePreventDisablingSetting": {}, + "snoozePreventDeletionSetting": "Запобігти видаленню", + "@snoozePreventDeletionSetting": {}, + "settings": "Налаштування", + "@settings": {}, + "noItemMessage": "{items} ще не додано", + "@noItemMessage": {}, + "chooseTaskTitle": "Виберіть завдання для додавання", + "@chooseTaskTitle": {}, + "mathTask": "Математичні завдання", + "@mathTask": {}, + "mathMediumDifficulty": "Середні (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Важкі (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Дуже важкі (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Повторно введіть текст", + "@retypeTask": {}, + "sequenceTask": "Послідовність", + "@sequenceTask": {}, + "taskTryButton": "Спробуйте", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "Складність", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Кількість символів", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Включити числа", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Включити малі літери", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Довжина послідовності", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Розмір таблиці", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Кількість завдань", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Ви хочете вийти без збереження?", + "@saveReminderAlert": {}, + "yesButton": "Так", + "@yesButton": {}, + "noButton": "Ні", + "@noButton": {}, + "noAlarmMessage": "Не створено жодного будильника", + "@noAlarmMessage": {}, + "noTimerMessage": "Немає жодного таймера", + "@noTimerMessage": {}, + "noTagsMessage": "Немає створених міток", + "@noTagsMessage": {}, + "noStopwatchMessage": "Немає створених секундомірів", + "@noStopwatchMessage": {}, + "noPresetsMessage": "Не створено набору налаштувань", + "@noPresetsMessage": {}, + "noLogsMessage": "Немає журналів будильників", + "@noLogsMessage": {}, + "deleteButton": "Видалити", + "@deleteButton": {}, + "skipAlarmButton": "Пропустити наступний будильник", + "@skipAlarmButton": {}, + "cancelSkipAlarmButton": "Скасувати пропуск", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Відхилити", + "@dismissAlarmButton": {}, + "scheduleDateFilterGroup": "Дата розкладу", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Дату створено", + "@createdDateFilterGroup": {}, + "todayFilter": "Сьогодні", + "@todayFilter": {}, + "tomorrowFilter": "Завтра", + "@tomorrowFilter": {}, + "stateFilterGroup": "Стан", + "@stateFilterGroup": {}, + "activeFilter": "Активний", + "@activeFilter": {}, + "inactiveFilter": "Неактивний", + "@inactiveFilter": {}, + "snoozedFilter": "Відкладений", + "@snoozedFilter": {}, + "disabledFilter": "Вимкнений", + "@disabledFilter": {}, + "completedFilter": "Виконаний", + "@completedFilter": {}, + "runningTimerFilter": "Виконується", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Призупинено", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Зупинено", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортувати", + "@sortGroup": {}, + "remainingTimeAsc": "Залишилося найбільше часу", + "@remainingTimeAsc": {}, + "durationAsc": "Найкоротший", + "@durationAsc": {}, + "durationDesc": "Найдовший", + "@durationDesc": {}, + "nameAsc": "Мітка А-Я", + "@nameAsc": {}, + "nameDesc": "Мітка Я-А", + "@nameDesc": {}, + "filterActions": "Дії фільтру", + "@filterActions": {}, + "clearFiltersAction": "Очистити всі фільтри", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Увімкнути всі відфільтровані будильники", + "@enableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Пропустити всі відфльтровані будильники", + "@skipAllFilteredAlarmsAction": {}, + "alarmDescriptionFinished": "Немає наєбутніх дат", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Не заплановано", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "Тільки сьогодні", + "@alarmDescriptionToday": {}, + "alarmDescriptionWeekend": "Кожні вихідні", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Попередній", + "@stopwatchPrevious": {}, + "alarmDescriptionWeekday": "Кожен будній день", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionWeekly": "Кожні {days}", + "@alarmDescriptionWeekly": {}, + "stopwatchFastest": "Найшвидший", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "У {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Щоденно} weekly{Щотжня} other{Інше}} з {startDate} да {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "Найповільніший", + "@stopwatchSlowest": {}, + "stopwatchAverage": "Середнє", + "@stopwatchAverage": {}, + "defaultSettingGroup": "Стандартні налаштування", + "@defaultSettingGroup": {}, + "timerDefaultSettingGroupDescription": "Встановити стандартні значення для нових таймерів", + "@timerDefaultSettingGroupDescription": {}, + "showUpcomingAlarmNotificationSetting": "Показувати сповіщення про майбутні будильники", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Найближчий час виконання", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Показувати сповіщення про відкладення", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Показувати сповіщення", + "@showNotificationSetting": {}, + "presetsSetting": "Набори налаштувань", + "@presetsSetting": {}, + "dismissActionSetting": "Відхилити тип дії", + "@dismissActionSetting": {}, + "dismissActionButtons": "Кнопки", + "@dismissActionButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат часу", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Показувати мілісекунди", + "@stopwatchShowMillisecondsSetting": {}, + "showPreviousLapSetting": "Показувати попереднє коло", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Показувати найшвидше коло", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Показувати середнє коло", + "@showAverageLapSetting": {}, + "exportSettingsSettingDescription": "Експортувати налаштування до локального файлу", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Імпорт", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Імпортувати налаштування з локального файлу", + "@importSettingsSettingDescription": {}, + "versionLabel": "Версія", + "@versionLabel": {}, + "packageNameLabel": "Назва пакунка", + "@packageNameLabel": {}, + "licenseLabel": "Ліцензія", + "@licenseLabel": {}, + "emailLabel": "Е-Пошта", + "@emailLabel": {}, + "viewOnGithubLabel": "Дивитись на GitHub", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Відкрити джерело ліцензій", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Учасники", + "@contributorsSetting": {}, + "donorsSetting": "Донори", + "@donorsSetting": {}, + "donateButton": "Спонсорувати", + "@donateButton": {}, + "sameTime": "Той самий час", + "@sameTime": {}, + "searchSettingPlaceholder": "Пошук налаштувань", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "Пошук міста", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "Це місто вже присутнє у ваших обраних", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Вибрати тривалість", + "@durationPickerTitle": {}, + "editButton": "Редагувати", + "@editButton": {}, + "tuesdayShort": "Вт", + "@tuesdayShort": {}, + "wednesdayShort": "Ср", + "@wednesdayShort": {}, + "thursdayShort": "Чт", + "@thursdayShort": {}, + "fridayShort": "Пт", + "@fridayShort": {}, + "saturdayShort": "Сб", + "@saturdayShort": {}, + "sundayShort": "Нд", + "@sundayShort": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "В", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "Н", + "@sundayLetter": {}, + "donateDescription": "Зробіть пожертву, щоб підтримати розробку програми", + "@donateDescription": {}, + "donorsDescription": "Наші щедрі меценати", + "@donorsDescription": {}, + "contributorsDescription": "Люди, завдяки яким ця програма стала можливою", + "@contributorsDescription": {}, + "widgetsSettingGroup": "Віджети", + "@widgetsSettingGroup": {}, + "layoutSettingGroup": "Макет", + "@layoutSettingGroup": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "defaultPageSetting": "Стандартна вкладка", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Показувати ДО/ПО", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Редагувати набори налаштувань", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Перший день тижня", + "@firstDayOfWeekSetting": {}, + "translateLink": "Перекласти", + "@translateLink": {}, + "translateDescription": "Допомогти з перекладом програми", + "@translateDescription": {}, + "separatorSetting": "Розділювач", + "@separatorSetting": {}, + "editTagLabel": "Редагувати мітку", + "@editTagLabel": {}, + "tagNamePlaceholder": "Назва мітки", + "@tagNamePlaceholder": {}, + "alarmTitle": "Будильник", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "generalSettingGroup": "Загальні", + "@generalSettingGroup": {}, + "swipActionCardAction": "Дії з картками", + "@swipActionCardAction": {}, + "styleThemeNamePlaceholder": "Тема стилю", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Тінь", + "@styleThemeShadowSettingGroup": {}, + "showIstantAlarmButtonSetting": "Показувати кнопку екземпляра будильника", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Показувати кнопку екземпляру такмера", + "@showIstantTimerButtonSetting": {}, + "styleThemeOpacitySetting": "Прозорість", + "@styleThemeOpacitySetting": {}, + "displaySettingGroup": "Дисплей", + "@displaySettingGroup": {}, + "materialBrightnessSystem": "Система", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Світла", + "@materialBrightnessLight": {}, + "darkColorSchemeSetting": "Темна кольорова схема", + "@darkColorSchemeSetting": {}, + "alarmWeekdaysSetting": "Дні тиждня", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Дати", + "@alarmDatesSetting": {}, + "alarmIntervalWeekly": "Щотижня", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Видалити після відхилення", + "@alarmDeleteAfterRingingSetting": {}, + "audioChannelAlarm": "Будильник", + "@audioChannelAlarm": {}, + "scheduleTypeWeekDescription": "Повторюватиметься у вказані дні тижня", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeRangeDescription": "Повторюватиметься протягом вказаного діапазону дат", + "@scheduleTypeRangeDescription": {}, + "snoozeEnableSetting": "Увімкнено", + "@snoozeEnableSetting": {}, + "tasksSetting": "Завдання", + "@tasksSetting": {}, + "mathEasyDifficulty": "Лугкі (X + Y)", + "@mathEasyDifficulty": {}, + "noTaskMessage": "Не створено жодного завдання", + "@noTaskMessage": {}, + "duplicateButton": "Дублювати", + "@duplicateButton": {}, + "dateFilterGroup": "Дата", + "@dateFilterGroup": {}, + "allFilter": "Всі", + "@allFilter": {}, + "defaultLabel": "За замовчуванням", + "@defaultLabel": {}, + "remainingTimeDesc": "Залишилося найменше часу", + "@remainingTimeDesc": {}, + "disableAllFilteredAlarmsAction": "Вимкнути всі відфільтровані будильники", + "@disableAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Скасувати пропуск всіх відфльтрованих будильників", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Видалити всі відфільтровані будильники", + "@deleteAllFilteredAction": {}, + "skippingDescriptionSuffix": "(пропускаючи наступне повторення)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Відкладено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionTomorrow": "Тільки завтра", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Кожного дня", + "@alarmDescriptionEveryDay": {}, + "alarmsDefaultSettingGroupDescription": "Встановити стандартні значення для нових будильників", + "@alarmsDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Фільтри", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Показати фільтри", + "@showFiltersSetting": {}, + "showSortSetting": "Показати сортування", + "@showSortSetting": {}, + "notificationsSettingGroup": "Сповіщення", + "@notificationsSettingGroup": {}, + "newPresetPlaceholder": "Новий набір налаштувань", + "@newPresetPlaceholder": {}, + "showSlowestLapSetting": "Показувати найповільніше коло", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Режим для лівші", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Експорт", + "@exportSettingsSetting": {}, + "noLapsMessage": "Ще немає кіл", + "@noLapsMessage": {}, + "elapsedTime": "Витрачений час", + "@elapsedTime": {}, + "mondayFull": "Понеділок", + "@mondayFull": {}, + "tuesdayFull": "Вівторок", + "@tuesdayFull": {}, + "sundayFull": "Неділя", + "@sundayFull": {}, + "wednesdayFull": "Середа", + "@wednesdayFull": {}, + "thursdayFull": "Четвер", + "@thursdayFull": {}, + "fridayFull": "Пʼятниця", + "@fridayFull": {}, + "saturdayFull": "Субота", + "@saturdayFull": {}, + "mondayShort": "Пн", + "@mondayShort": {}, + "digitalClockSettingGroup": "Цифровий годинник", + "@digitalClockSettingGroup": {}, + "showDateSetting": "Показувати дату", + "@showDateSetting": {}, + "settingsTitle": "Налаштування", + "@settingsTitle": {}, + "horizontalAlignmentSetting": "Горизонтальне вирівнювання", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикальне вирівнювання", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Вгорі", + "@alignmentTop": {}, + "alignmentBottom": "Внизу", + "@alignmentBottom": {}, + "alignmentLeft": "Зліва", + "@alignmentLeft": {}, + "alignmentCenter": "Центер", + "@alignmentCenter": {}, + "alignmentRight": "Зправа", + "@alignmentRight": {}, + "fontWeightSetting": "Товщина шрифту", + "@fontWeightSetting": {}, + "dateSettingGroup": "Дата", + "@dateSettingGroup": {}, + "timeSettingGroup": "Час", + "@timeSettingGroup": {}, + "sizeSetting": "Розмір", + "@sizeSetting": {} +} From 870ea4dc0768c46141b6e031b819cea87606b658 Mon Sep 17 00:00:00 2001 From: Unacceptium Date: Sat, 25 May 2024 06:07:11 +0000 Subject: [PATCH 026/177] Translated using Weblate (Hungarian) Currently translated at 5.9% (20 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/hu/ --- lib/l10n/app_hu.arb | 51 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 0967ef42..985744cc 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -1 +1,50 @@ -{} +{ + "clockTitle": "Óra", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "timerTitle": "Időzítő", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Stopperóra", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "generalSettingGroup": "Általános", + "@generalSettingGroup": {}, + "generalSettingGroupDescription": "Alkalmazáson belüli beállítások, mint a dátumformátum", + "@generalSettingGroupDescription": {}, + "languageSetting": "Nyelv", + "@languageSetting": {}, + "longDateFormatSetting": "Hosszú dátumformátum", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Idő Formátum", + "@timeFormatSetting": {}, + "timeFormat12": "12 órás", + "@timeFormat12": {}, + "timeFormat24": "24 órás", + "@timeFormat24": {}, + "timeFormatDevice": "Eszközbeállítások", + "@timeFormatDevice": {}, + "timePickerSetting": "Idő Kiválasztók", + "@timePickerSetting": {}, + "pickerDial": "Tárcsázós", + "@pickerDial": {}, + "pickerInput": "Beírás", + "@pickerInput": {}, + "swipeActionSetting": "csúsztatás", + "@swipeActionSetting": {}, + "system": "Rendszer", + "@system": {}, + "dateFormatSetting": "Dátumformátum", + "@dateFormatSetting": {}, + "durationPickerSetting": "Időtartam-választó", + "@durationPickerSetting": {}, + "pickerRings": "Gyűrűk", + "@pickerRings": {}, + "showSecondsSetting": "Másodpercek mutatása", + "@showSecondsSetting": {}, + "pickerSpinner": "Pörgetős", + "@pickerSpinner": {} +} From f604d6098331acb0159a06ae505cbfabe71c64b5 Mon Sep 17 00:00:00 2001 From: Sergio Marques Date: Sat, 25 May 2024 23:33:00 +0000 Subject: [PATCH 027/177] Translated using Weblate (Portuguese) Currently translated at 50.0% (1 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/pt/ --- fastlane/metadata/android/pt/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/pt/short_description.txt 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. From 9e27df47bc2b600bb5db5107d98c026cc4a7dbde Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sat, 25 May 2024 04:38:29 +0000 Subject: [PATCH 028/177] Translated using Weblate (Ukrainian) Currently translated at 50.0% (1 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/uk/ --- .../metadata/android/uk/full_description.txt | 35 +++++++++++++++++++ .../metadata/android/uk/short_description.txt | 1 + 2 files changed, 36 insertions(+) create mode 100644 fastlane/metadata/android/uk/full_description.txt create mode 100644 fastlane/metadata/android/uk/short_description.txt diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt new file mode 100644 index 00000000..1d05b893 --- /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 @@ +Сучасний і потужний годинник, будильник, таймер і секундомір. From 2e588af6a4af8d099696a31894fb7bea364c7b53 Mon Sep 17 00:00:00 2001 From: Benjamin Renzi Date: Sun, 26 May 2024 20:00:07 +0000 Subject: [PATCH 029/177] Translated using Weblate (French) Currently translated at 99.7% (333 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fr/ --- lib/l10n/app_fr.arb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 95a9cff3..3ecb32f6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -656,5 +656,21 @@ "showMeridiemSetting": "Afficher Am/Pm", "@showMeridiemSetting": {}, "fontWeightSetting": "Hauteur de la police", - "@fontWeightSetting": {} + "@fontWeightSetting": {}, + "translateLink": "Traduire", + "@translateLink": {}, + "translateDescription": "Aider à traduire l'application", + "@translateDescription": {}, + "editPresetsTitle": "Modifier les préréglages", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Premier jour de la semaine", + "@firstDayOfWeekSetting": {}, + "editTagLabel": "Modifier l'étiquette", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nom de l'étiquette", + "@tagNamePlaceholder": {}, + "longDateFormatSetting": "Format de date longue", + "@longDateFormatSetting": {}, + "noTagsMessage": "Aucun tag créé", + "@noTagsMessage": {} } From b6fba7f27864795edede25b0e0a049c6798dbf53 Mon Sep 17 00:00:00 2001 From: Sergio Marques Date: Sat, 25 May 2024 23:45:41 +0000 Subject: [PATCH 030/177] Translated using Weblate (Portuguese) Currently translated at 97.9% (327 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pt/ --- lib/l10n/app_pt.arb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fe324b06..7215dd3b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -337,7 +337,7 @@ "@timePickerModeButton": {}, "scheduleTypeDate": "Em datas específicas", "@scheduleTypeDate": {}, - "soundAndVibrationSettingGroup": "Sim e vibração", + "soundAndVibrationSettingGroup": "Som e vibração", "@soundAndVibrationSettingGroup": {}, "timeToFullVolumeSetting": "Duração para volume total", "@timeToFullVolumeSetting": {}, @@ -650,5 +650,15 @@ "ignoreBatteryOptimizationAlreadyGranted": "Permissão para ignorar otimização de bateria já concedida", "@ignoreBatteryOptimizationAlreadyGranted": {}, "showSnoozeNotificationSetting": "Mostrar notificação para snooze", - "@showSnoozeNotificationSetting": {} + "@showSnoozeNotificationSetting": {}, + "noTagsMessage": "Não existem etiquetas", + "@noTagsMessage": {}, + "separatorSetting": "Separador", + "@separatorSetting": {}, + "editTagLabel": "Editar etiqueta", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nome da etiqueta", + "@tagNamePlaceholder": {}, + "longDateFormatSetting": "Formato longo de data", + "@longDateFormatSetting": {} } From c37228a71b7418b36d964c47c69fde5fc9c27a93 Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sun, 26 May 2024 04:55:56 +0000 Subject: [PATCH 031/177] Translated using Weblate (Ukrainian) Currently translated at 99.1% (331 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/uk/ --- lib/l10n/app_uk.arb | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 2c956097..bf2a64ab 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -185,7 +185,7 @@ "@timerSettingGroup": {}, "stopwatchSettingGroup": "Секундомір", "@stopwatchSettingGroup": {}, - "backupSettingGroupDescription": "Експортуйте або імпортуйте налаштування локально", + "backupSettingGroupDescription": "Експортуйте або імпортуйте локальні налаштування", "@backupSettingGroupDescription": {}, "alarmRangeSetting": "Діапазон дат", "@alarmRangeSetting": {}, @@ -413,7 +413,7 @@ "@showNotificationSetting": {}, "presetsSetting": "Набори налаштувань", "@presetsSetting": {}, - "dismissActionSetting": "Відхилити тип дії", + "dismissActionSetting": "Тип дії відхилення", "@dismissActionSetting": {}, "dismissActionButtons": "Кнопки", "@dismissActionButtons": {}, @@ -656,5 +656,23 @@ "timeSettingGroup": "Час", "@timeSettingGroup": {}, "sizeSetting": "Розмір", - "@sizeSetting": {} + "@sizeSetting": {}, + "timeOfDayAsc": "Спочатку ранні години", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Спочатку пізні години", + "@timeOfDayDesc": {}, + "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ і 1 інша дата} other{ і {count} інші дати}}", + "@alarmDescriptionDates": {}, + "dismissActionSlide": "Ковзання", + "@dismissActionSlide": {}, + "dismissActionAreaButtons": "Об'ємні кнопки", + "@dismissActionAreaButtons": {}, + "comparisonLapBarsSettingGroup": "Смуги порівняння кіл", + "@comparisonLapBarsSettingGroup": {}, + "addLengthSetting": "Додати тривалість", + "@addLengthSetting": {}, + "relativeTime": "{hours} г. {relative, select, ahead{попереду} behind{позаду} other{Other}}", + "@relativeTime": {}, + "alignmentJustify": "Вирівняти по ширині", + "@alignmentJustify": {} } From 4b0f77838fa50dabbd2bdbdf57aec976611f56c4 Mon Sep 17 00:00:00 2001 From: Balanda Nazarii Date: Sun, 26 May 2024 15:25:55 +0000 Subject: [PATCH 032/177] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/uk/ --- fastlane/metadata/android/uk/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 1d05b893..a54ea3cd 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -24,7 +24,7 @@

Секундомір

    -
  • Історія кола з часом кола та минулим часом
  • +
  • "Історія кіл з часами кіл та загальним часом
  • Порівняння кіл
Зовнішній вигляд From e724b982a34a1ba20db5f0ab741992ac15f7fbd9 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 28 May 2024 13:04:06 +0500 Subject: [PATCH 033/177] Change permissions --- android/app/src/main/AndroidManifest.xml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8f9718c..b4a9535b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,8 +5,6 @@ android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - - @@ -14,12 +12,8 @@ android:name="android.permission.VIBRATE" /> - - - - + + Date: Tue, 28 May 2024 15:08:51 +0000 Subject: [PATCH 034/177] Translated using Weblate (Russian) Currently translated at 24.8% (83 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b08427d2..e3810e1b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -184,5 +184,13 @@ "notificationPermissionSetting": "Разрешения уведомлений", "@notificationPermissionSetting": {}, "pickerInput": "Интерфейс выбора времени, состоящий из полей ввода.", - "@pickerInput": {} + "@pickerInput": {}, + "longDateFormatSetting": "Длинный формат даты", + "@longDateFormatSetting": {}, + "pickerRings": "Кольца", + "@pickerRings": {}, + "pickerSpinner": "Спиннер", + "@pickerSpinner": {}, + "durationPickerSetting": "Выбор стиля длительности", + "@durationPickerSetting": {} } From dbdf07660149a69590553f866912fc0ac27f24fd Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Thu, 30 May 2024 07:17:36 +0000 Subject: [PATCH 035/177] Translated using Weblate (Russian) Currently translated at 97.6% (326 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 482 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 478 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index e3810e1b..95f6ef7b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -61,7 +61,7 @@ "@saveButton": {}, "vibrationSetting": "Вибрация", "@vibrationSetting": {}, - "backupSettingGroup": "Настройки", + "backupSettingGroup": "Резервное копирование", "@backupSettingGroup": {}, "systemDarkModeSetting": "Тёмная тема системы", "@systemDarkModeSetting": {}, @@ -83,11 +83,11 @@ "@showSecondsSetting": {}, "swipeActionCardActionDescription": "Смахните карточку влево или вправо, чтобы выполнить действие", "@swipeActionCardActionDescription": {}, - "batteryOptimizationSetting": "Отключить оптимизацию батареи", + "batteryOptimizationSetting": "Отключить оптимизацию батареи вручную", "@batteryOptimizationSetting": {}, "batteryOptimizationSettingDescription": "Отключить оптимизацию батареи для этого приложения, чтобы избежать откладывания будильника", "@batteryOptimizationSettingDescription": {}, - "allowNotificationSetting": "Разрешить уведомления", + "allowNotificationSetting": "Разрешение всех уведомлений вручную", "@allowNotificationSetting": {}, "autoStartSetting": "Автозапуск", "@autoStartSetting": {}, @@ -192,5 +192,479 @@ "pickerSpinner": "Спиннер", "@pickerSpinner": {}, "durationPickerSetting": "Выбор стиля длительности", - "@durationPickerSetting": {} + "@durationPickerSetting": {}, + "showIstantAlarmButtonSetting": "Показать кнопку быстрого будильника", + "@showIstantAlarmButtonSetting": {}, + "maxLogsSetting": "Макс. журналов", + "@maxLogsSetting": {}, + "snoozeLengthSetting": "Длина", + "@snoozeLengthSetting": {}, + "alarmDeleteAfterRingingSetting": "Удалить после закрытия", + "@alarmDeleteAfterRingingSetting": {}, + "alarmWeekdaysSetting": "Дни недели", + "@alarmWeekdaysSetting": {}, + "dismissActionAreaButtons": "Кнопки области", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат времени", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Показывать миллисекунды", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Сравнение кругов", + "@comparisonLapBarsSettingGroup": {}, + "saveReminderAlert": "Вы хотите выйти без сохранения?", + "@saveReminderAlert": {}, + "yesButton": "Да", + "@yesButton": {}, + "noStopwatchMessage": "Секундомеры не созданы", + "@noStopwatchMessage": {}, + "noTagsMessage": "Теги не созданы", + "@noTagsMessage": {}, + "alarmsDefaultSettingGroupDescription": "Установить значения по умолчанию для новых будильников", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Установить значения по умолчанию для новых таймеров", + "@timerDefaultSettingGroupDescription": {}, + "alarmDescriptionWeekday": "Каждый рабочий день", + "@alarmDescriptionWeekday": {}, + "stopwatchAverage": "Средний", + "@stopwatchAverage": {}, + "alarmDescriptionWeekly": "Каждые {days}", + "@alarmDescriptionWeekly": {}, + "defaultSettingGroup": "Настройки по умолчанию", + "@defaultSettingGroup": {}, + "deleteAllFilteredAction": "Удалить все отфильтрованные элементы", + "@deleteAllFilteredAction": {}, + "editButton": "Редактировать", + "@editButton": {}, + "noLapsMessage": "Еще нет кругов", + "@noLapsMessage": {}, + "fridayFull": "Пятница", + "@fridayFull": {}, + "sundayFull": "Воскресенье", + "@sundayFull": {}, + "mondayShort": "Пн", + "@mondayShort": {}, + "alignmentCenter": "Центр", + "@alignmentCenter": {}, + "alignmentRight": "Право", + "@alignmentRight": {}, + "alignmentJustify": "По ширине", + "@alignmentJustify": {}, + "translateLink": "Перевести", + "@translateLink": {}, + "translateDescription": "Помогите перевести приложение", + "@translateDescription": {}, + "aboutSettingGroup": "О приложении", + "@aboutSettingGroup": {}, + "reliabilitySettingGroup": "Надежность", + "@reliabilitySettingGroup": {}, + "styleSettingGroup": "Стиль", + "@styleSettingGroup": {}, + "backupSettingGroupDescription": "Локальный экспорт или импорт настроек", + "@backupSettingGroupDescription": {}, + "selectTime": "Выберите время", + "@selectTime": {}, + "labelField": "Метка", + "@labelField": {}, + "labelFieldPlaceholder": "Добавить метку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Расписание", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Один раз", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Ежедневно", + "@scheduleTypeDaily": {}, + "scheduleTypeDailyDescription": "Будет звонить каждый день", + "@scheduleTypeDailyDescription": {}, + "swipeActionSwitchTabsDescription": "Перелистывание вкладок", + "@swipeActionSwitchTabsDescription": {}, + "styleThemeNamePlaceholder": "Тема стиля", + "@styleThemeNamePlaceholder": {}, + "styleThemeElevationSetting": "Высота", + "@styleThemeElevationSetting": {}, + "styleThemeSpreadSetting": "Размах", + "@styleThemeSpreadSetting": {}, + "showIstantTimerButtonSetting": "Показать кнопку быстрого таймера", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Журналы", + "@logsSettingGroup": {}, + "previewLabel": "Предварительный просмотр", + "@previewLabel": {}, + "alarmDatesSetting": "Даты", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Диапазон дат", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Интервал", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Ежедневно", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Еженедельно", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterFinishingSetting": "Удалить после завершения", + "@alarmDeleteAfterFinishingSetting": {}, + "scheduleTypeOnceDescription": "Зазвонит при следующем наступлении времени", + "@scheduleTypeOnceDescription": {}, + "mathTaskDifficultySetting": "Сложность", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Количество символов", + "@retypeNumberChars": {}, + "noTimerMessage": "Таймеры не созданы", + "@noTimerMessage": {}, + "filterActions": "Фильтр действий", + "@filterActions": {}, + "clearFiltersAction": "Очистить все фильтры", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Включить все отфильтрованные будильники", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Отключить все отфильтрованные будильники", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Пропустить все отфильтрованные будильники", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Отменить пропуск всех отфильтрованных будильников", + "@cancelSkipAllFilteredAlarmsAction": {}, + "skippingDescriptionSuffix": "(пропуск следующего события)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Отложено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionToday": "Только сегодня", + "@alarmDescriptionToday": {}, + "alarmDescriptionEveryDay": "Каждый день", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Каждые выходные", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Предыдущий", + "@stopwatchPrevious": {}, + "dismissActionSetting": "Тип действия при отклонении", + "@dismissActionSetting": {}, + "dismissActionButtons": "Кнопки", + "@dismissActionButtons": {}, + "showPreviousLapSetting": "Показать предыдущий круг", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Показать лучший круг", + "@showFastestLapSetting": {}, + "sameTime": "Такое же время", + "@sameTime": {}, + "searchCityPlaceholder": "Поиск города", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Выбрать длительность", + "@durationPickerTitle": {}, + "elapsedTime": "Прошедшее время", + "@elapsedTime": {}, + "mondayFull": "Понедельник", + "@mondayFull": {}, + "tuesdayFull": "Вторник", + "@tuesdayFull": {}, + "wednesdayFull": "Среда", + "@wednesdayFull": {}, + "sundayShort": "Вс", + "@sundayShort": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "В", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "В", + "@sundayLetter": {}, + "donateDescription": "Пожертвуйте на поддержку разработки приложения", + "@donateDescription": {}, + "donorsDescription": "Наши щедрые патроны", + "@donorsDescription": {}, + "contributorsDescription": "Люди, благодаря которым это приложение стало возможным", + "@contributorsDescription": {}, + "horizontalAlignmentSetting": "Горизонтальное выравнивание", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикальное выравнивание", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Верх", + "@alignmentTop": {}, + "alignmentBottom": "Низ", + "@alignmentBottom": {}, + "alignmentLeft": "Лево", + "@alignmentLeft": {}, + "editPresetsTitle": "Редактировать предустановки", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Первый день недели", + "@firstDayOfWeekSetting": {}, + "separatorSetting": "Разделитель", + "@separatorSetting": {}, + "editTagLabel": "Редактировать тег", + "@editTagLabel": {}, + "tagNamePlaceholder": "Название тега", + "@tagNamePlaceholder": {}, + "scheduleTypeDate": "В определенные даты", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "Диапазон дат", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Будет повторяться в указанные даты", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Будет повторяться в указанном диапазоне дат", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Звук и вибрация", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Ещё", + "@settingGroupMore": {}, + "melodySetting": "Мелодия", + "@melodySetting": {}, + "audioChannelSetting": "Аудиоканал", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Будильник", + "@audioChannelAlarm": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "scheduleTypeWeek": "В определенные дни недели", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Будет повторяться в указанные дни недели", + "@scheduleTypeWeekDescription": {}, + "volumeSetting": "Громкость", + "@volumeSetting": {}, + "risingVolumeSetting": "Нарастающая громкость", + "@risingVolumeSetting": {}, + "snoozeSettingGroup": "Отсрочка", + "@snoozeSettingGroup": {}, + "snoozeEnableSetting": "Включено", + "@snoozeEnableSetting": {}, + "whileSnoozedSettingGroup": "Во время отсрочки", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "Запретить отключение", + "@snoozePreventDisablingSetting": {}, + "settings": "Настройки", + "@settings": {}, + "tasksSetting": "Задачи", + "@tasksSetting": {}, + "noItemMessage": "Пока нет добавленных {items}", + "@noItemMessage": {}, + "retypeIncludeNumSetting": "Включить числа", + "@retypeIncludeNumSetting": {}, + "noButton": "Нет", + "@noButton": {}, + "noAlarmMessage": "Будильники не созданы", + "@noAlarmMessage": {}, + "retypeLowercaseSetting": "Включить строчные буквы", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Длина последовательности", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Размер сетки", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Количество задач", + "@numberOfProblemsSetting": {}, + "cancelSkipAlarmButton": "Отменить пропуск", + "@cancelSkipAlarmButton": {}, + "durationDesc": "Самый длинный", + "@durationDesc": {}, + "timeOfDayAsc": "Ранние часы первыми", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Поздние часы первыми", + "@timeOfDayDesc": {}, + "dismissAlarmButton": "", + "@dismissAlarmButton": {}, + "durationAsc": "Самый короткий", + "@durationAsc": {}, + "stopwatchSlowest": "Самый медленный", + "@stopwatchSlowest": {}, + "newPresetPlaceholder": "Новая предустановка", + "@newPresetPlaceholder": {}, + "filtersSettingGroup": "Фильтры", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Показать фильтры", + "@showFiltersSetting": {}, + "showSortSetting": "Показать сортировку", + "@showSortSetting": {}, + "notificationsSettingGroup": "Уведомления", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Показать уведомления о предстоящих будильниках", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Время до наступления события", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Показать уведомления об отложенных будильниках", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Показать уведомление", + "@showNotificationSetting": {}, + "saturdayFull": "Суббота", + "@saturdayFull": {}, + "tuesdayShort": "Вт", + "@tuesdayShort": {}, + "wednesdayShort": "Ср", + "@wednesdayShort": {}, + "thursdayShort": "Чт", + "@thursdayShort": {}, + "fridayShort": "Пт", + "@fridayShort": {}, + "saturdayShort": "Сб", + "@saturdayShort": {}, + "thursdayFull": "Четверг", + "@thursdayFull": {}, + "openSourceLicensesSetting": "Лицензии открытого исходного кода", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Участники", + "@contributorsSetting": {}, + "donorsSetting": "Спонсоры", + "@donorsSetting": {}, + "donateButton": "Пожертвовать", + "@donateButton": {}, + "searchSettingPlaceholder": "Поиск настройки", + "@searchSettingPlaceholder": {}, + "cityAlreadyInFavorites": "Этот город уже в избранном", + "@cityAlreadyInFavorites": {}, + "alarmLogSetting": "Журналы будильника", + "@alarmLogSetting": {}, + "duplicateButton": "Дублировать", + "@duplicateButton": {}, + "skipAlarmButton": "Пропустить следующий будильник", + "@skipAlarmButton": {}, + "allFilter": "Все", + "@allFilter": {}, + "todayFilter": "Сегодня", + "@todayFilter": {}, + "tomorrowFilter": "Завтра", + "@tomorrowFilter": {}, + "snoozedFilter": "Отложенный", + "@snoozedFilter": {}, + "alarmDescriptionFinished": "Нет будущих дат", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Не запланировано", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionTomorrow": "Только завтра", + "@alarmDescriptionTomorrow": {}, + "stopwatchFastest": "Самый быстрый", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "По {days}", + "@alarmDescriptionDays": {}, + "widgetsSettingGroup": "Виджеты", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Цифровые часы", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Макет", + "@layoutSettingGroup": {}, + "nameAsc": "Метка от А до Я", + "@nameAsc": {}, + "nameDesc": "Метка от Я до А", + "@nameDesc": {}, + "presetsSetting": "Предустановки", + "@presetsSetting": {}, + "showAverageLapSetting": "Показать средний круг", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Показать медленный круг", + "@showSlowestLapSetting": {}, + "exportSettingsSetting": "Экспорт", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Экспорт настроек в локальный файл", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Импорт", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Импорт настроек из локального файла", + "@importSettingsSettingDescription": {}, + "versionLabel": "Версия", + "@versionLabel": {}, + "leftHandedSetting": "Режим для левой руки", + "@leftHandedSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Невозможно отключить будильник, пока он отложен", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "sequenceTask": "Последовательность", + "@sequenceTask": {}, + "taskTryButton": "Попробовать", + "@taskTryButton": {}, + "maxSnoozesSetting": "Макс. отсрочек", + "@maxSnoozesSetting": {}, + "snoozePreventDeletionSetting": "Запретить удаление", + "@snoozePreventDeletionSetting": {}, + "audioChannelRingtone": "Рингтон", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Медиа", + "@audioChannelMedia": {}, + "chooseTaskTitle": "Выберите задачу для добавления", + "@chooseTaskTitle": {}, + "mathTask": "Математические задачи", + "@mathTask": {}, + "mathEasyDifficulty": "Легко (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Средне (X × Y)", + "@mathMediumDifficulty": {}, + "timeToFullVolumeSetting": "Время до полной громкости", + "@timeToFullVolumeSetting": {}, + "mathHardDifficulty": "Сложно (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Очень сложно (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Повторить ввод текста", + "@retypeTask": {}, + "audioChannelNotification": "Уведомление", + "@audioChannelNotification": {}, + "dateFilterGroup": "Дата", + "@dateFilterGroup": {}, + "scheduleDateFilterGroup": "Запланированная дата", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Дата создания", + "@createdDateFilterGroup": {}, + "stateFilterGroup": "Состояние", + "@stateFilterGroup": {}, + "activeFilter": "Активный", + "@activeFilter": {}, + "inactiveFilter": "Неактивный", + "@inactiveFilter": {}, + "disabledFilter": "Отключенный", + "@disabledFilter": {}, + "completedFilter": "Завершенный", + "@completedFilter": {}, + "runningTimerFilter": "Работающий", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Приостановленный", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Остановленный", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортировать", + "@sortGroup": {}, + "defaultLabel": "По умолчанию", + "@defaultLabel": {}, + "remainingTimeDesc": "Минимальное оставшееся время", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Максимальное оставшееся время", + "@remainingTimeAsc": {}, + "noTaskMessage": "Задачи не созданы", + "@noTaskMessage": {}, + "noPresetsMessage": "Пресеты не созданы", + "@noPresetsMessage": {}, + "noLogsMessage": "Нет журналов будильника", + "@noLogsMessage": {}, + "deleteButton": "Удалить", + "@deleteButton": {}, + "emailLabel": "Email", + "@emailLabel": {}, + "packageNameLabel": "Название пакета", + "@packageNameLabel": {}, + "licenseLabel": "Лицензия", + "@licenseLabel": {}, + "viewOnGithubLabel": "Посмотреть на GitHub", + "@viewOnGithubLabel": {}, + "addLengthSetting": "Добавить длительность", + "@addLengthSetting": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "showDateSetting": "Показать дату", + "@showDateSetting": {}, + "settingsTitle": "Настройки", + "@settingsTitle": {}, + "fontWeightSetting": "Начертание шрифта", + "@fontWeightSetting": {}, + "dateSettingGroup": "Дата", + "@dateSettingGroup": {}, + "timeSettingGroup": "Время", + "@timeSettingGroup": {}, + "sizeSetting": "Размер", + "@sizeSetting": {}, + "defaultPageSetting": "Вкладка по умолчанию", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Показывать AM/PM", + "@showMeridiemSetting": {} } From 35009bead17d8c9c46f9015bb3d7264695cdd0a0 Mon Sep 17 00:00:00 2001 From: Unreal Vision Date: Thu, 30 May 2024 12:11:15 +0000 Subject: [PATCH 036/177] Translated using Weblate (French) Currently translated at 100.0% (334 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fr/ --- lib/l10n/app_fr.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 3ecb32f6..6ad478df 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -672,5 +672,7 @@ "longDateFormatSetting": "Format de date longue", "@longDateFormatSetting": {}, "noTagsMessage": "Aucun tag créé", - "@noTagsMessage": {} + "@noTagsMessage": {}, + "separatorSetting": "Séparateur", + "@separatorSetting": {} } From a9016ab4541e3f750e87baf34eb91e16f6f52080 Mon Sep 17 00:00:00 2001 From: UngoIiant Date: Wed, 29 May 2024 22:21:37 +0000 Subject: [PATCH 037/177] Translated using Weblate (German) Currently translated at 100.0% (334 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/de/ --- lib/l10n/app_de.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7df8e5a5..19e1b64f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -483,7 +483,7 @@ "@sundayShort": {}, "longDateFormatSetting": "Langes Datumsformat", "@longDateFormatSetting": {}, - "batteryOptimizationSettingDescription": "Deaktivieren Sie die Batterieoptimierung für diese Anwendung, um zu verhindern, dass Alarme verzögert werden.", + "batteryOptimizationSettingDescription": "Deaktivieren Sie die Batterieoptimierung für diese Anwendung, um zu verhindern, dass Alarme verzögert werden", "@batteryOptimizationSettingDescription": {}, "allowNotificationSettingDescription": "Sperrbildschirm Benachrichtigungen für Alarme und Timer zulassen", "@allowNotificationSettingDescription": {}, From 3ec2fa8a83ad0df01109483c5838122c543efd3b Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Thu, 30 May 2024 09:50:10 +0000 Subject: [PATCH 038/177] Translated using Weblate (Russian) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/ru/ --- .../android/ru-RU/full_description.txt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt 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
  • +
  • Широкие возможности настройки цветовых тем
  • +
  • Широкие возможности настройки тем стиля
  • +
From d35b1af1f24d876d16d3e785d753f596c9cbb65c Mon Sep 17 00:00:00 2001 From: Miki utn Date: Fri, 31 May 2024 23:18:17 +0000 Subject: [PATCH 039/177] Translated using Weblate (Italian) Currently translated at 51.1% (171 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 284 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 7b24eefb..1576c4d8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -66,5 +66,287 @@ "generalSettingGroupDescription": "Impostare la applicazione, es. formato dell'ora", "@generalSettingGroupDescription": {}, "pickerDial": "Quadrante", - "@pickerDial": {} + "@pickerDial": {}, + "notificationPermissionAlreadyGranted": "Il permesso di notifiche è già stato concesso", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Ignora il permesso d'ottimizzazione della batteria già concesso", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "backupSettingGroup": "Copia di sicurezza", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Opzioni dello sviluppatore", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "Mostrare il tasto dell'allarme istantanea", + "@showIstantAlarmButtonSetting": {}, + "alarmWeekdaysSetting": "Giorni della settimana", + "@alarmWeekdaysSetting": {}, + "systemDarkModeSetting": "Modalità scura del sistema", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "Schema di colori", + "@colorSchemeSetting": {}, + "alarmDatesSetting": "Date", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Intervallo di date", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "intervallo", + "@alarmIntervalSetting": {}, + "snoozePreventDisablingSetting": "Evitare disattivazione", + "@snoozePreventDisablingSetting": {}, + "sequenceTask": "Sequenza", + "@sequenceTask": {}, + "taskTryButton": "Provare", + "@taskTryButton": {}, + "accessibilitySettingGroup": "Accessibilità", + "@accessibilitySettingGroup": {}, + "settingGroupMore": "Altro", + "@settingGroupMore": {}, + "longDateFormatSetting": "Formato per date lunghe", + "@longDateFormatSetting": {}, + "vendorSetting": "Impostazioni del fornitore", + "@vendorSetting": {}, + "vendorSettingDescription": "Disattivare manualmente le ottimizzazioni specifiche del fornitore", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Disattivare le ottimizzazioni della batteria manualmente", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Disattivare le ottimizzazioni della batteria di quest'applicazione per evitare ritardi nelle allarme", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Permettere notificazioni nella schermata di blocco per allarmi e temporizzatori", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Alcuni dispositivi richiedono l'attivazione dell'inizio automatico perché le allarme possano suonare quando l'applicazione sia chiusa", + "@autoStartSettingDescription": {}, + "permissionsSettingGroup": "Permessi", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Ignorare l'ottimizzazione della batteria", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Permessi di notifiche", + "@notificationPermissionSetting": {}, + "colorSchemeShadowSettingGroup": "Ombra", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "Utilizzi i colori accentuati per il contorno", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Utilizzare il colore accentuato come ombra", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeNamePlaceholder": "Stilo del tema", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Ombra", + "@styleThemeShadowSettingGroup": {}, + "styleThemeShapeSettingGroup": "Forma", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Elevazione", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "Curvatura degl'angoli", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Opacità", + "@styleThemeOpacitySetting": {}, + "styleThemeOutlineSettingGroup": "Contorno", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeBlurSetting": "Sfumare", + "@styleThemeBlurSetting": {}, + "styleThemeSpreadSetting": "Stendere", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineWidthSetting": "Largo", + "@styleThemeOutlineWidthSetting": {}, + "showIstantTimerButtonSetting": "Mostrare il tasto del temporizzatore istantaneo", + "@showIstantTimerButtonSetting": {}, + "maxLogsSetting": "Massimo di registri", + "@maxLogsSetting": {}, + "alarmLogSetting": "Registri di allarmi", + "@alarmLogSetting": {}, + "aboutSettingGroup": "A proposito di", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Ripristinare i valori predeterminati", + "@restoreSettingGroup": {}, + "overrideAccentSetting": "Annullare l'accentuazione del colore", + "@overrideAccentSetting": {}, + "accentColorSetting": "Accentuare il colore", + "@accentColorSetting": {}, + "useMaterialStyleSetting": "Utilizzare Material Style", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "Stile del tema", + "@styleThemeSetting": {}, + "darkColorSchemeSetting": "Schema di colori scuro", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "Orologio", + "@clockSettingGroup": {}, + "timerSettingGroup": "Timer", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Cronometro", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Esportare o importare le tue impostazioni localmente", + "@backupSettingGroupDescription": {}, + "alarmIntervalWeekly": "Settimanalmente", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Eliminare dopo scartare", + "@alarmDeleteAfterRingingSetting": {}, + "alarmDeleteAfterFinishingSetting": "Eliminare una volta finito", + "@alarmDeleteAfterFinishingSetting": {}, + "selectTime": "Selezionare l'ora", + "@selectTime": {}, + "scheduleTypeRange": "Intervallo di date", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Si ripeterà in date specificate", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Si ripeterà durante un intervallo specifico di date", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Suono e vibrazione", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "suono", + "@soundSettingGroup": {}, + "melodySetting": "Melodia", + "@melodySetting": {}, + "vibrationSetting": "Vibrazione", + "@vibrationSetting": {}, + "audioChannelSetting": "Canale audio", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Allarme", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Notifica", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Suoneria", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Media", + "@audioChannelMedia": {}, + "volumeSetting": "Volume", + "@volumeSetting": {}, + "risingVolumeSetting": "Alzare il volume", + "@risingVolumeSetting": {}, + "snoozeEnableSetting": "Abilitare", + "@snoozeEnableSetting": {}, + "snoozeLengthSetting": "Durata", + "@snoozeLengthSetting": {}, + "snoozeSettingGroup": "Posponi", + "@snoozeSettingGroup": {}, + "settings": "Impostazioni", + "@settings": {}, + "tasksSetting": "Compiti", + "@tasksSetting": {}, + "noItemMessage": "Non ci sono ancora {items} aggiunti", + "@noItemMessage": {}, + "chooseTaskTitle": "Scegli compiti da aggiungere", + "@chooseTaskTitle": {}, + "mathTask": "Problemi matematici", + "@mathTask": {}, + "mathEasyDifficulty": "Facile (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Medio (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Difficile (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Molto difficile (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Ripetere il testo", + "@retypeTask": {}, + "mathTaskDifficultySetting": "Difficoltà", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Numero di caratteri", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Includere numeri", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Includere minuscole", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Largo della sequenza", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Dimensione della griglia", + "@sequenceGridSizeSetting": {}, + "numberOfProblemsSetting": "Numero di problemi", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Vuoi uscire senza salvare?", + "@saveReminderAlert": {}, + "yesButton": "Si", + "@yesButton": {}, + "noButton": "No", + "@noButton": {}, + "noAlarmMessage": "Nessun allarme creata", + "@noAlarmMessage": {}, + "noTimerMessage": "Nessun timer creato", + "@noTimerMessage": {}, + "noTagsMessage": "Nessun etichetta creata", + "@noTagsMessage": {}, + "scheduleTypeField": "Tipo", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "Una volta", + "@scheduleTypeOnce": {}, + "scheduleTypeOnceDescription": "Suonerà alla prossima ora fissata", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Suonerà ogni giorno", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "In giorni specifici della settimana", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Si ripeterà in giorni specifici della settimana", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "In date specifiche", + "@scheduleTypeDate": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Non è possibile disattivare l'allarme essendo stata sospesa", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "timePickerModeButton": "Modo", + "@timePickerModeButton": {}, + "cancelButton": "Cancellare", + "@cancelButton": {}, + "customizeButton": "Customizzare", + "@customizeButton": {}, + "saveButton": "Salvare", + "@saveButton": {}, + "labelField": "Etichettare", + "@labelField": {}, + "labelFieldPlaceholder": "Aggiungi un'etichetta", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Pianificare", + "@alarmScheduleSettingGroup": {}, + "maxSnoozesSetting": "Massimo di ripetizioni", + "@maxSnoozesSetting": {}, + "whileSnoozedSettingGroup": "Mentre posponi", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDeletionSetting": "Evitare eliminazione", + "@snoozePreventDeletionSetting": {}, + "durationPickerSetting": "Selettore di durata", + "@durationPickerSetting": {}, + "swipeActionCardActionDescription": "Trascini il dito a sinistra o a destra nella carta per realizzare azioni", + "@swipeActionCardActionDescription": {}, + "swipActionSwitchTabs": "Cambiare finestre", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Passare da una finestra all'altra", + "@swipeActionSwitchTabsDescription": {}, + "tagsSetting": "Etichette", + "@tagsSetting": {}, + "autoStartSetting": "Inizio automatico", + "@autoStartSetting": {}, + "allowNotificationSetting": "Permettere manualmente tutte le notifiche", + "@allowNotificationSetting": {}, + "colorSchemeAccentSettingGroup": "Accentuare", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeCardSettingGroup": "Carta", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Contorno", + "@colorSchemeOutlineSettingGroup": {}, + "logsSettingGroup": "Registri", + "@logsSettingGroup": {}, + "resetButton": "Ripristinare", + "@resetButton": {}, + "cardLabel": "Carta", + "@cardLabel": {}, + "materialBrightnessSetting": "Luminosità", + "@materialBrightnessSetting": {}, + "accentLabel": "Accentuare", + "@accentLabel": {}, + "errorLabel": "Errore", + "@errorLabel": {}, + "previewLabel": "Anteprima", + "@previewLabel": {}, + "displaySettingGroup": "Schermo", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "Fiducia", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Colori", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Stile", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "Utilizzare Material You", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSystem": "Sistema", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Chiaro", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Scuro", + "@materialBrightnessDark": {}, + "scheduleTypeDaily": "Ogni giorno", + "@scheduleTypeDaily": {} } From 40c45523cce3cbda03898575cbdfeb88c3b24127 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 2 Jun 2024 11:59:02 +0500 Subject: [PATCH 040/177] Fix time picker time format --- lib/common/widgets/time_picker.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/common/widgets/time_picker.dart b/lib/common/widgets/time_picker.dart index c86a596a..8f651041 100644 --- a/lib/common/widgets/time_picker.dart +++ b/lib/common/widgets/time_picker.dart @@ -2820,8 +2820,15 @@ class _TimePickerDialogState extends State TextTheme textTheme = theme.textTheme; ColorScheme colorScheme = theme.colorScheme; - bool use24hMode = MediaQuery.of(context).alwaysUse24HourFormat || - appSettings.getSetting("Time Format").value == TimeFormat.h24; + TimeFormat timeFormat = appSettings.getSetting("Time Format").value; + + bool use24hMode = false; + if (timeFormat == TimeFormat.device) { + use24hMode = MediaQuery.of(context).alwaysUse24HourFormat; + } else { + use24hMode = + appSettings.getSetting("Time Format").value == TimeFormat.h24; + } switch (type) { case TimePickerType.spinner: From a7997bb350d9b630dac6e31c5a1f9168a6cdbbe2 Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Tue, 4 Jun 2024 20:47:24 +0000 Subject: [PATCH 041/177] Translated using Weblate (Russian) Currently translated at 97.9% (327 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 95f6ef7b..725f278a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -105,7 +105,7 @@ "@swipeActionSetting": {}, "swipActionCardAction": "Действия карточки", "@swipActionCardAction": {}, - "swipActionSwitchTabs": "Сменить вкладу", + "swipActionSwitchTabs": "Сменить вкладку", "@swipActionSwitchTabs": {}, "melodiesSetting": "Мелодии", "@melodiesSetting": {}, From 7d74e7b6fdab9d04dffcebe39fe5e2ca83556a4e Mon Sep 17 00:00:00 2001 From: TCr3 Date: Tue, 4 Jun 2024 12:42:22 +0000 Subject: [PATCH 042/177] Translated using Weblate (Turkish) Currently translated at 100.0% (334 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/tr/ --- lib/l10n/app_tr.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 712c6fb1..4f1d269a 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -225,7 +225,7 @@ "@showSecondsSetting": {}, "pickerInput": "Girdi", "@pickerInput": {}, - "timePickerSetting": "", + "timePickerSetting": "Zaman Seçici", "@timePickerSetting": {}, "ignoreBatteryOptimizationSetting": "Batarya İyileştirmesini Yoksay", "@ignoreBatteryOptimizationSetting": {}, From d56254531bc9d35dd278ae684e125d2b0e72cbb9 Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Tue, 4 Jun 2024 21:41:34 +0000 Subject: [PATCH 043/177] Translated using Weblate (Russian) Currently translated at 100.0% (334 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 725f278a..091b51d4 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -33,7 +33,7 @@ "@useMaterialYouColorSetting": {}, "overrideAccentSetting": "Изменить цвет акцента", "@overrideAccentSetting": {}, - "materialBrightnessSetting": "Тёмная тема", + "materialBrightnessSetting": "Регулировка яркости", "@materialBrightnessSetting": {}, "styleThemeSetting": "Тема", "@styleThemeSetting": {}, @@ -101,7 +101,7 @@ "@nameField": {}, "colorSetting": "Цвет", "@colorSetting": {}, - "swipeActionSetting": "Смахивание", + "swipeActionSetting": "Действие свайпа", "@swipeActionSetting": {}, "swipActionCardAction": "Действия карточки", "@swipActionCardAction": {}, @@ -183,7 +183,7 @@ "@permissionsSettingGroup": {}, "notificationPermissionSetting": "Разрешения уведомлений", "@notificationPermissionSetting": {}, - "pickerInput": "Интерфейс выбора времени, состоящий из полей ввода.", + "pickerInput": "Ввод", "@pickerInput": {}, "longDateFormatSetting": "Длинный формат даты", "@longDateFormatSetting": {}, @@ -399,7 +399,7 @@ "@editTagLabel": {}, "tagNamePlaceholder": "Название тега", "@tagNamePlaceholder": {}, - "scheduleTypeDate": "В определенные даты", + "scheduleTypeDate": "В определённые даты", "@scheduleTypeDate": {}, "scheduleTypeRange": "Диапазон дат", "@scheduleTypeRange": {}, @@ -421,7 +421,7 @@ "@audioChannelAlarm": {}, "timePickerModeButton": "Режим", "@timePickerModeButton": {}, - "scheduleTypeWeek": "В определенные дни недели", + "scheduleTypeWeek": "В определённые дни недели", "@scheduleTypeWeek": {}, "scheduleTypeWeekDescription": "Будет повторяться в указанные дни недели", "@scheduleTypeWeekDescription": {}, @@ -465,7 +465,7 @@ "@timeOfDayAsc": {}, "timeOfDayDesc": "Поздние часы первыми", "@timeOfDayDesc": {}, - "dismissAlarmButton": "", + "dismissAlarmButton": "Отклонить", "@dismissAlarmButton": {}, "durationAsc": "Самый короткий", "@durationAsc": {}, @@ -666,5 +666,13 @@ "defaultPageSetting": "Вкладка по умолчанию", "@defaultPageSetting": {}, "showMeridiemSetting": "Показывать AM/PM", - "@showMeridiemSetting": {} + "@showMeridiemSetting": {}, + "dismissActionSlide": "Скольжение", + "@dismissActionSlide": {}, + "relativeTime": "{hours}ч {relative, select, ahead{впереди} behind{позади} other{Другое}}", + "@relativeTime": {}, + "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ и ещё 1 дата} other{ и ещё {count} дат(ы)}}", + "@alarmDescriptionDates": {}, + "alarmDescriptionRange": "{interval, select, daily{Ежедневно} weekly{Еженедельно} other{Другое}} с {startDate} по {endDate}", + "@alarmDescriptionRange": {} } From a21f99008f9c170cbfd5623a423f689faa2b358c Mon Sep 17 00:00:00 2001 From: er2de2 Date: Wed, 5 Jun 2024 13:20:24 +0000 Subject: [PATCH 044/177] Translated using Weblate (Polish) Currently translated at 22.1% (74 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 951757b9..27112080 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -152,5 +152,7 @@ "animationSpeedSetting": "Szybkość animacji", "@animationSpeedSetting": {}, "extraAnimationSetting": "Dodatkowe animacje", - "@extraAnimationSetting": {} + "@extraAnimationSetting": {}, + "generalSettingGroupDescription": "Ustawienia aplikacji, takie jak format czasu", + "@generalSettingGroupDescription": {} } From dee2217690bff4c226d0278775f7fafc74cae264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8B=89=E5=A4=AB=E8=A5=BF=E5=A5=A7=C2=B7=E7=BE=85?= =?UTF-8?q?=E9=BD=8A=E5=A5=A7=E9=A6=AC=E7=88=BE?= Date: Thu, 6 Jun 2024 20:45:30 +0000 Subject: [PATCH 045/177] Translated using Weblate (Polish) Currently translated at 23.0% (77 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 27112080..8e9ecd4c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -154,5 +154,11 @@ "extraAnimationSetting": "Dodatkowe animacje", "@extraAnimationSetting": {}, "generalSettingGroupDescription": "Ustawienia aplikacji, takie jak format czasu", - "@generalSettingGroupDescription": {} + "@generalSettingGroupDescription": {}, + "longDateFormatSetting": "Długi format daty", + "@longDateFormatSetting": {}, + "timePickerSetting": "Selektor czasu", + "@timePickerSetting": {}, + "pickerDial": "Zegar", + "@pickerDial": {} } From bb9daa3fb0b969cb17482b2a80dbe5baa79b9a2a Mon Sep 17 00:00:00 2001 From: Miki utn Date: Sat, 8 Jun 2024 01:26:53 +0000 Subject: [PATCH 046/177] Translated using Weblate (Italian) Currently translated at 79.9% (267 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 194 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 1576c4d8..86446b47 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -348,5 +348,197 @@ "materialBrightnessDark": "Scuro", "@materialBrightnessDark": {}, "scheduleTypeDaily": "Ogni giorno", - "@scheduleTypeDaily": {} + "@scheduleTypeDaily": {}, + "showSortSetting": "Mostra ordinati", + "@showSortSetting": {}, + "timerDefaultSettingGroupDescription": "Stabilire valori determinati per nuovi temporizzatori", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Filtri", + "@filtersSettingGroup": {}, + "notificationsSettingGroup": "Notifiche", + "@notificationsSettingGroup": {}, + "dateFilterGroup": "Data", + "@dateFilterGroup": {}, + "activeFilter": "Attiva", + "@activeFilter": {}, + "createdDateFilterGroup": "Data di creazione", + "@createdDateFilterGroup": {}, + "stateFilterGroup": "Stato", + "@stateFilterGroup": {}, + "inactiveFilter": "Inattivo", + "@inactiveFilter": {}, + "completedFilter": "Completo", + "@completedFilter": {}, + "pausedTimerFilter": "Pausato", + "@pausedTimerFilter": {}, + "snoozedFilter": "Posposto", + "@snoozedFilter": {}, + "skipAllFilteredAlarmsAction": "Omettere tutte le allarmi filtrate", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Cancellare l'omissione delle allarmi filtrate", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Eliminare tutti gli elementi filtrati", + "@deleteAllFilteredAction": {}, + "showFiltersSetting": "Mostra Filtri", + "@showFiltersSetting": {}, + "alarmIntervalDaily": "Ogni giorno", + "@alarmIntervalDaily": {}, + "timeToFullVolumeSetting": "Tempo per il volume massimo", + "@timeToFullVolumeSetting": {}, + "noStopwatchMessage": "Non sono stati creati cronometri", + "@noStopwatchMessage": {}, + "noTaskMessage": "Non ci sono compiti creati", + "@noTaskMessage": {}, + "noPresetsMessage": "Configurazioni predefinite non ancora create", + "@noPresetsMessage": {}, + "noLogsMessage": "Nessun registro d'allarme", + "@noLogsMessage": {}, + "deleteButton": "Eliminare", + "@deleteButton": {}, + "duplicateButton": "Duplicare", + "@duplicateButton": {}, + "skipAlarmButton": "Ignora il prossimo allarme", + "@skipAlarmButton": {}, + "dismissAlarmButton": "Scartare", + "@dismissAlarmButton": {}, + "allFilter": "Tutte", + "@allFilter": {}, + "scheduleDateFilterGroup": "Data programmata", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Tipo", + "@logTypeFilterGroup": {}, + "todayFilter": "Oggi", + "@todayFilter": {}, + "tomorrowFilter": "Domani", + "@tomorrowFilter": {}, + "disabledFilter": "Disabilitato", + "@disabledFilter": {}, + "runningTimerFilter": "In Esecuzione", + "@runningTimerFilter": {}, + "nameDesc": "Nome Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Le prime ore per prima", + "@timeOfDayAsc": {}, + "disableAllFilteredAlarmsAction": "Disabilitare tutte le allarmi filtrate", + "@disableAllFilteredAlarmsAction": {}, + "alarmDescriptionWeekly": "Ogni {days}", + "@alarmDescriptionWeekly": {}, + "alarmDescriptionRange": "{interval, select, daily{Diariamente} weekly{Settimanalmente} other{Altri}} da {startDate} a {endDate}", + "@alarmDescriptionRange": {}, + "alarmDescriptionSnooze": "Posposto fino a{date}", + "@alarmDescriptionSnooze": {}, + "stopwatchSlowest": "Più lenta", + "@stopwatchSlowest": {}, + "alarmDescriptionDates": "Il {date}{count, plural, =0{} =1{e 1 data in più} other{ e {count} altre date}}", + "@alarmDescriptionDates": {}, + "defaultSettingGroup": "Impostazioni per difetto", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Stabilire valori predeterminati per nuove allarme", + "@alarmsDefaultSettingGroupDescription": {}, + "showUpcomingAlarmNotificationSetting": "Visualizza prossime notifiche di allarme", + "@showUpcomingAlarmNotificationSetting": {}, + "horizontalAlignmentSetting": "Allineamento orizzontale", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Allineamento verticale", + "@verticalAlignmentSetting": {}, + "alignmentTop": "Sopra", + "@alignmentTop": {}, + "alignmentBottom": "Sotto", + "@alignmentBottom": {}, + "alignmentLeft": "Sinistra", + "@alignmentLeft": {}, + "alignmentCenter": "Centro", + "@alignmentCenter": {}, + "alignmentRight": "Destra", + "@alignmentRight": {}, + "alignmentJustify": "Giustificare", + "@alignmentJustify": {}, + "fontWeightSetting": "Spessore lettere", + "@fontWeightSetting": {}, + "dateSettingGroup": "Data", + "@dateSettingGroup": {}, + "timeSettingGroup": "Tempo", + "@timeSettingGroup": {}, + "sizeSetting": "Grandezza", + "@sizeSetting": {}, + "defaultPageSetting": "Etichetta predeterminata", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Modo AM/PM", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Editare impostazioni predeterminate", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Primo giorno della settimana", + "@firstDayOfWeekSetting": {}, + "translateLink": "Traduci", + "@translateLink": {}, + "translateDescription": "Aiuta a tradurre l'applicazione", + "@translateDescription": {}, + "separatorSetting": "Separatore", + "@separatorSetting": {}, + "editTagLabel": "Editare etichetta", + "@editTagLabel": {}, + "tagNamePlaceholder": "Nome dell'etichetta", + "@tagNamePlaceholder": {}, + "stoppedTimerFilter": "Fermato", + "@stoppedTimerFilter": {}, + "durationAsc": "Più corto", + "@durationAsc": {}, + "durationDesc": "Più lungo", + "@durationDesc": {}, + "nameAsc": "Nome A-Z", + "@nameAsc": {}, + "sortGroup": "Ordinare", + "@sortGroup": {}, + "defaultLabel": "Per difetto", + "@defaultLabel": {}, + "remainingTimeDesc": "Poco tempo rimasto", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Più tempo rimasto", + "@remainingTimeAsc": {}, + "filterActions": "Filtrare azioni", + "@filterActions": {}, + "clearFiltersAction": "Pulire tutti i filtri", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Abilitare tutte le allarmi filtrate", + "@enableAllFilteredAlarmsAction": {}, + "pickerSpinner": "Spinner", + "@pickerSpinner": {}, + "pickerRings": "Suonerie", + "@pickerRings": {}, + "swipActionCardAction": "Azioni con carte", + "@swipActionCardAction": {}, + "skippingDescriptionSuffix": "(omettere il prossimo allarme)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionFinished": "Non ci sono prossime date", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Non programmato", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "Solo per oggi", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "Solo domani", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Tutti i giorni", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Ogni fine settimana", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "Precedente", + "@stopwatchPrevious": {}, + "alarmDescriptionWeekday": "Ogni giorno lavorativo della settimana", + "@alarmDescriptionWeekday": {}, + "stopwatchFastest": "Più rapida", + "@stopwatchFastest": {}, + "alarmDescriptionDays": "Tutti i{days}", + "@alarmDescriptionDays": {}, + "digitalClockSettingGroup": "Orologio digitale", + "@digitalClockSettingGroup": {}, + "textSettingGroup": "Testo", + "@textSettingGroup": {}, + "settingsTitle": "Impostazioni", + "@settingsTitle": {}, + "showDateSetting": "Mostrare Data", + "@showDateSetting": {}, + "timeOfDayDesc": "Le ultime ore per prima", + "@timeOfDayDesc": {}, + "stopwatchAverage": "Promedio", + "@stopwatchAverage": {} } From d7baa655370bf32ab74748414b2d66d055ba5c44 Mon Sep 17 00:00:00 2001 From: Miki utn Date: Sat, 8 Jun 2024 02:42:59 +0000 Subject: [PATCH 047/177] Translated using Weblate (Italian) Currently translated at 50.0% (1 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/it/ --- fastlane/metadata/android/it-IT/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/it-IT/short_description.txt 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. From d12c663ca8810e3747edad754f07acd5e61e1859 Mon Sep 17 00:00:00 2001 From: Krzysztof Kolumb Date: Fri, 14 Jun 2024 01:03:45 +0000 Subject: [PATCH 048/177] Translated using Weblate (Polish) Currently translated at 26.0% (87 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8e9ecd4c..82791306 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -115,7 +115,7 @@ "@ignoreBatteryOptimizationSetting": {}, "notificationPermissionSetting": "Uprawnienia powiadomień", "@notificationPermissionSetting": {}, - "notificationPermissionAlreadyGranted": "Uprawnienia na powiadomenia już zostały nadane", + "notificationPermissionAlreadyGranted": "Uprawnienia na powiadomienia zostały przyznane", "@notificationPermissionAlreadyGranted": {}, "ignoreBatteryOptimizationAlreadyGranted": "Optymalizacje baterii zostały już wyłączone", "@ignoreBatteryOptimizationAlreadyGranted": {}, @@ -137,13 +137,13 @@ "@timeFormatSetting": {}, "timeFormat12": "12 godzinny", "@timeFormat12": {}, - "batteryOptimizationSetting": "Wyłącz optymalizacje baterii", + "batteryOptimizationSetting": "Wyłączenie optymalizacje baterii", "@batteryOptimizationSetting": {}, - "batteryOptimizationSettingDescription": "Wyłącz optymalizacje tej aplikacji aby alarmy nie były opóźnione", + "batteryOptimizationSettingDescription": "Wyłącz optymalizacje baterii dla tej aplikacji, aby alarmy nie były opóźnione", "@batteryOptimizationSettingDescription": {}, - "allowNotificationSettingDescription": "Zezwół na alarmy na blokadzie ekrany", + "allowNotificationSettingDescription": "Zezwalaj na powiadomienia na ekranie blokady dla alarmów i minutników", "@allowNotificationSettingDescription": {}, - "allowNotificationSetting": "Zezwól na powiadomienia manualnie", + "allowNotificationSetting": "Zezwól na wszystkie powiadomienia", "@allowNotificationSetting": {}, "autoStartSetting": "Automatyczne uruchamianie", "@autoStartSetting": {}, @@ -160,5 +160,33 @@ "timePickerSetting": "Selektor czasu", "@timePickerSetting": {}, "pickerDial": "Zegar", - "@pickerDial": {} + "@pickerDial": {}, + "pickerInput": "wprowadź", + "@pickerInput": {}, + "pickerRings": "dzwonki", + "@pickerRings": {}, + "noTaskMessage": "nie utworzono zadań", + "@noTaskMessage": {}, + "vendorSetting": "ustawienia producenta", + "@vendorSetting": {}, + "melodiesSetting": "melodie", + "@melodiesSetting": {}, + "autoStartSettingDescription": "niektóre urządzenia wymagają uaktywnienia funkcji auto startu dla alarmów podczas gdy aplikacja jest zamknięta", + "@autoStartSettingDescription": {}, + "colorSchemeBackgroundSettingGroup": "tło", + "@colorSchemeBackgroundSettingGroup": {}, + "nameField": "nazwa", + "@nameField": {}, + "colorSetting": "Kolor", + "@colorSetting": {}, + "appearanceSettingGroupDescription": "ustawienia schematu kolorów i", + "@appearanceSettingGroupDescription": {}, + "textColorSetting": "tekst", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "schemat kolorów", + "@colorSchemeNamePlaceholder": {}, + "tagsSetting": "znaczniki", + "@tagsSetting": {}, + "vendorSettingDescription": "ręczne wyłączenie ustawień producenta", + "@vendorSettingDescription": {} } From 50c20fa31f6dd8d8cee473f2f91c3ca3d625cf94 Mon Sep 17 00:00:00 2001 From: Krzysztof Kolumb Date: Fri, 14 Jun 2024 02:11:37 +0000 Subject: [PATCH 049/177] Translated using Weblate (Polish) Currently translated at 50.0% (1 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/pl/ --- fastlane/metadata/android/pl-PL/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/pl-PL/short_description.txt 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..8061c454 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -0,0 +1 @@ +nowoczesny i potężny zegar, budzik, minutnik i stoper. From 7f41020fc7a74292884614ed34b726a44f536349 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 15 Jun 2024 21:53:30 +0500 Subject: [PATCH 050/177] Add current lap to stopwatch --- lib/common/utils/json_serialize.dart | 13 ++-- lib/common/utils/list_storage.dart | 7 +- lib/common/widgets/list/custom_list_view.dart | 5 ++ lib/stopwatch/screens/stopwatch_screen.dart | 33 +++++----- lib/stopwatch/types/lap.dart | 52 +++++++++++---- lib/stopwatch/types/stopwatch.dart | 60 +++++++++++------ lib/stopwatch/widgets/lap_card.dart | 66 ++++++++++++++++--- lib/stopwatch/widgets/stopwatch_ticker.dart | 8 --- pubspec.yaml | 2 + 9 files changed, 170 insertions(+), 76 deletions(-) diff --git a/lib/common/utils/json_serialize.dart b/lib/common/utils/json_serialize.dart index 55c8016d..c052c95f 100644 --- a/lib/common/utils/json_serialize.dart +++ b/lib/common/utils/json_serialize.dart @@ -9,6 +9,7 @@ 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/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 +28,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,19 +36,20 @@ 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()}"); rethrow; diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart index 437e38c4..71383cf5 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -61,12 +61,13 @@ Future> loadList(String key) async { Future saveList( String key, List list) async { - await saveTextFile(key, listToString(list)); + await saveTextFile(key, listToString(list)); } 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 { diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 9e4f3fe2..4c92e016 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -39,6 +39,8 @@ class CustomListView extends StatefulWidget { this.sortOptions = const [], this.initialSortIndex = 0, this.onChangeSortIndex, + this.header, + }); final List items; @@ -60,6 +62,7 @@ class CustomListView extends StatefulWidget { final List> customActions; final List> sortOptions; final Function(int index)? onChangeSortIndex; + final Widget? header; @override State createState() => _CustomListViewState(); @@ -462,6 +465,8 @@ class _CustomListViewState reorderDecorationBuilder: widget.isReorderable ? reorderableListDecorator : null, footer: const SizedBox(height: 64 + 80), + // header: widget.header, + // cacheExtent: double.infinity, ), ), diff --git a/lib/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index a4645316..6a477d11 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; @@ -17,8 +18,6 @@ import 'package:clock_app/stopwatch/widgets/stopwatch_ticker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - class StopwatchScreen extends StatefulWidget { const StopwatchScreen({super.key}); @@ -30,9 +29,8 @@ class _StopwatchScreenState extends State { final _listController = ListController(); late Setting _showNotificationSetting; - - late final ClockStopwatch _stopwatch; + late final ClockStopwatch _stopwatch; void update(dynamic value) { setState(() {}); @@ -42,7 +40,7 @@ class _StopwatchScreenState extends State { void initState() { super.initState(); _stopwatch = loadListSync('stopwatches').first; - + _showNotificationSetting = appSettings.getGroup("Stopwatch").getSetting("Show Notification"); @@ -68,7 +66,6 @@ class _StopwatchScreenState extends State { @override void dispose() { - // updateNotificationInterval?.cancel(); // updateNotificationInterval = null; @@ -88,18 +85,23 @@ class _StopwatchScreenState extends State { void _handleAddLap() { if (_stopwatch.currentLapTime.inMilliseconds == 0) return; + _stopwatch.finishLap(_stopwatch.laps.first); + _listController.changeItems((laps) => {}); _listController.addItem(_stopwatch.getLap()); saveList('stopwatches', [_stopwatch]); showProgressNotification(); } void _handleToggleState() { + if (_stopwatch.isStopped) { + _listController.addItem(_stopwatch.getLap()); + } setState(() { _stopwatch.toggleState(); }); + saveList('stopwatches', [_stopwatch]); if (_stopwatch.isRunning) { - // ticker!.start(); showProgressNotification(); } else { stopwatchNotificationInterval?.cancel(); @@ -137,17 +139,20 @@ class _StopwatchScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StopwatchTicker(stopwatch:_stopwatch), - const SizedBox(height: 8), + StopwatchTicker(stopwatch: _stopwatch), + const SizedBox(height: 8), Expanded( child: CustomListView( items: _stopwatch.laps, listController: _listController, - itemBuilder: (lap) => LapCard( - key: ValueKey(lap), - lap: lap, - ), + itemBuilder: (lap) => lap.isActive + ? ActiveLapCard(stopwatch: _stopwatch) + : LapCard( + key: ValueKey(lap), + lap: lap, + ), placeholderText: AppLocalizations.of(context)!.noLapsMessage, + // header: CardContainer(child:), isDeleteEnabled: false, isDuplicateEnabled: false, isReorderable: false, @@ -182,5 +187,3 @@ class _StopwatchScreenState extends State { ); } } - - diff --git a/lib/stopwatch/types/lap.dart b/lib/stopwatch/types/lap.dart index e5f3a7d3..803c96c2 100644 --- a/lib/stopwatch/types/lap.dart +++ b/lib/stopwatch/types/lap.dart @@ -3,27 +3,45 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/timer/types/time_duration.dart'; class Lap extends ListItem { - late int number; - late TimeDuration lapTime; - late TimeDuration elapsedTime; + late int _number; + late TimeDuration _lapTime; + late TimeDuration _elapsedTime; + late bool _isActive; + int get number => _number; + bool get isActive => _isActive; + set isActive(bool value) => _isActive = value; + set lapTime(TimeDuration value) => _lapTime = value; + set elapsedTime(TimeDuration value) => _elapsedTime = value; + TimeDuration get lapTime => _lapTime; + TimeDuration get elapsedTime => _elapsedTime; @override int get id => number; @override bool get isDeletable => false; - Lap({required this.elapsedTime, required this.number, required this.lapTime}); + Lap( + {required int number, + TimeDuration elapsedTime = const TimeDuration(), + TimeDuration lapTime = const TimeDuration(), + bool isActive = false}) + : _lapTime = lapTime, + _number = number, + _elapsedTime = elapsedTime, + _isActive = isActive; Lap.fromJson(Json? json) { if (json == null) { - number = 0; - lapTime = TimeDuration.zero; - elapsedTime = TimeDuration.zero; + _number = 0; + _lapTime = TimeDuration.zero; + _elapsedTime = TimeDuration.zero; + _isActive = false; return; } - number = json['number'] ?? 0; - lapTime = TimeDuration.fromJson(json['lapTime']); - elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _number = json['number'] ?? 0; + _lapTime = TimeDuration.fromJson(json['lapTime']); + _elapsedTime = TimeDuration.fromJson(json['elapsedTime']); + _isActive = json['isActive'] ?? false; } @override @@ -31,17 +49,23 @@ class Lap extends ListItem { 'number': number, 'lapTime': lapTime.toJson(), 'elapsedTime': elapsedTime.toJson(), + 'isActive': _isActive, }; @override copy() { - return Lap(elapsedTime: elapsedTime, number: number, lapTime: lapTime); + return Lap( + elapsedTime: elapsedTime, + number: number, + lapTime: lapTime, + isActive: _isActive); } @override void copyFrom(other) { - number = other.number; - lapTime = TimeDuration.from(other.lapTime); - elapsedTime = TimeDuration.from(other.elapsedTime); + _number = other.number; + _lapTime = TimeDuration.from(other.lapTime); + _elapsedTime = TimeDuration.from(other.elapsedTime); + _isActive = other._isActive; } } diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index 2f1701e2..cc3a2a68 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -1,10 +1,12 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; +// All time units are in milliseconds class ClockStopwatch extends JsonSerializable { int _elapsedMillisecondsOnPause = 0; DateTime _startTime = DateTime(0); @@ -16,28 +18,37 @@ class ClockStopwatch extends JsonSerializable { int get id => _id; List get laps => _laps; + List get finishedLaps => _laps.where((lap) => !lap.isActive).toList(); int get elapsedMilliseconds => _state == TimerState.running ? DateTime.now().difference(_startTime).toTimeDuration().inMilliseconds : _elapsedMillisecondsOnPause; + TimeDuration get elapsedTime => + TimeDuration.fromMilliseconds(elapsedMilliseconds); bool get isRunning => _state == TimerState.running; + bool get isStopped => _state == TimerState.stopped; bool get isStarted => _state == TimerState.running || _state == TimerState.paused; TimerState get state => _state; TimeDuration get currentLapTime => - TimeDuration.fromMilliseconds(elapsedMilliseconds - - (_laps.isNotEmpty ? _laps.first.elapsedTime.inMilliseconds : 0)); - Lap? get previousLap => _laps.isNotEmpty ? _laps.first : null; + TimeDuration.fromMilliseconds(elapsedMilliseconds - lastLapElapsedTime); + int get lastLapElapsedTime { + if (finishedLaps.isEmpty) return 0; + return finishedLaps.first.elapsedTime.inMilliseconds; + } + + Lap? get previousLap => finishedLaps.isNotEmpty ? finishedLaps.first : null; Lap? get fastestLap => _fastestLap; Lap? get slowestLap => _slowestLap; Lap? get averageLap { - if (_laps.isEmpty) return null; - var totalMilliseconds = _laps.fold( + if (finishedLaps.isEmpty) return null; + var totalMilliseconds = finishedLaps.fold( 0, (previousValue, lap) => previousValue + lap.lapTime.inMilliseconds); return Lap( - elapsedTime: - TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + elapsedTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), number: _laps.length + 1, - lapTime: TimeDuration.fromMilliseconds(totalMilliseconds ~/ _laps.length), + lapTime: TimeDuration.fromMilliseconds( + totalMilliseconds ~/ finishedLaps.length), ); } @@ -94,31 +105,37 @@ class ClockStopwatch extends JsonSerializable { } } - void updateFastestAndSlowestLap() { - if(laps.isEmpty) return; - _fastestLap = _laps.reduce((value, element) => + if (finishedLaps.isEmpty) return; + _fastestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds < element.lapTime.inMilliseconds ? value : element); - _slowestLap = _laps.reduce((value, element) => + _slowestLap = finishedLaps.reduce((value, element) => value.lapTime.inMilliseconds > element.lapTime.inMilliseconds ? value : element); } void addLap() { - if (currentLapTime.inMilliseconds == 0) return; + if (_laps.isNotEmpty) { + if (currentLapTime.inMilliseconds == 0) return; + finishLap(_laps.first); + updateFastestAndSlowestLap(); + } _laps.insert(0, getLap()); - updateFastestAndSlowestLap(); } + void finishLap(Lap lap) { + // This needs to be set before elapsedTime and isActive + lap.lapTime = currentLapTime; + lap.elapsedTime = TimeDuration.fromMilliseconds(elapsedMilliseconds); + lap.isActive = false; + } + + // Lap getLap() { - return Lap( - elapsedTime: TimeDuration.fromMilliseconds(elapsedMilliseconds), - number: _laps.length + 1, - lapTime: currentLapTime, - ); + return Lap(number: finishedLaps.length + 1, isActive: true); } @override @@ -128,7 +145,7 @@ class ClockStopwatch extends JsonSerializable { 'elapsedMillisecondsOnPause': _elapsedMillisecondsOnPause, 'startTime': _startTime.toIso8601String(), 'state': _state.toString(), - 'laps': _laps.map((e) => e.toJson()).toList(), + 'laps': listToString(_laps), }; } @@ -145,7 +162,8 @@ class ClockStopwatch extends JsonSerializable { (e) => e.toString() == (json['state'] ?? ''), orElse: () => TimerState.stopped); _id = json['id'] ?? UniqueKey().hashCode; - _laps = ((json['laps'] ?? []) as List).map((e) => Lap.fromJson(e)).toList(); + // _finishedLaps = []; + _laps = listFromString(json['laps'] ?? ''); updateFastestAndSlowestLap(); } } diff --git a/lib/stopwatch/widgets/lap_card.dart b/lib/stopwatch/widgets/lap_card.dart index 5ce040a5..bb279d7d 100644 --- a/lib/stopwatch/widgets/lap_card.dart +++ b/lib/stopwatch/widgets/lap_card.dart @@ -1,23 +1,67 @@ import 'package:clock_app/stopwatch/types/lap.dart'; +import 'package:clock_app/stopwatch/types/stopwatch.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class LapCard extends StatefulWidget { - const LapCard({super.key, required this.lap, this.onInit}); +class LapCard extends StatelessWidget { + const LapCard({super.key, required this.lap}); final Lap lap; - final VoidCallback? onInit; @override - State createState() => _LapCardState(); + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Text('${lap.number}'), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(lap.lapTime.toTimeString(showMilliseconds: true), + style: Theme.of(context).textTheme.displaySmall), + Text( + '${AppLocalizations.of(context)!.elapsedTime}: ${lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + ], + ), + ], + ), + ); + } +} + +class ActiveLapCard extends StatefulWidget { + const ActiveLapCard({ + super.key, + required this.stopwatch, + }); + + final ClockStopwatch stopwatch; + + @override + State createState() => _ActiveLapCardState(); } -class _LapCardState extends State { +class _ActiveLapCardState extends State { + late Ticker ticker; + + void tick(Duration elapsed) { + setState(() {}); + } + @override void initState() { + ticker = Ticker(tick); + ticker.start(); super.initState(); - // widget.onInit?.call(); + } + + @override + void dispose() { + ticker.dispose(); + super.dispose(); } @override @@ -26,15 +70,17 @@ class _LapCardState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Text('${widget.lap.number}'), + Text('${widget.stopwatch.laps.length}'), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.lap.lapTime.toTimeString(showMilliseconds: true), + Text( + widget.stopwatch.currentLapTime + .toTimeString(showMilliseconds: true), style: Theme.of(context).textTheme.displaySmall), Text( - '${AppLocalizations.of(context)!.elapsedTime}: ${widget.lap.elapsedTime.toTimeString(showMilliseconds: true)}'), + '${AppLocalizations.of(context)!.elapsedTime}: ${widget.stopwatch.elapsedTime.toTimeString(showMilliseconds: true)}'), ], ), ], diff --git a/lib/stopwatch/widgets/stopwatch_ticker.dart b/lib/stopwatch/widgets/stopwatch_ticker.dart index 5a3bae65..9bbe4a5f 100644 --- a/lib/stopwatch/widgets/stopwatch_ticker.dart +++ b/lib/stopwatch/widgets/stopwatch_ticker.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - class StopwatchTicker extends StatefulWidget { const StopwatchTicker({super.key, required this.stopwatch}); @@ -31,10 +30,6 @@ class _StopwatchTickerState extends State { } void tick(Duration elapsed) { - // var t = elapsed.inMicroseconds * 1e-6; - // double radius = 100; - // drawState.x = radius * math.sin(t); - // drawState.y = radius * math.cos(t); setState(() {}); } @@ -77,9 +72,6 @@ class _StopwatchTickerState extends State { ticker.stop(); ticker.dispose(); - // updateNotificationInterval?.cancel(); - // updateNotificationInterval = null; - super.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 068c8a30..25068e71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: git: url: https://github.com/AhsanSarwar45/great_list_view ref: master + # great_list_view: + # path: "../great_list_view" get_storage: ^2.1.1 queue: ^3.1.0+2 table_calendar: ^3.0.8 From 1bc561e258fc426ec0dd74af1345f027401644e9 Mon Sep 17 00:00:00 2001 From: Miki utn Date: Sat, 15 Jun 2024 12:05:11 +0000 Subject: [PATCH 051/177] Translated using Weblate (Italian) Currently translated at 88.9% (297 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 62 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 86446b47..e607297d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -540,5 +540,65 @@ "timeOfDayDesc": "Le ultime ore per prima", "@timeOfDayDesc": {}, "stopwatchAverage": "Promedio", - "@stopwatchAverage": {} + "@stopwatchAverage": {}, + "cancelSkipAlarmButton": "Cancella ignorare", + "@cancelSkipAlarmButton": {}, + "upcomingLeadTimeSetting": "Configurare notificazione previa", + "@upcomingLeadTimeSetting": {}, + "showSnoozeNotificationSetting": "Mostra notifiche rinviate", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Visualizza notificazioni", + "@showNotificationSetting": {}, + "presetsSetting": "Configurazioni prestabilite", + "@presetsSetting": {}, + "newPresetPlaceholder": "Nuove configurazioni", + "@newPresetPlaceholder": {}, + "dismissActionSetting": "Scartare tipo di azione", + "@dismissActionSetting": {}, + "dismissActionSlide": "Scorrere", + "@dismissActionSlide": {}, + "dismissActionButtons": "Tasti", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Tasti di area", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Formato dell'ora", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Visualizza millisecondi", + "@stopwatchShowMillisecondsSetting": {}, + "showAverageLapSetting": "Visualizza giro promedio", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Visualizza il giro più lento", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Modo per mancini", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Esportare", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Esportare configurazioni a un file locale", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Importare", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importare configurazioni da un file locale", + "@importSettingsSettingDescription": {}, + "versionLabel": "Versione", + "@versionLabel": {}, + "packageNameLabel": "Nome del pacchetto", + "@packageNameLabel": {}, + "licenseLabel": "licenza", + "@licenseLabel": {}, + "emailLabel": "direzione di posta elettronica", + "@emailLabel": {}, + "viewOnGithubLabel": "Visualizza in GitHub", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Licenza Open Source", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Collaboratori", + "@contributorsSetting": {}, + "addLengthSetting": "Aggiungi durata", + "@addLengthSetting": {}, + "showFastestLapSetting": "Visualizza il giro più veloce", + "@showFastestLapSetting": {}, + "donorsSetting": "Donatori", + "@donorsSetting": {}, + "donateButton": "Donare", + "@donateButton": {} } From 59c115b505344187cbcceaa49bec73794545a6f4 Mon Sep 17 00:00:00 2001 From: Miki utn Date: Fri, 21 Jun 2024 03:03:49 +0000 Subject: [PATCH 052/177] Translated using Weblate (Italian) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/it/ --- .../android/it-IT/full_description.txt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/full_description.txt 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
  • +
From 281375fe8f07d253836b1eb6cf987f9e561c4878 Mon Sep 17 00:00:00 2001 From: Marilena Lugato Date: Fri, 21 Jun 2024 14:25:45 +0000 Subject: [PATCH 053/177] Translated using Weblate (Italian) Currently translated at 99.4% (332 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 72 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e607297d..00cb99a4 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -600,5 +600,75 @@ "donorsSetting": "Donatori", "@donorsSetting": {}, "donateButton": "Donare", - "@donateButton": {} + "@donateButton": {}, + "sundayLetter": "D", + "@sundayLetter": {}, + "tuesdayShort": "Mar", + "@tuesdayShort": {}, + "sundayFull": "Domenica", + "@sundayFull": {}, + "thursdayLetter": "G", + "@thursdayLetter": {}, + "fridayLetter": "V", + "@fridayLetter": {}, + "wednesdayShort": "Mer", + "@wednesdayShort": {}, + "thursdayShort": "Giov", + "@thursdayShort": {}, + "fridayShort": "Ven", + "@fridayShort": {}, + "saturdayShort": "Sab", + "@saturdayShort": {}, + "sundayShort": "Dom", + "@sundayShort": {}, + "mondayLetter": "L", + "@mondayLetter": {}, + "tuesdayLetter": "M", + "@tuesdayLetter": {}, + "wednesdayLetter": "M", + "@wednesdayLetter": {}, + "donateDescription": "Fai una donazione per supportare lo sviluppo dell'app", + "@donateDescription": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "donorsDescription": "I nostri donatori", + "@donorsDescription": {}, + "showPreviousLapSetting": "Mostra barra precedente", + "@showPreviousLapSetting": {}, + "cityAlreadyInFavorites": "Questa città è già presente tra i tuoi preferiti", + "@cityAlreadyInFavorites": {}, + "editButton": "Modifica", + "@editButton": {}, + "saturdayFull": "Sabato", + "@saturdayFull": {}, + "mondayShort": "Lun", + "@mondayShort": {}, + "contributorsDescription": "Chi rende questa app possibile", + "@contributorsDescription": {}, + "sameTime": "Stessa ora", + "@sameTime": {}, + "relativeTime": "{hours}ore {relative, select, ahead{in avanti} behind{indietro} other{altro}}", + "@relativeTime": {}, + "searchCityPlaceholder": "Cerca città", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Scegli durata", + "@durationPickerTitle": {}, + "elapsedTime": "Tempo passato", + "@elapsedTime": {}, + "mondayFull": "Lunedì", + "@mondayFull": {}, + "tuesdayFull": "Martedì", + "@tuesdayFull": {}, + "wednesdayFull": "Mercoledì", + "@wednesdayFull": {}, + "thursdayFull": "Giovedì", + "@thursdayFull": {}, + "fridayFull": "Venerdì", + "@fridayFull": {}, + "widgetsSettingGroup": "Widgets", + "@widgetsSettingGroup": {}, + "layoutSettingGroup": "Disposizione", + "@layoutSettingGroup": {}, + "searchSettingPlaceholder": "Cerca impostazione", + "@searchSettingPlaceholder": {} } From e44864ca6353115021e9f6ce0b7108e586965e64 Mon Sep 17 00:00:00 2001 From: Kawczynski Olaf Date: Sat, 22 Jun 2024 09:19:21 +0000 Subject: [PATCH 054/177] Translated using Weblate (Polish) Currently translated at 27.5% (92 of 334 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 82791306..30d81c3f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -188,5 +188,15 @@ "tagsSetting": "znaczniki", "@tagsSetting": {}, "vendorSettingDescription": "ręczne wyłączenie ustawień producenta", - "@vendorSettingDescription": {} + "@vendorSettingDescription": {}, + "pickerSpinner": "Wybór", + "@pickerSpinner": {}, + "durationPickerSetting": "Wybór czasu trwania", + "@durationPickerSetting": {}, + "swipActionCardAction": "Akcje kart", + "@swipActionCardAction": {}, + "swipeActionCardActionDescription": "Przesuń w lewo lub w prawo na karcie, aby wykonać czynności", + "@swipeActionCardActionDescription": {}, + "swipActionSwitchTabs": "Przełącz karty", + "@swipActionSwitchTabs": {} } From 157530073e3d81ca947e1baca85010bc3a6fe571 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 24 Jun 2024 23:42:34 +0500 Subject: [PATCH 055/177] Add next alarm filter --- lib/alarm/data/alarm_sort_options.dart | 4 +- lib/alarm/logic/new_alarm_snackbar.dart | 72 ++++++++++++++--- lib/alarm/screens/alarm_screen.dart | 81 ++++++++++++++----- lib/alarm/utils/next_alarm.dart | 16 ++++ lib/common/widgets/list/custom_list_view.dart | 1 + .../widgets/list/persistent_list_view.dart | 11 ++- lib/l10n/app_en.arb | 34 +++++++- lib/navigation/screens/nav_scaffold.dart | 2 +- .../data/alarm_app_settings_schema.dart | 3 + lib/stopwatch/screens/stopwatch_screen.dart | 1 - 10 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 lib/alarm/utils/next_alarm.dart 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/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/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index cc96b771..4e7aba13 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -2,6 +2,7 @@ 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'; @@ -10,18 +11,18 @@ 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'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/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/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/listener_manager.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, @@ -40,6 +41,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 +56,22 @@ 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(); + // ListenerManager().addListener(); } @@ -75,6 +80,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 +126,17 @@ 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(getSnackbar( + 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 +150,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,6 +187,24 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } + 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; + } + @override Widget build(BuildContext context) { Future selectTime() async { @@ -221,29 +253,38 @@ class _AlarmScreenState extends State { }, placeholderText: AppLocalizations.of(context)!.noAlarmMessage, reloadOnPop: true, - listFilters: _showFilters.value ? alarmListFilters : [], + onSaveItems: (items) { + nextAlarm = getNextAlarm(); + setState(() {}); + }, + // header: getNextAlarmWidget(), + listFilters: getListFilterItems(), customActions: _showFilters.value ? [ ListFilterCustomAction( - name: AppLocalizations.of(context)!.enableAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .enableAllFilteredAlarmsAction, icon: Icons.alarm_on_rounded, action: (alarms) { _handleEnableChangeMultiple(alarms, true); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.disableAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .disableAllFilteredAlarmsAction, icon: Icons.alarm_off_rounded, action: (alarms) { _handleEnableChangeMultiple(alarms, false); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.skipAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .skipAllFilteredAlarmsAction, icon: Icons.skip_next_rounded, action: (alarms) { _handleSkipChangeMultiple(alarms, true); }), ListFilterCustomAction( - name: AppLocalizations.of(context)!.cancelSkipAllFilteredAlarmsAction, + name: AppLocalizations.of(context)! + .cancelSkipAllFilteredAlarmsAction, icon: Icons.skip_next_rounded, action: (alarms) { _handleSkipChangeMultiple(alarms, false); diff --git a/lib/alarm/utils/next_alarm.dart b/lib/alarm/utils/next_alarm.dart new file mode 100644 index 00000000..ebb8eaf3 --- /dev/null +++ b/lib/alarm/utils/next_alarm.dart @@ -0,0 +1,16 @@ + + +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/common/utils/json_serialize.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/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 4c92e016..429eb7cb 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -417,6 +417,7 @@ class _CustomListViewState ), ), ), + if(widget.header != null) widget.header!, Expanded( flex: 1, child: Stack(children: [ diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index ac453fc8..fb7b3b6d 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -75,6 +75,8 @@ class PersistentListView extends StatefulWidget { this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], + this.header, + this.onSaveItems = null, // this.initialSortIndex = 0, }); @@ -91,10 +93,12 @@ class PersistentListView extends StatefulWidget { final bool isDuplicateEnabled; final bool reloadOnPop; final bool shouldInsertOnTop; + final Widget? header; // final int initialSortIndex; final List> listFilters; final List> customActions; final List> sortOptions; + final Function(List items)? onSaveItems; @override State createState() => _PersistentListViewState(); @@ -159,10 +163,12 @@ class _PersistentListViewState } } - void _saveItems() { + void _saveItems () async { if (widget.saveTag.isNotEmpty) { - saveList(widget.saveTag, _items); + await saveList(widget.saveTag, _items); } + widget.onSaveItems?.call(_items); + } void _handleChangeSort(int index) { @@ -191,6 +197,7 @@ class _PersistentListViewState sortOptions: widget.sortOptions, initialSortIndex: _initialSortIndex, onChangeSortIndex: _handleChangeSort, + header: widget.header, ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a95d89ae..a94311d7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -674,7 +674,35 @@ "editTagLabel": "Edit Tag", "@editTagLabel": {}, "tagNamePlaceholder": "Tag name", - "@tagNamePlaceholder": {} - - + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 week} other{{count} weeks}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 month} other{{count} months}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 year} other{{count} years}}", + "@yearsString": {}, + "lessThanOneMinute": "less than 1 minute", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Alarm will ring in {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Next: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} and {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}h", + "@shortTimeFormat": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showNextAlarm": "Show Next Alarm", + "@showNextAlarm": {} } diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 0770b4a8..904a6acf 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -57,7 +57,7 @@ class _NavScaffoldState extends State { DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmSnackbarText(alarm), fab: true, navBar: true)); + getSnackbar(getNewAlarmText(context, alarm), fab: true, navBar: true)); }); } diff --git a/lib/settings/data/alarm_app_settings_schema.dart b/lib/settings/data/alarm_app_settings_schema.dart index 89a3a6c3..eaf9076a 100644 --- a/lib/settings/data/alarm_app_settings_schema.dart +++ b/lib/settings/data/alarm_app_settings_schema.dart @@ -77,6 +77,9 @@ SettingGroup alarmAppSettingsSchema = SettingGroup( (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/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index 6a477d11..68a3bf13 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -152,7 +152,6 @@ class _StopwatchScreenState extends State { lap: lap, ), placeholderText: AppLocalizations.of(context)!.noLapsMessage, - // header: CardContainer(child:), isDeleteEnabled: false, isDuplicateEnabled: false, isReorderable: false, From 115a68978132bcf92f80841ac8c12aaa6606c944 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 25 Jun 2024 00:28:11 +0500 Subject: [PATCH 056/177] Fix range schedule not working --- lib/alarm/data/alarm_settings_schema.dart | 2 +- .../types/schedules/range_alarm_schedule.dart | 16 +++++++++------- lib/common/widgets/fields/date_picker_field.dart | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index 84e19fe0..46928399 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -121,7 +121,7 @@ SettingGroup alarmSettingsSchema = SettingGroup( DateTimeSetting( "Date Range", (context) => AppLocalizations.of(context)!.alarmRangeSetting, - [], + [DateTime.now(), DateTime.now().add(const Duration(days: 2))], rangeOnly: true, enableConditions: [ ValueCondition(["Type"], (value) => value == RangeAlarmSchedule) diff --git a/lib/alarm/types/schedules/range_alarm_schedule.dart b/lib/alarm/types/schedules/range_alarm_schedule.dart index 8af5bd44..e5b00105 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,23 @@ class RangeAlarmSchedule extends AlarmSchedule { } @override - Future schedule(Time time,String description) async { + Future schedule(Time time, String description) async { // 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 { + print('$alarmDate $startDate $endDate'); + if (alarmDate.isAfter(endDate)) { _isFinished = true; + } else { + print("_____________"); + await _alarmRunner.schedule(alarmDate, description); + _isFinished = false; } } @override - Future cancel()async { + Future cancel() async { await _alarmRunner.cancel(); } 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; From eb44c8c71d7209e415a6ed91e99e03083de00f56 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Thu, 27 Jun 2024 15:00:26 +0500 Subject: [PATCH 057/177] Add foreground notification option --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 11 +- .../metadata/android/en-US/changelogs/251.txt | 2 + lib/app.dart | 41 +++- lib/l10n/app_en.arb | 4 +- lib/main.dart | 1 + lib/navigation/screens/nav_scaffold.dart | 67 ++++- .../data/general_settings_schema.dart | 1 + lib/system/logic/initialize_isolate.dart | 3 + lib/timer/screens/timer_screen.dart | 229 ++++++++++++++---- pubspec.lock | 64 +++++ pubspec.yaml | 3 +- 12 files changed, 378 insertions(+), 50 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/251.txt diff --git a/android/app/build.gradle b/android/app/build.gradle index 08ef8d71..5ee517ef 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,7 +53,7 @@ android { defaultConfig { applicationId "com.vicolo.chrono" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b4a9535b..912369a8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,16 +5,19 @@ android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - - + + + + + + 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..2d9729ba --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -0,0 +1,2 @@ +* Added current lap card for stopwatch. Now you can view the current lap time in real time. +* Removed support for Android 5 (min api level now is 23/Android 6) diff --git a/lib/app.dart b/lib/app.dart index c191902c..0cb3a662 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -22,9 +22,43 @@ 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'; +void _initForegroundTask() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: + 'This notification appears when the foreground service is running.', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + iconData: const NotificationIconData( + resType: ResourceType.drawable, + resPrefix: ResourcePrefix.ic, + name: 'alarm_icon', + ), + // buttons: [ + // const NotificationButton(id: 'sendButton', text: 'Send'), + // const NotificationButton(id: 'testButton', text: 'Test'), + // ], + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: true, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 5000, + isOnceEvent: false, + autoRunOnBoot: true, + allowWakeLock: true, + // allowWifiLock: true, + ), + ); +} + class App extends StatefulWidget { const App({super.key}); @@ -59,6 +93,8 @@ class _AppState extends State { void initState() { super.initState(); + _initForegroundTask(); + setDigitalClockWidgetData(context); NotificationController.setListeners(); @@ -210,11 +246,12 @@ class _AppState extends State { return MaterialPageRoute( builder: (context) { final args = settings.arguments as AlarmNotificationArguments; - return AlarmNotificationScreen( + return WithForegroundTask( + child: AlarmNotificationScreen( scheduleId: args.scheduleIds[0], initialIndex: args.tasksOnly ? 0 : -1, dismissType: args.dismissType, - ); + )); }, ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a94311d7..28f18dae 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -704,5 +704,7 @@ "shortSecondsString": "{seconds}s", "@shortSecondsString": {}, "showNextAlarm": "Show Next Alarm", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "showForegroundNotification": "Show Foreground Notification", + "@showForegroundNotification": {} } diff --git a/lib/main.dart b/lib/main.dart index bfd15198..724c29bb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'dart:ui'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; +import 'package:awesome_notifications/android_foreground_service.dart'; import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 904a6acf..c7dd4ec9 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:isolate'; import 'package:clock_app/alarm/logic/new_alarm_snackbar.dart'; import 'package:clock_app/alarm/types/alarm.dart'; @@ -14,8 +15,40 @@ import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/system/logic/handle_intents.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:receive_intent/receive_intent.dart' as intent_handler; +// The callback function should always be a top-level function. +@pragma('vm:entry-point') +void startCallback() { + // The setTaskHandler function must be called to handle the task in the background. + FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +} + +class FirstTaskHandler extends TaskHandler { + SendPort? _sendPort; + + // Called when the task is started. + @override + void onStart(DateTime timestamp, SendPort? sendPort) async { + _sendPort = sendPort; + } + + @override + void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {} + + @override + void onDestroy(DateTime timestamp, SendPort? sendPort) async {} + + @override + void onNotificationButtonPressed(String id) {} + + @override + void onNotificationPressed() { + FlutterForegroundTask.launchApp("/"); + } +} + class NavScaffold extends StatefulWidget { const NavScaffold({super.key, this.initialTabIndex = 0}); @@ -29,6 +62,7 @@ class _NavScaffoldState extends State { late int _selectedTabIndex; late Setting useMaterialNavBarSetting; late Setting swipeActionSetting; + late Setting showForegroundSetting; late StreamSubscription _sub; late PageController _controller; @@ -56,8 +90,10 @@ class _NavScaffoldState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar( - getSnackbar(getNewAlarmText(context, alarm), fab: true, navBar: true)); + ScaffoldMessenger.of(context).showSnackBar(getSnackbar( + getNewAlarmText(context, alarm), + fab: true, + navBar: true)); }); } @@ -86,6 +122,25 @@ class _NavScaffoldState extends State { }); } + Future _updateForegroundNotification(dynamic value) async { + if (!value) { + return FlutterForegroundTask.stopService(); + } + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.updateService( + notificationTitle: 'Foreground Service is running', + notificationText: '', + callback: startCallback, + ); + } else { + return FlutterForegroundTask.startService( + notificationTitle: 'Foreground Service is running', + notificationText: '', + callback: startCallback, + ); + } + } + @override void initState() { super.initState(); @@ -96,16 +151,24 @@ class _NavScaffoldState extends State { .getSetting("Use Material Style"); swipeActionSetting = appSettings.getGroup("General").getSetting("Swipe Action"); + showForegroundSetting = appSettings + .getGroup("General") + .getGroup("Reliability") + .getSetting("Show Foreground Notification"); swipeActionSetting.addListener(update); useMaterialNavBarSetting.addListener(update); + showForegroundSetting.addListener(_updateForegroundNotification); _controller = PageController(initialPage: widget.initialTabIndex); _selectedTabIndex = widget.initialTabIndex; + + _updateForegroundNotification(showForegroundSetting.value); } @override void dispose() { useMaterialNavBarSetting.removeListener(update); swipeActionSetting.removeListener(update); + showForegroundSetting.removeListener(_updateForegroundNotification); _sub.cancel(); _controller.dispose(); super.dispose(); diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 1843e658..750034e2 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -231,6 +231,7 @@ SettingGroup generalSettingsSchema = SettingGroup( ), SettingGroup("Reliability", (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, [ + SwitchSetting("Show Foreground Notification", (context) => AppLocalizations.of(context)!.showForegroundNotification, false), SettingAction( "Ignore Battery Optimizations", (context) => diff --git a/lib/system/logic/initialize_isolate.dart b/lib/system/logic/initialize_isolate.dart index cda95c0d..12035168 100644 --- a/lib/system/logic/initialize_isolate.dart +++ b/lib/system/logic/initialize_isolate.dart @@ -7,9 +7,12 @@ import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; import 'package:clock_app/settings/logic/initialize_settings.dart'; import 'package:clock_app/system/data/device_info.dart'; +import 'package:flutter/widgets.dart'; Future initializeIsolate() async { DartPluginRegistrant.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + await initializeAndroidInfo(); await initializeAppDataDirectory(); await initializeStorage(false); diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 71f7e56e..2d2e049b 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -1,22 +1,27 @@ import 'dart:async'; +import 'dart:isolate'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/common/logic/customize_screen.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/data/timer_list_filters.dart'; import 'package:clock_app/timer/data/timer_sort_options.dart'; import 'package:clock_app/timer/logic/timer_notification.dart'; import 'package:clock_app/timer/screens/timer_fullscreen.dart'; +import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/widgets/timer_duration_picker.dart'; import 'package:clock_app/timer/widgets/timer_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:great_list_view/great_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; @@ -24,6 +29,85 @@ import 'package:clock_app/timer/types/timer.dart'; import 'package:clock_app/timer/widgets/timer_card.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +// Future updateForegroundTask(List timers) async { +// final runningTimers = timers.where((timer) => !timer.isStopped).toList(); +// if (runningTimers.isEmpty) { +// FlutterForegroundTask.stopService(); +// // timerNotificationInterval?.cancel(); +// return false; +// } +// // Get timer with lowest remaining time +// final timer = runningTimers +// .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); +// final count = runningTimers.length; +// +// if (await FlutterForegroundTask.isRunningService) { +// return FlutterForegroundTask.updateService( +// notificationTitle: +// "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count - 1} timers' : ''}", +// notificationText: +// TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), +// callback: startCallback, +// ); +// } else { +// return FlutterForegroundTask.startService( +// notificationTitle: +// "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count - 1} timers' : ''}", +// notificationText: +// TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), +// callback: startCallback, +// ); +// } +// } +// +// // The callback function should always be a top-level function. +// @pragma('vm:entry-point') +// void startCallback() async { +// await initializeIsolate(); +// // The setTaskHandler function must be called to handle the task in the background. +// FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +// } +// +// class FirstTaskHandler extends TaskHandler { +// // SendPort? _sendPort; +// +// // Called when the task is started. +// @override +// void onStart(DateTime timestamp, SendPort? sendPort) async {} +// +// // Called every [interval] milliseconds in [ForegroundTaskOptions]. +// @override +// void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { +// // Send data to the main isolate. +// // sendPort?.send(timestamp); +// final timers = await loadList('timers'); +// updateForegroundTask(timers); +// } +// +// // Called when the notification button on the Android platform is pressed. +// @override +// void onDestroy(DateTime timestamp, SendPort? sendPort) async {} +// +// // Called when the notification button on the Android platform is pressed. +// @override +// void onNotificationButtonPressed(String id) { +// // print('onNotificationButtonPressed >> $id'); +// } +// +// // Called when the notification itself on the Android platform is pressed. +// // +// // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for +// // this function to be called. +// @override +// void onNotificationPressed() { +// // Note that the app will only route to "/resume-route" when it is exited so +// // it will usually be necessary to send a message through the send port to +// // signal it to restore state when the app is already started. +// FlutterForegroundTask.launchApp("/"); +// // _sendPort?.send('onNotificationPressed'); +// } +// } + typedef TimerCardBuilder = Widget Function( BuildContext context, int index, @@ -42,19 +126,77 @@ class _TimerScreenState extends State { late Setting _showFilters; late Setting _showSort; late Setting _showNotification; + ReceivePort? _receivePort; void update(value) { setState(() {}); _listController.changeItems((timers) => {}); } + void _updateTimerNotification() { + // updateForegroundTask(_listController.getItems()); + if (!_showNotification.value) { + AwesomeNotifications() + .cancelNotificationsByChannelKey(timerNotificationChannelKey); + timerNotificationInterval?.cancel(); + return; + } + final runningTimers = + _listController.getItems().where((timer) => !timer.isStopped).toList(); + if (runningTimers.isEmpty) { + AwesomeNotifications() + .cancelNotificationsByChannelKey(timerNotificationChannelKey); + timerNotificationInterval?.cancel(); + return; + } + // Get timer with lowest remaining time + final timer = runningTimers + .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); + + updateTimerNotification(timer, runningTimers.length); + timerNotificationInterval?.cancel(); + timerNotificationInterval = Timer.periodic(const Duration(seconds: 1), (t) { + updateTimerNotification(timer, runningTimers.length); + }); + } + void onTimerUpdate() async { if (mounted) { _listController.reload(); setState(() {}); // _listController.changeItems((timers) => {}); } - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); + } + + bool _registerReceivePort(ReceivePort? newReceivePort) { + if (newReceivePort == null) { + return false; + } + + _closeReceivePort(); + + _receivePort = newReceivePort; + _receivePort?.listen((data) { + // if (data is int) { + // print('eventCount: $data'); + // } else if (data is String) { + // if (data == 'onNotificationPressed') { + // Navigator.of(context).pushNamed('/resume-route'); + // } + // } else if (data is DateTime) { + // print('timestamp: ${data.toString()}'); + // } + }); + + return _receivePort != null; + } + + void _closeReceivePort() { + _receivePort?.close(); + _receivePort = null; } @override @@ -75,7 +217,17 @@ class _TimerScreenState extends State { _showSort.addListener(update); _showNotification.addListener(update); ListenerManager.addOnChangeListener("timers", onTimerUpdate); - showProgressNotification(); + // showProgressNotification(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + // await _requestPermissionForAndroid(); + + // You can get the previous ReceivePort without restarting the service. + if (await FlutterForegroundTask.isRunningService) { + final newReceivePort = FlutterForegroundTask.receivePort; + _registerReceivePort(newReceivePort); + } + }); } @override @@ -83,6 +235,7 @@ class _TimerScreenState extends State { _showFilters.removeListener(update); _showSort.removeListener(update); _showNotification.removeListener(update); + _closeReceivePort(); // ListenerManager.removeOnChangeListener("timers", onTimerUpdate); super.dispose(); @@ -90,21 +243,27 @@ class _TimerScreenState extends State { Future _onDeleteTimer(ClockTimer deletedTimer) async { await deletedTimer.reset(); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); // _listController.deleteItem(deletedTimer); } Future _handleToggleState(ClockTimer timer) async { await timer.toggleState(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleStartTimer(ClockTimer timer) async { if (timer.isRunning) return; await timer.start(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleStartMultipleTimers(List timers) async { @@ -113,14 +272,17 @@ class _TimerScreenState extends State { await timer.start(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handlePauseTimer(ClockTimer timer) async { if (timer.isPaused) return; await timer.pause(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + // showProgressNotification(); } Future _handlePauseMultipleTimers(List timers) async { @@ -129,13 +291,17 @@ class _TimerScreenState extends State { await timer.pause(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleResetTimer(ClockTimer timer) async { await timer.reset(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleResetMultipleTimers(List timers) async { @@ -143,13 +309,17 @@ class _TimerScreenState extends State { await timer.reset(); } _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _handleAddTimeToTimer(ClockTimer timer) async { await timer.addTime(); _listController.changeItems((timers) {}); - showProgressNotification(); + _updateTimerNotification(); + + // showProgressNotification(); } Future _openCustomizeTimerScreen( @@ -178,34 +348,10 @@ class _TimerScreenState extends State { await timer.start(); _listController.changeItems((timers) {}); }); - showProgressNotification(); - return timer; - } - - Future showProgressNotification() async { - if (!_showNotification.value) { - AwesomeNotifications() - .cancelNotificationsByChannelKey(timerNotificationChannelKey); - timerNotificationInterval?.cancel(); - return; - } - final runningTimers = - _listController.getItems().where((timer) => !timer.isStopped).toList(); - if (runningTimers.isEmpty) { - AwesomeNotifications() - .cancelNotificationsByChannelKey(timerNotificationChannelKey); - timerNotificationInterval?.cancel(); - return; - } - // Get timer with lowest remaining time - final timer = runningTimers - .reduce((a, b) => a.remainingSeconds < b.remainingSeconds ? a : b); + _updateTimerNotification(); - updateTimerNotification(timer, runningTimers.length); - timerNotificationInterval?.cancel(); - timerNotificationInterval = Timer.periodic(const Duration(seconds: 1), (t) { - updateTimerNotification(timer, runningTimers.length); - }); + // showProgressNotification(); + return timer; } @override @@ -223,8 +369,8 @@ class _TimerScreenState extends State { onToggleState: () => _handleToggleState(timer), onPressDelete: () => _listController.deleteItem(timer), onPressDuplicate: () => _listController.duplicateItem(timer), - onPressReset: ()=> _handleResetTimer(timer), - onPressAddTime: ()=> _handleAddTimeToTimer(timer), + onPressReset: () => _handleResetTimer(timer), + onPressAddTime: () => _handleAddTimeToTimer(timer), ), onTapItem: (timer, index) async { await Navigator.push( @@ -289,7 +435,8 @@ class _TimerScreenState extends State { await timer.start(); _listController.addItem(timer); } - showProgressNotification(); + _updateTimerNotification(); + // showProgressNotification(); } }, ) diff --git a/pubspec.lock b/pubspec.lock index e8de28dc..a5587009 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" + url: "https://pub.dev" + source: hosted + version: "6.5.0" flutter_html: dependency: "direct main" description: @@ -865,6 +873,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" simple_gesture_detector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 25068e71..d267aad9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: clock_app description: An alarm, clock, timer and stowatch app. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.5.1+24 +version: 0.5.2+25 environment: sdk: ">=2.18.6 <4.0.0" @@ -78,6 +78,7 @@ dependencies: ref: main permission_handler: ^11.3.1 device_info_plus: ^10.1.0 + flutter_foreground_task: ^6.5.0 dev_dependencies: From c95e08da8e174dec7b8163b309afd9e33c922719 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Thu, 27 Jun 2024 15:02:43 +0500 Subject: [PATCH 058/177] Fix typo in stopwatch serialization --- lib/stopwatch/types/stopwatch.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index cc3a2a68..abb706ba 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -163,7 +163,7 @@ class ClockStopwatch extends JsonSerializable { orElse: () => TimerState.stopped); _id = json['id'] ?? UniqueKey().hashCode; // _finishedLaps = []; - _laps = listFromString(json['laps'] ?? ''); + _laps = listFromString(json['laps'] ?? '[]'); updateFastestAndSlowestLap(); } } From 7b96f0f8735c723d6a90826c05a21136c12286f6 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 28 Jun 2024 21:39:00 +0500 Subject: [PATCH 059/177] Revert min sdk version back to 21 --- android/app/src/main/AndroidManifest.xml | 6 +-- .../src/main/res/drawable/ic_alarm_icon.png | Bin 0 -> 4923 bytes lib/app.dart | 40 +--------------- lib/main.dart | 2 + lib/navigation/screens/nav_scaffold.dart | 9 ++-- .../data/notification_channel.dart | 18 +++++++ lib/notifications/logic/foreground_task.dart | 33 +++++++++++++ lib/notifications/logic/notifications.dart | 3 +- lib/timer/screens/timer_screen.dart | 44 +----------------- pubspec.lock | 7 ++- pubspec.yaml | 5 +- 11 files changed, 73 insertions(+), 94 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_alarm_icon.png create mode 100644 lib/notifications/logic/foreground_task.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 912369a8..673baac5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -111,9 +111,9 @@ - - - + + + MwEfH&3?U2>B?zKNbcyl}qK`Ui^xk_PHAamVohZ>WYV;sM7(|U2(L>aU zlBh96=gIpE-uq$Sv(~-qu6x$n`|f-0i9={Alan%#0sue`S5eTpiKu^o65Wiw(pFz? z0*R-Jkq-c9e)NT0R+on2hen)M7h@31xU-wp=l#Ei(^74mG|Hy}u~R zz8{{U1tuY1Nj>1pD2tX0#K{JTuB40e_#1l$a71NKx)U~h3O?S=e7B59r(-usYes5b z8&>MO!V5l1A(3kfdxdinK1xzu`BKRB5qfKTQ72P{ntNk!TSN)RmjC}iIEW`rIM4r; z&R1w~J}KqKfg~j*jbqMQ-t@C46Y1U}=qmegv=AbF{mfRgoSS}LnySbI#F%hE(L|Bx zK-ub^cyJ!C4&dE_Wk`ZR7_QdS3JUe->z6&csp16@wr=NCL{A|Yh7FrS!-6;X%j1%L zrZ_;SauUoMNQesM^j9w&I*ms6=W7Wmy*RD2x){B`*817oi-t{5ns|^i{ruKYopstM zF~Ira?Wa$nv=dk5%)Spf_doj`sy7Z_#w@bOMvITUiwuz zL!5%OZgVAhFN0wfUu1`+LA1 zol^(MKu3Y>Tm1#V9=u4LpftIN6}TV%2L^Mq#c*M42IV^Rj4!v2wqz1ZLVeapzJGAS zy}VB_tKaLY2T@vw+|dH}BBg<>goDm&O+|9LNSyc%c#}q5S?I(OaEqz(2VJCLJfQ2Z zr_Ww=BR&m^kIGw`GWSdley(Q&>5ZR4uC!|n-v!)GQAJGv-0D(V<(*R2|>C@ltN}@-T;-mRa9aj{UKrXPv1w=-LRe$eu|w2e4HnJi-ki4 zFXUZT6c_t`)J9-l&!qNuq)`r3jzo6tX_(wgOfFUN1ipj3ttCgDGhF9aHDCs|Y^x3Z zd?>VO6(4tga2k?a>&3CegQ8%m!5u7|@|(%KfhJ4v^Ye3mACY)b%hCdl-;PY|V!8wL zjD2;B5hpcCR*w{lI8e^WPMmm+(0Z5svJ3Ea{K&}W4CwUXhVJhC`bCcrt7?H}5F&O) zv9o`ziJr=#O1J7p4F~x6y+V)4^&_3*W#Pco_JsnQP^dAVKQZ0&%&$mNb_@j>8HL5n z{CvA&Uy(7}Y%x&v{WJ;p-Xw9)X2Xp2Lg4miiw{rTHPN183WAmY7L5iZUeRkk^U&wx zyAPs+)aQWMEFYtcfK~ndOQA>s?}Wa+YMLZt2sJ57z9zO@wlgMW?=-eANtfJqaP}|x zgLdn(mm&8tSd5|K-5OFLEFj=Ra0%QMj_QOXKsVN5VjJ%@y?ehfvt0TT9Tf;ltb`<M)dZ5JiiuK9JU>|IbICEPYG!>8UDP<~i!=~?-Qv+J4py1}`w=vgx=4*(td+Z+g9!5XB7Q|o8;Q8h9J|N0;Xy1j3p;I{NRd7p9!^tGks&;k z%rwKjX^*RfDT!BNDJq zM=dHd$augW9WqH$#Myh|FV*@@q-u48Te|2l-Yn`PQXcGgX=j32oF3;$-ubL0SRV|< zTiFMuZJi5LpXwA)3#xg2nBTebo~P(%0!;@l3Sa`PiT#}YUhrnBf)8B>N5e`XjD~yp z6o5omy(pzGwCI3fE=fE!!2KDUkk;4Nb1Imkf&Q%!%d`Md=eSVNva;Z4N`GRZVef>b zzEoi>AL}VS5L}gUE$((p)h`1v| z_qN*nkHE0F8y0T$Po1@#1dZY|xDDaUty%rVKd(f9Fl7^vGO=t6(bDq!Aq55^?(O^=XDWY?+ZAV@&)ep zcJioCGe4YIfAi(@Mno!+73x`?F3awhz%o|&0KE;YU$a({*r4m1jtrODEYVFRBCkiOzxX8z+f= zw51M>8q`gk9TdH}g<|ZeByuv1Vf>_2)`rE36O2Va`$k1B&_C-F^#x>Onprq6IESkqx9y=$2fn6z(TXY1pv=_z;g-Lgw)uCBOV9r3yA` zSbLUSvKh1e-q>ig_Fg_vc4368^uR>kWwoZc$2 zz7QwMm+e!R63xiDd&9|IS%{Y3T-&WLKm}*yk0%q;_S-)*gh2IoE-Iakz~HCtd30ft3La5@y?j&XE98m4XZzj-ymFAT-=urMCM{2BB>T8u8R^Gc~k^eaPd!`imWJyDPxbZsxjuEJ(@{#;Zi4k1INh}qLP znT$rQRoT&pRO7^RiEYVQ&OV;XMQSpkQmSBaCK%0rK20 zwfj`jRX<{y9~EqngI6sM-<{kaPqJZ)jHAk$Mm~s9KM@rH%eospYS)j=MF+ha8ovZG z4h~&#~Z7-8`{{g)Y#ExtSC=%&R`^ z3wF*;YWyAAmT(x!-7qF;BFGw}1Qd%jh#Z_zWf{6`nOQ~+a(sT7g=%f8oQzvH(T_2# z5ZIlZf?^mdRei69poUHM8inK?)j(5Q}@IU&Ac2)Bf@;) z!D?=KxY={=j{bK@cQ3NfiCg3FU?;XzXgZO-TsBX&sdzojlO)c8Sis(W=eX!YCeO!~ zCn1SCoBWHE{IsjrUI(Hu6R{<)+fSysbr#nb=2|)}ZQ%>CfPu$sJ$<~CyF!RcGrv*+ znf*G|AN0rxd{Y%lGIZk9@5O5_xyv3ev=XGHAvO5nS!}crm(hTVQN6h` zo@e|bS{!4?ycK14M$~wjNT%oivVrY+?^@`_)iq=%kVFNTH1nmBrN)Uew6(RhtcULL z*_$RKsO5noI+1G)jvdQ&oan`66_tgJ(35ly1ByeIriPu&o{{ad#eA|WGP%)OdS4y8 ziR;`_iq|ftoyrPSDXrt@3OO6X$j4+Yf!K$u`UKps3SmBdg@B*FM~GLX^?x z4%i$UzN+DDpiO%7H}R&QewR=gUgPxfP_Fg2(tvFh%FY_6HnbsMOdD}(Ha6UwG$?4n zHr})}fgVyxy&GCNM9t4RwB{`zh;7d$p(Q|Q7&ZMjVnoc=7+V}tk_%O$n9Mf&nsz{p z(8y9BEOR@bP`bR=f7CHRxT2D=ce@}Fq_Wj7C@oB~K?Csgt%h@F7Es_E*b+(q)f?c8 zvPO`uqR57G{V3jmFiH{NY{PBXP>w=^jj2Qo^x~{RIVKfd|4cFyVzrW86*9my7neh@Q2!SSS+*tBP^!p zy_D7`$l|KtNcrNTMuluw0K^;>#M+Dn|ZY{7$m$ab#*VC&yK2&f;iF#Z7nkmfVcEyYBHm=x5Ah;s;>%Flu_E^ocu5QzlX!p zY=>r=T0%W(+^UFvwH#X2M?5)_y1%CuuEypx`B+a8Sc8*UKrT(hj_t>)o#v%Q;{6rj z{ViHtunw@LfW`!{j3T7Uiv?dNH05t4BhXH{8SOg$G4xxB;&6G2iz1gc zpH2DI?V`O({+G$o^sVa&eN`Oas)iXYGc-#KwQlm>{M_{_dXe=u|KRCWyS<5h%)y<& z0J60THn;5h_XxfhkK?7%IZLX*0Lt}o`@q!MW1I$rDZJ$ z)dgnx$*L3NV;Qjdd2!J^zqk61t{(`H4K|tY> z5#8NivVSpr&HL70DvO>icW#Cmh}@hbneSdI+X-fID{2D?7H}mLe0Muuc%Du61~QC) zeU#K>s2V68J__(+zsX~MTkXS})$=TLK|pD8B+1%%A4Kp{8RQzDp4M? z9=*1_LJG?+K7A$A{7xB@uK=YX$x+xy0K@O%aa}?SO6k^7{0r@^m_)Sm!|sH zwcg^Uw@m|{&H5)cRdghWry!gW(gV&wGH=MOO}+j`u-Z)4kVNMq+vvg#UCiah`Z4*d zg47kbv|TSdMoc9%MvHWuXq8+gwOJ(U&sGh_WH23-?d|8dpu&%NZ;K&gw!y#MIqKnl hPNYTM{}-HEoa=5fbT&=hyJ>s@a79gpS~<&z{{bPcG_3#t literal 0 HcmV?d00001 diff --git a/lib/app.dart b/lib/app.dart index 0cb3a662..d69af086 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -26,39 +26,6 @@ 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'; -void _initForegroundTask() { - FlutterForegroundTask.init( - androidNotificationOptions: AndroidNotificationOptions( - channelId: 'foreground_service', - channelName: 'Foreground Service Notification', - channelDescription: - 'This notification appears when the foreground service is running.', - channelImportance: NotificationChannelImportance.LOW, - priority: NotificationPriority.LOW, - iconData: const NotificationIconData( - resType: ResourceType.drawable, - resPrefix: ResourcePrefix.ic, - name: 'alarm_icon', - ), - // buttons: [ - // const NotificationButton(id: 'sendButton', text: 'Send'), - // const NotificationButton(id: 'testButton', text: 'Test'), - // ], - ), - iosNotificationOptions: const IOSNotificationOptions( - showNotification: true, - playSound: false, - ), - foregroundTaskOptions: const ForegroundTaskOptions( - interval: 5000, - isOnceEvent: false, - autoRunOnBoot: true, - allowWakeLock: true, - // allowWifiLock: true, - ), - ); -} - class App extends StatefulWidget { const App({super.key}); @@ -93,8 +60,6 @@ class _AppState extends State { void initState() { super.initState(); - _initForegroundTask(); - setDigitalClockWidgetData(context); NotificationController.setListeners(); @@ -246,12 +211,11 @@ class _AppState extends State { return MaterialPageRoute( builder: (context) { final args = settings.arguments as AlarmNotificationArguments; - return WithForegroundTask( - child: AlarmNotificationScreen( + return AlarmNotificationScreen( scheduleId: args.scheduleIds[0], initialIndex: args.tasksOnly ? 0 : -1, dismissType: args.dismissType, - )); + ); }, ); diff --git a/lib/main.dart b/lib/main.dart index 724c29bb..dc5357a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/common/utils/debug.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; +import 'package:clock_app/notifications/logic/foreground_task.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; import 'package:clock_app/settings/logic/initialize_settings.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; @@ -47,6 +48,7 @@ void main() async { await updateAlarms("Update Alarms on Start"); await updateTimers("Update Timers on Start"); AppVisibility.initialize(); + initForegroundTask(); ReceivePort receivePort = ReceivePort(); IsolateNameServer.removePortNameMapping(updatePortName); diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index c7dd4ec9..fb5d2a3c 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -128,13 +128,13 @@ class _NavScaffoldState extends State { } if (await FlutterForegroundTask.isRunningService) { return FlutterForegroundTask.updateService( - notificationTitle: 'Foreground Service is running', + notificationTitle: 'Foreground service is running', notificationText: '', callback: startCallback, ); } else { return FlutterForegroundTask.startService( - notificationTitle: 'Foreground Service is running', + notificationTitle: 'Foreground service is running', notificationText: '', callback: startCallback, ); @@ -178,7 +178,8 @@ class _NavScaffoldState extends State { Widget build(BuildContext context) { Orientation orientation = MediaQuery.of(context).orientation; final tabs = getTabs(context); - return Scaffold( + return WithForegroundTask( + child: Scaffold( appBar: orientation == Orientation.portrait ? AppTopBar( title: Text( @@ -281,6 +282,6 @@ class _NavScaffoldState extends State { onTabSelected: _onTabSelected, ) : null, - ); + )); } } diff --git a/lib/notifications/data/notification_channel.dart b/lib/notifications/data/notification_channel.dart index 4c7c1282..16714689 100644 --- a/lib/notifications/data/notification_channel.dart +++ b/lib/notifications/data/notification_channel.dart @@ -1,6 +1,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/theme/theme.dart'; +const String foregroundNotificationChannelKey = 'foreground'; const String chronoNotificationChannelGroupKey = 'chrono'; const String reminderNotificationChannelKey = 'reminders'; const String stopwatchNotificationChannelKey = 'stopwatch'; @@ -22,6 +23,23 @@ final NotificationChannel alarmNotificationChannel = NotificationChannel( enableLights: false, ); + +// final NotificationChannel foregroundNotificationChannel = NotificationChannel( +// icon: 'resource://drawable/alarm_icon', +// // channelGroupKey: chronoNotificationChannelGroupKey, +// channelKey: foregroundNotificationChannelKey, +// channelName: 'Foreground Service', +// channelDescription: 'Notification channel for foreground service', +// defaultColor: defaultColorScheme.accent, +// locked: true, +// importance: NotificationImportance.Low, +// criticalAlerts: false, +// playSound: false, +// enableVibration: false, +// enableLights: false, +// ); + + final NotificationChannel reminderNotificationChannel = NotificationChannel( icon: 'resource://drawable/alarm_icon', // channelGroupKey: chronoNotificationChannelGroupKey, diff --git a/lib/notifications/logic/foreground_task.dart b/lib/notifications/logic/foreground_task.dart new file mode 100644 index 00000000..6855f40f --- /dev/null +++ b/lib/notifications/logic/foreground_task.dart @@ -0,0 +1,33 @@ +import 'package:clock_app/notifications/data/notification_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +void initForegroundTask() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: 'This notification appears when the foreground service is running.', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + iconData: const NotificationIconData( + resType: ResourceType.drawable, + resPrefix: ResourcePrefix.ic, + name: 'alarm_icon', + ), + // buttons: [ + // const NotificationButton(id: 'sendButton', text: 'Send'), + // const NotificationButton(id: 'testButton', text: 'Test'), + // ], + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: true, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 5000, + isOnceEvent: false, + autoRunOnBoot: true, + allowWakeLock: true, + allowWifiLock: true, + ), + );} diff --git a/lib/notifications/logic/notifications.dart b/lib/notifications/logic/notifications.dart index a4bd0bac..167ea376 100644 --- a/lib/notifications/logic/notifications.dart +++ b/lib/notifications/logic/notifications.dart @@ -25,7 +25,8 @@ Future initializeNotifications() async { alarmNotificationChannel, reminderNotificationChannel, stopwatchNotificationChannel, - timerNotificationChannel + timerNotificationChannel, + // foregroundNotificationChannel, ], // channelGroups: [alarmNotificationChannelGroup], debug: false, diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 2d2e049b..1be83cd3 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -5,23 +5,20 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/common/logic/customize_screen.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; -import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/settings/types/setting.dart'; -import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/data/timer_list_filters.dart'; import 'package:clock_app/timer/data/timer_sort_options.dart'; import 'package:clock_app/timer/logic/timer_notification.dart'; import 'package:clock_app/timer/screens/timer_fullscreen.dart'; -import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/widgets/timer_duration_picker.dart'; import 'package:clock_app/timer/widgets/timer_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +// import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:great_list_view/great_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; @@ -171,34 +168,6 @@ class _TimerScreenState extends State { // showProgressNotification(); } - bool _registerReceivePort(ReceivePort? newReceivePort) { - if (newReceivePort == null) { - return false; - } - - _closeReceivePort(); - - _receivePort = newReceivePort; - _receivePort?.listen((data) { - // if (data is int) { - // print('eventCount: $data'); - // } else if (data is String) { - // if (data == 'onNotificationPressed') { - // Navigator.of(context).pushNamed('/resume-route'); - // } - // } else if (data is DateTime) { - // print('timestamp: ${data.toString()}'); - // } - }); - - return _receivePort != null; - } - - void _closeReceivePort() { - _receivePort?.close(); - _receivePort = null; - } - @override void initState() { super.initState(); @@ -218,16 +187,6 @@ class _TimerScreenState extends State { _showNotification.addListener(update); ListenerManager.addOnChangeListener("timers", onTimerUpdate); // showProgressNotification(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - // await _requestPermissionForAndroid(); - - // You can get the previous ReceivePort without restarting the service. - if (await FlutterForegroundTask.isRunningService) { - final newReceivePort = FlutterForegroundTask.receivePort; - _registerReceivePort(newReceivePort); - } - }); } @override @@ -235,7 +194,6 @@ class _TimerScreenState extends State { _showFilters.removeListener(update); _showSort.removeListener(update); _showNotification.removeListener(update); - _closeReceivePort(); // ListenerManager.removeOnChangeListener("timers", onTimerUpdate); super.dispose(); diff --git a/pubspec.lock b/pubspec.lock index a5587009..525a1aa3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -323,10 +323,9 @@ packages: flutter_foreground_task: dependency: "direct main" description: - name: flutter_foreground_task - sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" - url: "https://pub.dev" - source: hosted + path: "../flutter_foreground_task" + relative: true + source: path version: "6.5.0" flutter_html: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index d267aad9..745f4258 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,8 @@ dependencies: ref: alarm_show_intent just_audio: ^0.9.31 awesome_notifications: ^0.9.3 + # awesome_notifications: + # path: "../awesome_notifications" audio_session: ^0.1.13 flutter_fgbg: ^0.3.0 move_to_background: ^1.0.2 @@ -78,7 +80,8 @@ dependencies: ref: main permission_handler: ^11.3.1 device_info_plus: ^10.1.0 - flutter_foreground_task: ^6.5.0 + flutter_foreground_task: + path: "../flutter_foreground_task" dev_dependencies: From b53108c6a944a8f6a5b9f45bfa23703bb597887c Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 28 Jun 2024 22:02:18 +0500 Subject: [PATCH 060/177] Fix typo in pubspec --- pubspec.lock | 8 +++++--- pubspec.yaml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 525a1aa3..fa54acb0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -323,9 +323,11 @@ packages: flutter_foreground_task: dependency: "direct main" description: - path: "../flutter_foreground_task" - relative: true - source: path + path: "." + ref: master + resolved-ref: "00ec90a97fd240029f4ee2c3ef1c1cb97177d621" + url: "https://github.com/vicolo-dev/flutter_foreground_task" + source: git version: "6.5.0" flutter_html: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 745f4258..2be7f14b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,7 +81,9 @@ dependencies: permission_handler: ^11.3.1 device_info_plus: ^10.1.0 flutter_foreground_task: - path: "../flutter_foreground_task" + git: + url: https://github.com/vicolo-dev/flutter_foreground_task + ref: master dev_dependencies: From 5af3db80f3e6374fe081cf40bb8fda74f67f44a2 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 28 Jun 2024 22:15:32 +0500 Subject: [PATCH 061/177] Rever minsdkversion --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ee517ef..08ef8d71 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,7 +53,7 @@ android { defaultConfig { applicationId "com.vicolo.chrono" - minSdkVersion 23 + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName From 13e51b91976c3071b481cb8416eb32a6c8d78732 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 28 Jun 2024 23:31:18 +0500 Subject: [PATCH 062/177] Change pubspec --- pubspec.lock | 2 +- pubspec.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fa54acb0..bdad134c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,7 +325,7 @@ packages: description: path: "." ref: master - resolved-ref: "00ec90a97fd240029f4ee2c3ef1c1cb97177d621" + resolved-ref: "6ab85aadb67e68377ec14beefe7ba7ea7fb34caa" url: "https://github.com/vicolo-dev/flutter_foreground_task" source: git version: "6.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2be7f14b..5748562c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,10 +81,11 @@ dependencies: permission_handler: ^11.3.1 device_info_plus: ^10.1.0 flutter_foreground_task: + # path: "../flutter_foreground_task" git: url: https://github.com/vicolo-dev/flutter_foreground_task ref: master - + # dev_dependencies: flutter_test: From 5dfce7f4197802bdf801c718c9cbfc9ab45a15cb Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 28 Jun 2024 23:48:04 +0500 Subject: [PATCH 063/177] Add guard for stopwatch loading --- lib/stopwatch/screens/stopwatch_screen.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index 68a3bf13..f7a026bd 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -39,8 +39,16 @@ class _StopwatchScreenState extends State { @override void initState() { super.initState(); - _stopwatch = loadListSync('stopwatches').first; + final stopwatches = loadListSync('stopwatches'); + if(stopwatches.isEmpty){ + _stopwatch = ClockStopwatch(); + saveList('stopwatches', [_stopwatch]); + } + else{ + _stopwatch = stopwatches.first; + } + _showNotificationSetting = appSettings.getGroup("Stopwatch").getSetting("Show Notification"); From 88e2e0a9b0527d0b3c0cd72ca25e21e393680d14 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 08:41:21 +0500 Subject: [PATCH 064/177] Fix alarm screen state not updating --- lib/alarm/screens/alarm_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 4e7aba13..47a48264 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -68,7 +68,7 @@ class _AlarmScreenState extends State { _showNextAlarm.addListener(update); _showSort.addListener(update); - ListenerManager.addOnChangeListener("alarms", update); + // ListenerManager.addOnChangeListener("alarms", update); nextAlarm = getNextAlarm(); @@ -81,7 +81,7 @@ class _AlarmScreenState extends State { _showFilters.removeListener(update); _showSort.removeListener(update); _showNextAlarm.removeListener(update); - ListenerManager.removeOnChangeListener("alarms", update); + // ListenerManager.removeOnChangeListener("alarms", update); super.dispose(); } From f57105b1dc15a18874a1ee59161c7fc5e60f618f Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 09:06:26 +0500 Subject: [PATCH 065/177] Add descriptions for various settings --- .../metadata/android/en-US/changelogs/251.txt | 4 ++- lib/common/widgets/fields/switch_field.dart | 28 +++++++++++++++---- lib/l10n/app_en.arb | 8 +++++- .../data/general_settings_schema.dart | 22 ++++++++++----- lib/settings/widgets/switch_setting_card.dart | 1 + 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index 2d9729ba..62b0fd43 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -1,2 +1,4 @@ +* Added option to turn on foreground notification to keep app alive * Added current lap card for stopwatch. Now you can view the current lap time in real time. -* Removed support for Android 5 (min api level now is 23/Android 6) +* Added option to show next alarm in filters + diff --git a/lib/common/widgets/fields/switch_field.dart b/lib/common/widgets/fields/switch_field.dart index f3d76b2c..d2467f4e 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,11 @@ 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 +35,19 @@ 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) + Text(widget.description, style: textTheme.bodyMedium) + ], + ), ), ), const Spacer(), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 28f18dae..8bb89105 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -706,5 +706,11 @@ "showNextAlarm": "Show Next Alarm", "@showNextAlarm": {}, "showForegroundNotification": "Show Foreground Notification", - "@showForegroundNotification": {} + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Show a persistent notification to keep app alive", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Allow notifications to be showed", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Show animations that are notpolished and might cause frame drops in low-end devices", + "@extraAnimationSettingDescription": {} } diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 750034e2..cc805423 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -107,7 +107,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Date Format", (context) => AppLocalizations.of(context)!.dateFormatSetting, dateFormatOptions, - getDescription: (context) => "How to display the dates", onChange: (context, index) async { // await HomeWidget.saveWidgetData( // "dateFormat", dateFormatOptions[index].value); @@ -118,7 +117,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Long Date Format", (context) => AppLocalizations.of(context)!.longDateFormatSetting, longDateFormatOptions, - getDescription: (context) => "How to display the dates", onChange: (context, index) async { setDigitalClockWidgetData(context); @@ -131,7 +129,6 @@ SettingGroup generalSettingsSchema = SettingGroup( "Time Format", (context) => AppLocalizations.of(context)!.timeFormatSetting, timeFormatOptions, - getDescription: (context) => "12 or 24 hour time", onChange: (context, index) async { String timeFormat = getTimeFormatString(context, timeFormatOptions[index].value); @@ -231,7 +228,13 @@ SettingGroup generalSettingsSchema = SettingGroup( ), SettingGroup("Reliability", (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, [ - SwitchSetting("Show Foreground Notification", (context) => AppLocalizations.of(context)!.showForegroundNotification, false), + SwitchSetting( + "Show Foreground Notification", + (context) => AppLocalizations.of(context)!.showForegroundNotification, + false, + getDescription: (context) => + AppLocalizations.of(context)!.showForegroundNotificationDescription, + ), SettingAction( "Ignore Battery Optimizations", (context) => @@ -261,6 +264,8 @@ SettingGroup generalSettingsSchema = SettingGroup( .notificationPermissionAlreadyGranted) }); }, + getDescription: (context) => + AppLocalizations.of(context)!.notificationPermissionDescription, ), SettingAction( "Vendor Specific", @@ -351,9 +356,12 @@ SettingGroup generalSettingsSchema = SettingGroup( // ], ), SwitchSetting( - "Extra Animations", - (context) => AppLocalizations.of(context)!.extraAnimationSetting, - false), + "Extra Animations", + (context) => AppLocalizations.of(context)!.extraAnimationSetting, + false, + getDescription: (context) => + AppLocalizations.of(context)!.extraAnimationSettingDescription, + ), ]) ], icon: FluxIcons.settings, diff --git a/lib/settings/widgets/switch_setting_card.dart b/lib/settings/widgets/switch_setting_card.dart index 7203a373..212ad546 100644 --- a/lib/settings/widgets/switch_setting_card.dart +++ b/lib/settings/widgets/switch_setting_card.dart @@ -24,6 +24,7 @@ class _SwitchSettingCardState extends State { SwitchField switchCard = SwitchField( name: widget.setting.displayName(context), value: widget.setting.value, + description: widget.setting.displayDescription(context), onChanged: (value) { setState(() { widget.setting.setValue(context, value); From 37011e979824b512110caa9ac527a1d2fb1679d4 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 12:27:18 +0500 Subject: [PATCH 066/177] Tweak UI --- fastlane/metadata/android/en-US/changelogs/251.txt | 10 +++++++++- lib/common/widgets/fields/switch_field.dart | 8 ++++---- lib/l10n/app_en.arb | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index 62b0fd43..5d5057b7 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -1,4 +1,12 @@ -* Added option to turn on foreground notification to keep app alive +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Goto 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 +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically getttin disabled + diff --git a/lib/common/widgets/fields/switch_field.dart b/lib/common/widgets/fields/switch_field.dart index d2467f4e..89729337 100644 --- a/lib/common/widgets/fields/switch_field.dart +++ b/lib/common/widgets/fields/switch_field.dart @@ -23,8 +23,6 @@ class _SwitchFieldState extends State { ThemeData theme = Theme.of(context); TextTheme textTheme = theme.textTheme; - - return Material( color: Colors.transparent, child: InkWell( @@ -38,14 +36,16 @@ class _SwitchFieldState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.name, style: textTheme.headlineMedium, ), - if (widget.description.isNotEmpty) + if (widget.description.isNotEmpty) ...[ + const SizedBox(height: 4), Text(widget.description, style: textTheme.bodyMedium) + ], ], ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8bb89105..283a9a32 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -711,6 +711,6 @@ "@showForegroundNotificationDescription": {}, "notificationPermissionDescription": "Allow notifications to be showed", "@notificationPermissionDescription": {}, - "extraAnimationSettingDescription": "Show animations that are notpolished and might cause frame drops in low-end devices", + "extraAnimationSettingDescription": "Show animations that are not polished and might cause frame drops in low-end devices", "@extraAnimationSettingDescription": {} } From 051064496c57237d2c9a2a77be3955c2e068e680 Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Wed, 26 Jun 2024 08:38:25 +0000 Subject: [PATCH 067/177] Translated using Weblate (Russian) Currently translated at 97.9% (342 of 349 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 091b51d4..8b497d52 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -29,7 +29,7 @@ "@displaySettingGroup": {}, "colorsSettingGroup": "Цвета", "@colorsSettingGroup": {}, - "useMaterialYouColorSetting": "Использовать Material You дизайн", + "useMaterialYouColorSetting": "Использовать Material You", "@useMaterialYouColorSetting": {}, "overrideAccentSetting": "Изменить цвет акцента", "@overrideAccentSetting": {}, @@ -235,7 +235,7 @@ "@deleteAllFilteredAction": {}, "editButton": "Редактировать", "@editButton": {}, - "noLapsMessage": "Еще нет кругов", + "noLapsMessage": "Кругов еще нет", "@noLapsMessage": {}, "fridayFull": "Пятница", "@fridayFull": {}, @@ -255,7 +255,7 @@ "@translateDescription": {}, "aboutSettingGroup": "О приложении", "@aboutSettingGroup": {}, - "reliabilitySettingGroup": "Надежность", + "reliabilitySettingGroup": "Стабильность", "@reliabilitySettingGroup": {}, "styleSettingGroup": "Стиль", "@styleSettingGroup": {}, @@ -674,5 +674,21 @@ "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ и ещё 1 дата} other{ и ещё {count} дат(ы)}}", "@alarmDescriptionDates": {}, "alarmDescriptionRange": "{interval, select, daily{Ежедневно} weekly{Еженедельно} other{Другое}} с {startDate} по {endDate}", - "@alarmDescriptionRange": {} + "@alarmDescriptionRange": {}, + "lessThanOneMinute": "менее 1 минуты", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Будильник прозвонит через {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Следующий: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} и {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}ч", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}м", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}с", + "@shortSecondsString": {}, + "showNextAlarm": "Показать следующий будильник", + "@showNextAlarm": {} } From 1c9cf1524e8868bd33f2ca8472f0bf14ac75d4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8D=E4=BA=88?= Date: Wed, 26 Jun 2024 11:27:51 +0000 Subject: [PATCH 068/177] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (349 of 349 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/zh_Hans/ --- lib/l10n/app_zh.arb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 66221052..814a26b8 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -674,5 +674,35 @@ "tagNamePlaceholder": "标签名称", "@tagNamePlaceholder": {}, "longDateFormatSetting": "长日期格式", - "@longDateFormatSetting": {} + "@longDateFormatSetting": {}, + "alarmRingInMessage": "闹钟将于{duration}后响铃", + "@alarmRingInMessage": {}, + "nextAlarmIn": "下一次:{duration}", + "@nextAlarmIn": {}, + "showNextAlarm": "显示下一个闹钟", + "@showNextAlarm": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "lessThanOneMinute": "不到一分钟", + "@lessThanOneMinute": {}, + "hoursString": "{count, plural, =0{} =1{1 小时} other{{count} 小时}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 分钟} other{{count} 分钟}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 秒} other{{count} 秒}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 天} other{{count} 天}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 周} other{{count} 周}}", + "@weeksString": {}, + "yearsString": "{count, plural, =0{} =1{1 年} other{{count} 年}}", + "@yearsString": {}, + "monthsString": "{count, plural, =0{} =1{1 个月} other{{count} 个月}}", + "@monthsString": {}, + "combinedTime": "{hours}与{minutes}", + "@combinedTime": {} } From f6fa93c6de3460889ee9025243dae33dacd80054 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 27 Jun 2024 12:13:43 +0000 Subject: [PATCH 069/177] Translated using Weblate (Spanish) Currently translated at 100.0% (349 of 349 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/es/ --- lib/l10n/app_es.arb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 61302315..5736ae8d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -666,5 +666,35 @@ "noTagsMessage": "No se crearon etiquetas", "@noTagsMessage": {}, "editTagLabel": "Editar etiqueta", - "@editTagLabel": {} + "@editTagLabel": {}, + "minutesString": "{count, plural, =0{} =1{1 minuto} other{{count} minutos}}", + "@minutesString": {}, + "hoursString": "{count, plural, =0{} =1{1 hora} other{{count} horas}}", + "@hoursString": {}, + "monthsString": "{count, plural, =0{} =1{1 mes} other{{count} meses}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 año} other{{count} años}}", + "@yearsString": {}, + "nextAlarmIn": "Siguiente: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} y {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}hr.", + "@shortHoursString": {}, + "alarmRingInMessage": "La alarma sonará en {duration}", + "@alarmRingInMessage": {}, + "shortMinutesString": "{minutes}min", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}seg.", + "@shortSecondsString": {}, + "secondsString": "{count, plural, =0{} =1{1 segundo} other{{count} segundos}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 día} other{{count} días}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 semana} other{{count} semanas}}", + "@weeksString": {}, + "lessThanOneMinute": "menos de 1 minuto", + "@lessThanOneMinute": {}, + "showNextAlarm": "Mostrar la siguiente alarma", + "@showNextAlarm": {} } From a2852f9bf168157266690f7fcfa806f964b50738 Mon Sep 17 00:00:00 2001 From: Stzyxh Date: Fri, 28 Jun 2024 12:58:21 +0000 Subject: [PATCH 070/177] Translated using Weblate (German) Currently translated at 97.9% (342 of 349 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/de/ --- lib/l10n/app_de.arb | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 19e1b64f..71ca6c88 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -674,5 +674,35 @@ "cityAlreadyInFavorites": "Diese Stadt ist bereits in deinen Favoriten", "@cityAlreadyInFavorites": {}, "durationPickerTitle": "Dauer wählen", - "@durationPickerTitle": {} + "@durationPickerTitle": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 week} other{{count} weeks}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 month} other{{count} months}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 year} other{{count} years}}", + "@yearsString": {}, + "lessThanOneMinute": "weniger als 1 Minute", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Der Alarm ertönt in {duration}", + "@alarmRingInMessage": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "nextAlarmIn": "Nächste: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} und {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "showNextAlarm": "Nächsten Alarm anzeigen", + "@showNextAlarm": {} } From 2c7ff875e62bc25396ae036bafeb074cb6c265c1 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 12:31:54 +0500 Subject: [PATCH 071/177] Bump pubspec --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5748562c..0c9db033 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: clock_app description: An alarm, clock, timer and stowatch app. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.5.2+25 +version: 0.5.2-beta1+25 environment: sdk: ">=2.18.6 <4.0.0" From 37db4b66998f04c102864219803afd83d38d186e Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 13:52:49 +0500 Subject: [PATCH 072/177] Update changelog --- fastlane/metadata/android/en-US/changelogs/251.txt | 2 +- fastlane/metadata/android/en-US/changelogs/252.txt | 12 ++++++++++++ fastlane/metadata/android/en-US/changelogs/253.txt | 12 ++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/252.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/253.txt diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index 5d5057b7..9dd117cb 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -7,6 +7,6 @@ 🐛 Fixes -* Fixed range schedule automatically getttin disabled +* Fixed range schedule automatically gettting disabled 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..9dd117cb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/252.txt @@ -0,0 +1,12 @@ +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Goto 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 +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically gettting disabled + + 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..9dd117cb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/253.txt @@ -0,0 +1,12 @@ +✨ Enhancements + +* Added option to turn on foreground notification to keep app alive. Goto 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 +* Updated translations + +🐛 Fixes + +* Fixed range schedule automatically gettting disabled + + From e858f347d5551a607d8ff39f95d635b25a0a3616 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 14:34:29 +0500 Subject: [PATCH 073/177] Fix timer bugs --- lib/alarm/logic/alarm_isolate.dart | 9 +-------- lib/settings/data/timer_app_settings_schema.dart | 6 +++--- lib/timer/types/timer.dart | 13 +++++++++++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 7823a6ed..f49642ca 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -197,15 +197,8 @@ void stopTimer(int scheduleId, AlarmStopAction action) async { 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/settings/data/timer_app_settings_schema.dart b/lib/settings/data/timer_app_settings_schema.dart index d7fbfdb4..b5a7b546 100644 --- a/lib/settings/data/timer_app_settings_schema.dart +++ b/lib/settings/data/timer_app_settings_schema.dart @@ -39,7 +39,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ], [ SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionSlide, + (context) => AppLocalizations.of(context)!.dismissActionAreaButtons, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => AreaNotificationAction( @@ -51,7 +51,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ), ), SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionButtons, + (context) => AppLocalizations.of(context)!.dismissActionSlide, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => SlideNotificationAction( @@ -63,7 +63,7 @@ SettingGroup timerAppSettingsSchema = SettingGroup( ), ), SelectSettingOption( - (context) => AppLocalizations.of(context)!.dismissActionAreaButtons, + (context) => AppLocalizations.of(context)!.dismissActionButtons, NotificationAction( builder: (onDismiss, onSnooze, dismissLabel, snoozeLabel) => ButtonsNotificationAction( diff --git a/lib/timer/types/timer.dart b/lib/timer/types/timer.dart index 1578490c..e9c63822 100644 --- a/lib/timer/types/timer.dart +++ b/lib/timer/types/timer.dart @@ -60,7 +60,10 @@ class ClockTimer extends CustomizableListItem { if (isRunning) { return math.max( _milliSecondsRemainingOnPause - - DateTime.now().difference(_startTime).toTimeDuration().inMilliseconds, + DateTime.now() + .difference(_startTime) + .toTimeDuration() + .inMilliseconds, 0); } else { return _milliSecondsRemainingOnPause; @@ -148,6 +151,13 @@ class ClockTimer extends CustomizableListItem { } } + Future snooze() async { + TimeDuration addedDuration = TimeDuration(minutes: addLength.floor()); + _currentDuration = addedDuration; + _milliSecondsRemainingOnPause = addedDuration.inSeconds * 1000; + await start(); + } + Future pause() async { await cancelAlarm(_id, ScheduledNotificationType.timer); _milliSecondsRemainingOnPause -= @@ -245,7 +255,6 @@ class ClockTimer extends CustomizableListItem { _state = other._state; _settings = other._settings.copy(); _id = other._id; - } @override From 29055e7d9e98787e0a5ad56a5ba86f36b6dfd6d4 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 14:55:27 +0500 Subject: [PATCH 074/177] Change timer title to include remaining time --- fastlane/metadata/android/en-US/changelogs/251.txt | 2 ++ lib/timer/logic/timer_notification.dart | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index 9dd117cb..d18837ea 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -8,5 +8,7 @@ 🐛 Fixes * Fixed range schedule automatically gettting disabled +* Fixed timer dismiss actions being mapped incorrectly +* Fixed timer add length not working correctly after timer rings diff --git a/lib/timer/logic/timer_notification.dart b/lib/timer/logic/timer_notification.dart index fe6d9729..828bbe94 100644 --- a/lib/timer/logic/timer_notification.dart +++ b/lib/timer/logic/timer_notification.dart @@ -46,9 +46,8 @@ Future updateTimerNotification(ClockTimer timer, int count) async { content: NotificationContent( id: 2, channelKey: timerNotificationChannelKey, - title: - "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}", - body: TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString(), + title: "${TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString()} - ${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}", + // body: "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}" , category: NotificationCategory.Progress, notificationLayout: NotificationLayout.ProgressBar, payload: { From d891ad43ef0bd32bfbcd3a9d6ecdb44bbcc0224b Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 16:35:43 +0500 Subject: [PATCH 075/177] Fix stopwatch --- .../metadata/android/en-US/changelogs/251.txt | 6 +++-- lib/common/utils/logger.dart | 23 +++++++++++++++++++ .../logic/stopwatch_notification.dart | 6 ++--- lib/stopwatch/screens/stopwatch_screen.dart | 22 ++++++++++-------- lib/stopwatch/types/stopwatch.dart | 2 ++ lib/timer/logic/timer_notification.dart | 2 +- pubspec.lock | 8 +++++++ pubspec.yaml | 1 + 8 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 lib/common/utils/logger.dart diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index d18837ea..117c1322 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -1,14 +1,16 @@ ✨ Enhancements -* Added option to turn on foreground notification to keep app alive. Goto General > Display > Reliability > Show Foreground Notification +* 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 * Updated translations +* Changed timer and stopwatch notification so time appears in title 🐛 Fixes -* Fixed range schedule automatically gettting disabled +* 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/lib/common/utils/logger.dart b/lib/common/utils/logger.dart new file mode 100644 index 00000000..6c35fb95 --- /dev/null +++ b/lib/common/utils/logger.dart @@ -0,0 +1,23 @@ +import 'package:logger/logger.dart'; + +var logger = Logger(); + +logDebug(String message) { + logger.d(message); +} + +logError(String message, String error) { + logger.e(message, error: error); +} + +logInfo(String message) { + logger.i(message); +} + +logTrace(String message) { + logger.t(message); +} + +logWarning(String message) { + logger.w(message); +} diff --git a/lib/stopwatch/logic/stopwatch_notification.dart b/lib/stopwatch/logic/stopwatch_notification.dart index 3ec7bb35..0ec6cf10 100644 --- a/lib/stopwatch/logic/stopwatch_notification.dart +++ b/lib/stopwatch/logic/stopwatch_notification.dart @@ -8,9 +8,9 @@ Future updateStopwatchNotification(ClockStopwatch stopwatch) async { content: NotificationContent( id: stopwatch.id, channelKey: stopwatchNotificationChannelKey, - title: 'Stopwatch', - body: - "${TimeDuration.fromMilliseconds(stopwatch.elapsedMilliseconds).toTimeString(showMilliseconds: false)} - LAP ${stopwatch.laps.length + 1}", + title: + "${TimeDuration.fromMilliseconds(stopwatch.elapsedMilliseconds).toTimeString(showMilliseconds: false)} - LAP ${stopwatch.laps.length}", + body: "Stopwatch", category: NotificationCategory.StopWatch, ), actionButtons: [ diff --git a/lib/stopwatch/screens/stopwatch_screen.dart b/lib/stopwatch/screens/stopwatch_screen.dart index f7a026bd..2d17acea 100644 --- a/lib/stopwatch/screens/stopwatch_screen.dart +++ b/lib/stopwatch/screens/stopwatch_screen.dart @@ -40,15 +40,13 @@ class _StopwatchScreenState extends State { void initState() { super.initState(); final stopwatches = loadListSync('stopwatches'); - if(stopwatches.isEmpty){ - _stopwatch = ClockStopwatch(); + if (stopwatches.isEmpty) { + _stopwatch = ClockStopwatch(); saveList('stopwatches', [_stopwatch]); + } else { + _stopwatch = stopwatches.first; } - else{ - _stopwatch = stopwatches.first; - } - _showNotificationSetting = appSettings.getGroup("Stopwatch").getSetting("Show Notification"); @@ -61,14 +59,18 @@ class _StopwatchScreenState extends State { void _handleStopwatchChange() { final newList = loadListSync('stopwatches'); + _stopwatch.copyFrom(newList.first); + if (mounted) { - newList.first.laps - .where((lap) => !_stopwatch.laps.contains(lap)) - .forEach((lap) => _listController.addItem(lap)); + // // If there are any new laps, tell the listcontroller to update the ui with them + // newList.first.laps + // .where((lap) => + // !_stopwatch.laps.map((l) => l.number).contains(lap.number)) + // .forEach((lap) => _listController.addItem(lap)); + _listController.reload(_stopwatch.laps); setState(() {}); } - _stopwatch.copyFrom(newList.first); showProgressNotification(); } diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index abb706ba..6414d6c0 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -2,6 +2,7 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; +import 'package:clock_app/common/utils/logger.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; @@ -118,6 +119,7 @@ class ClockStopwatch extends JsonSerializable { } void addLap() { + logInfo("${laps}"); if (_laps.isNotEmpty) { if (currentLapTime.inMilliseconds == 0) return; finishLap(_laps.first); diff --git a/lib/timer/logic/timer_notification.dart b/lib/timer/logic/timer_notification.dart index 828bbe94..175cc22f 100644 --- a/lib/timer/logic/timer_notification.dart +++ b/lib/timer/logic/timer_notification.dart @@ -47,7 +47,7 @@ Future updateTimerNotification(ClockTimer timer, int count) async { id: 2, channelKey: timerNotificationChannelKey, title: "${TimeDuration.fromSeconds(timer.remainingSeconds).toTimeString()} - ${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}", - // body: "${timer.label.isEmpty ? 'Timer' : timer.label}${count > 1 ? ' + ${count-1} timers' : ''}" , + body: "Timer" , category: NotificationCategory.Progress, notificationLayout: NotificationLayout.ProgressBar, payload: { diff --git a/pubspec.lock b/pubspec.lock index bdad134c..0ea12e63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -618,6 +618,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + url: "https://pub.dev" + source: hosted + version: "2.3.0" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c9db033..ec127ba3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: git: url: https://github.com/vicolo-dev/flutter_foreground_task ref: master + logger: ^2.3.0 # dev_dependencies: From 8b977c5a5abc993563327903dc0227ca774a2d3f Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 16:37:10 +0500 Subject: [PATCH 076/177] Update changelog --- fastlane/metadata/android/en-US/changelogs/251.txt | 4 +--- fastlane/metadata/android/en-US/changelogs/252.txt | 8 +++++--- fastlane/metadata/android/en-US/changelogs/253.txt | 8 +++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index 117c1322..ee40f3c6 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -3,8 +3,8 @@ * 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 -* Updated translations * Changed timer and stopwatch notification so time appears in title +* Updated translations 🐛 Fixes @@ -12,5 +12,3 @@ * 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 index 9dd117cb..ee40f3c6 100644 --- a/fastlane/metadata/android/en-US/changelogs/252.txt +++ b/fastlane/metadata/android/en-US/changelogs/252.txt @@ -1,12 +1,14 @@ ✨ Enhancements -* Added option to turn on foreground notification to keep app alive. Goto General > Display > Reliability > Show Foreground Notification +* 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 +* Changed timer and stopwatch notification so time appears in title * Updated translations 🐛 Fixes -* Fixed range schedule automatically gettting disabled - +* 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 index 9dd117cb..ee40f3c6 100644 --- a/fastlane/metadata/android/en-US/changelogs/253.txt +++ b/fastlane/metadata/android/en-US/changelogs/253.txt @@ -1,12 +1,14 @@ ✨ Enhancements -* Added option to turn on foreground notification to keep app alive. Goto General > Display > Reliability > Show Foreground Notification +* 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 +* Changed timer and stopwatch notification so time appears in title * Updated translations 🐛 Fixes -* Fixed range schedule automatically gettting disabled - +* Fixed range schedule automatically getting disabled +* Fixed timer dismiss actions being mapped incorrectly +* Fixed timer add length not working correctly after timer rings From 14cb7d75384bfc4fb174ec3f692b43f9068431d1 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 29 Jun 2024 18:19:40 +0500 Subject: [PATCH 077/177] Update changelog --- fastlane/metadata/android/en-US/changelogs/251.txt | 2 +- fastlane/metadata/android/en-US/changelogs/252.txt | 2 +- fastlane/metadata/android/en-US/changelogs/253.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/251.txt b/fastlane/metadata/android/en-US/changelogs/251.txt index ee40f3c6..ecf6e5f3 100644 --- a/fastlane/metadata/android/en-US/changelogs/251.txt +++ b/fastlane/metadata/android/en-US/changelogs/251.txt @@ -2,7 +2,7 @@ * 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 +* 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 diff --git a/fastlane/metadata/android/en-US/changelogs/252.txt b/fastlane/metadata/android/en-US/changelogs/252.txt index ee40f3c6..ecf6e5f3 100644 --- a/fastlane/metadata/android/en-US/changelogs/252.txt +++ b/fastlane/metadata/android/en-US/changelogs/252.txt @@ -2,7 +2,7 @@ * 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 +* 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 diff --git a/fastlane/metadata/android/en-US/changelogs/253.txt b/fastlane/metadata/android/en-US/changelogs/253.txt index ee40f3c6..ecf6e5f3 100644 --- a/fastlane/metadata/android/en-US/changelogs/253.txt +++ b/fastlane/metadata/android/en-US/changelogs/253.txt @@ -2,7 +2,7 @@ * 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 +* 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 From 92203462a53d82db2fc2e9a97cebf103739f9717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Karpi=C5=84ski?= Date: Fri, 23 Aug 2024 02:21:23 +0000 Subject: [PATCH 078/177] Translated using Weblate (Polish) Currently translated at 100.0% (2 of 2 strings) Translation: Chrono/Fastlane Translate-URL: https://hosted.weblate.org/projects/chrono/fastlane/pl/ --- .../android/pl-PL/full_description.txt | 35 +++++++++++++++++++ .../android/pl-PL/short_description.txt | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/pl-PL/full_description.txt 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 index 8061c454..9460d702 100644 --- a/fastlane/metadata/android/pl-PL/short_description.txt +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -1 +1 @@ -nowoczesny i potężny zegar, budzik, minutnik i stoper. +Nowoczesny i potężny zegar, budzik, minutnik i stoper. From 45b1b6e3bfb7b03d967085a65471b6f57e710956 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 25 Aug 2024 00:17:27 +0500 Subject: [PATCH 079/177] Add multiselect --- lib/alarm/screens/alarm_screen.dart | 1 + lib/clock/screens/clock_screen.dart | 1 + lib/common/logic/card_decoration.dart | 7 +- lib/common/widgets/card_container.dart | 4 +- lib/common/widgets/list/custom_list_view.dart | 166 +++++++++++++----- lib/common/widgets/list/list_item_card.dart | 16 +- .../widgets/list/persistent_list_view.dart | 5 +- lib/l10n/app_en.arb | 8 + .../data/general_settings_schema.dart | 58 ++++-- lib/settings/data/settings_schema.dart | 6 +- lib/settings/types/setting_group.dart | 7 +- 11 files changed, 211 insertions(+), 68 deletions(-) diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 47a48264..27d321e3 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -257,6 +257,7 @@ class _AlarmScreenState extends State { nextAlarm = getNextAlarm(); setState(() {}); }, + isSelectable: true, // header: getNextAlarmWidget(), listFilters: getListFilterItems(), customActions: _showFilters.value diff --git a/lib/clock/screens/clock_screen.dart b/lib/clock/screens/clock_screen.dart index b6a28a26..38668467 100644 --- a/lib/clock/screens/clock_screen.dart +++ b/lib/clock/screens/clock_screen.dart @@ -73,6 +73,7 @@ class _ClockScreenState extends State { onDelete: () => _listController.deleteItem(city)), placeholderText: "No cities added", isDuplicateEnabled: false, + isSelectable: true, ), ), ]), diff --git a/lib/common/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart index 28246179..e22c5936 100644 --- a/lib/common/logic/card_decoration.dart +++ b/lib/common/logic/card_decoration.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; BoxDecoration getCardDecoration(BuildContext context, {Color? color, bool showLightBorder = false, + bool isSelected = false, showShadow = true, elevationMultiplier = 1, blurStyle = BlurStyle.normal}) { @@ -12,7 +13,11 @@ BoxDecoration getCardDecoration(BuildContext context, 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, diff --git a/lib/common/widgets/card_container.dart b/lib/common/widgets/card_container.dart index b430f80c..d6ebb20e 100644 --- a/lib/common/widgets/card_container.dart +++ b/lib/common/widgets/card_container.dart @@ -38,7 +38,7 @@ class CardContainer extends StatelessWidget { this.onTap, this.alignment, this.showShadow = true, - + this.isSelected = false, this.showLightBorder = false, this.blurStyle = BlurStyle.normal, }); @@ -52,6 +52,7 @@ class CardContainer extends StatelessWidget { final bool showShadow; final BlurStyle blurStyle; final bool showLightBorder; + final bool isSelected; // TonalPalette primaryTonalP = toTonalPalette(_primaryColor); // primaryTonalP.get(50); // Getting the specific color @@ -74,6 +75,7 @@ class CardContainer extends StatelessWidget { decoration: getCardDecoration( context, color: cardColor, + isSelected: isSelected, showLightBorder: showLightBorder, showShadow: showShadow, elevationMultiplier: elevationMultiplier, diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 429eb7cb..8375420f 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -6,6 +6,9 @@ import 'package:clock_app/common/utils/reorderable_list_decorator.dart'; import 'package:clock_app/common/widgets/list/delete_alert_dialogue.dart'; import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; import 'package:clock_app/common/widgets/list/list_item_card.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:great_list_view/great_list_view.dart'; @@ -34,13 +37,13 @@ class CustomListView extends StatefulWidget { this.isDeleteEnabled = true, this.isDuplicateEnabled = true, this.shouldInsertOnTop = true, + this.isSelectable = false, this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], this.initialSortIndex = 0, this.onChangeSortIndex, this.header, - }); final List items; @@ -63,6 +66,7 @@ class CustomListView extends StatefulWidget { final List> sortOptions; final Function(int index)? onChangeSortIndex; final Widget? header; + final bool isSelectable; @override State createState() => _CustomListViewState(); @@ -74,11 +78,20 @@ class _CustomListViewState double _itemCardHeight = 0; final _scrollController = ScrollController(); final _controller = AnimatedListController(); - late int selectedSortIndex = widget.initialSortIndex; + late int _selectedSortIndex = widget.initialSortIndex; + late Setting _longPressActionSetting; + List _selectedIndices = []; + bool _isSelecting = false; @override void initState() { super.initState(); + + _longPressActionSetting = appSettings + .getGroup("General") + .getGroup("Interactions") + .getSetting("Long Press Action"); + widget.listController.setChangeItems(_handleChangeItems); widget.listController.setAddItem(_handleAddItem); widget.listController.setDeleteItem(_handleDeleteItem); @@ -87,7 +100,7 @@ class _CustomListViewState widget.listController.setReloadItems(_handleReloadItems); widget.listController.setClearItems(_handleClear); widget.listController.setGetItems(() => widget.items); - updateCurrentList(); + _updateCurrentList(); // widget.listController.setChangeItemWithId(_handleChangeItemWithId); } @@ -96,7 +109,7 @@ class _CustomListViewState widget.items.clear(); widget.items.addAll(items); - updateCurrentList(); + _updateCurrentList(); }); // TODO: MAN THIS SUCKS, WHY YOU GOTTA DO THIS _controller.notifyRemovedRange( @@ -104,14 +117,14 @@ class _CustomListViewState _controller.notifyInsertedRange(0, widget.items.length); } - void updateCurrentList() { - if (selectedSortIndex > widget.sortOptions.length) { - selectedSortIndex = 0; + void _updateCurrentList() { + if (_selectedSortIndex > widget.sortOptions.length) { + _selectedSortIndex = 0; } currentList.clear(); - if (selectedSortIndex != 0) { + if (_selectedSortIndex != 0) { final temp = [...widget.items]; - temp.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction); + temp.sort(widget.sortOptions[_selectedSortIndex - 1].sortFunction); currentList.addAll(temp); } else { currentList.addAll(widget.items); @@ -152,10 +165,11 @@ class _CustomListViewState _getChangeWidgetBuilder(widget.items[index])(context, index, data); bool _handleReorderItems(int oldIndex, int newIndex, Object? slot) { - if (newIndex >= widget.items.length || selectedSortIndex != 0) return false; + if (newIndex >= widget.items.length || _selectedSortIndex != 0) + return false; widget.onReorderItem?.call(widget.items[oldIndex]); widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); - updateCurrentList(); + _updateCurrentList(); widget.onModifyList?.call(); return true; @@ -168,7 +182,7 @@ class _CustomListViewState callback(widget.items); setState(() { - updateCurrentList(); + _updateCurrentList(); }); final deletedItems = List.from(initialList @@ -208,7 +222,7 @@ class _CustomListViewState setState(() { widget.items.removeWhere((element) => element.id == deletedItem.id); - updateCurrentList(); + _updateCurrentList(); }); _controller.notifyRemovedRange( @@ -226,7 +240,7 @@ class _CustomListViewState setState(() { widget.items.removeWhere((element) => element.id == item.id); - updateCurrentList(); + _updateCurrentList(); }); _controller.notifyRemovedRange( @@ -253,7 +267,7 @@ class _CustomListViewState widget.items.insert(index, item); await widget.onAddItem?.call(item); setState(() { - updateCurrentList(); + _updateCurrentList(); }); int currentListIndex = _getItemIndex(item); @@ -280,6 +294,36 @@ class _CustomListViewState duration: const Duration(milliseconds: 250), curve: Curves.easeIn); } + void _endSelection() { + setState(() { + _isSelecting = false; + _selectedIndices.clear(); + }); + _notifyChangeList(); + } + + void _startSelection(int index) { + setState(() { + _isSelecting = true; + _selectedIndices = [index]; + }); + _notifyChangeList(); + } + + void _handleSelect(int index) { + setState(() { + if (_selectedIndices.contains(index)) { + _selectedIndices.remove(index); + } else { + _selectedIndices.add(index); + } + }); + if (_selectedIndices.isEmpty) { + _endSelection(); + } + _notifyChangeList(); + } + _getItemBuilder() { return (BuildContext context, Item item, data) { for (var filter in widget.listFilters) { @@ -288,24 +332,46 @@ class _CustomListViewState return Container(); } } - return data.measuring + int index = _getItemIndex(item); + var itemWidget = data.measuring ? SizedBox(height: _itemCardHeight) : ListItemCard( key: ValueKey(item), onTap: () { - return widget.onTapItem?.call(item, _getItemIndex(item)); + return widget.onTapItem?.call(item, index); }, onDelete: widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null, onDuplicate: () => _handleDuplicateItem(item), isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, isDuplicateEnabled: widget.isDuplicateEnabled, + isSelected: _selectedIndices.contains(index), child: widget.itemBuilder(item), ); + if (widget.isSelectable && + _longPressActionSetting.value == LongPressAction.multiSelect) { + itemWidget = GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: () { + if (!_isSelecting) { + _startSelection(index); + } else { + _handleSelect(index); + } + }, + onTap: () { + if (_isSelecting) { + _handleSelect(index); + } + }, + child: AbsorbPointer(absorbing: _isSelecting, child: itemWidget), + ); + } + return itemWidget; }; } - void onFilterChange() { + void _onFilterChange() { setState(() { _notifyChangeList(); }); @@ -314,8 +380,8 @@ class _CustomListViewState List getCurrentList() { final List items = List.from(widget.items); - if (selectedSortIndex != 0) { - items.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction); + if (_selectedSortIndex != 0) { + items.sort(widget.sortOptions[_selectedSortIndex - 1].sortFunction); } return items; @@ -325,16 +391,19 @@ class _CustomListViewState Widget build(BuildContext context) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; + final isReorderable = + _longPressActionSetting.value == LongPressAction.reorder && + widget.isReorderable; - if (selectedSortIndex > widget.sortOptions.length) { - updateCurrentList(); + if (_selectedSortIndex > widget.sortOptions.length) { + _updateCurrentList(); } List getFilterChips() { List widgets = []; int activeFilterCount = widget.listFilters.where((filter) => filter.isActive).length; - if (activeFilterCount > 0) { + if (activeFilterCount > 0 || _selectedIndices.isNotEmpty) { widgets.add(ListFilterActionChip( actions: [ ListFilterAction( @@ -344,16 +413,26 @@ class _CustomListViewState for (var filter in widget.listFilters) { filter.reset(); } - onFilterChange(); + _selectedIndices.clear(); + _onFilterChange(); }, ), ...widget.customActions.map((action) => ListFilterAction( name: action.name, icon: action.icon, - action: () => action.action(widget.items - .where((item) => widget.listFilters - .every((filter) => filter.filterFunction(item))) - .toList()), + action: () { + final list = _selectedIndices.isNotEmpty + ? _selectedIndices + .map((index) => widget.items[index]) + .toList() + : widget.items; + + action.action(list + .where((item) => widget.listFilters + .every((filter) => filter.filterFunction(item))) + .toList()); + _endSelection(); + }, )), ListFilterAction( name: AppLocalizations.of(context)!.deleteAllFilteredAction, @@ -364,24 +443,31 @@ class _CustomListViewState final result = await showDeleteAlertDialogue(context); if (result == null || result == false) return; - final toRemove = List.from(widget.items.where((item) => - widget.listFilters - .every((filter) => filter.filterFunction(item)))); + final list = _selectedIndices.isNotEmpty + ? _selectedIndices + .map((index) => widget.items[index]) + .toList() + : widget.items; + final toRemove = List.from(list.where((item) => widget + .listFilters + .every((filter) => filter.filterFunction(item)))); + _endSelection(); await _handleDeleteItemList(toRemove); widget.onModifyList?.call(); }, ) ], - activeFilterCount: activeFilterCount, + activeFilterCount: + activeFilterCount + (_selectedIndices.isNotEmpty ? 1 : 0), )); } widgets.addAll(widget.listFilters - .map((filter) => getListFilterChip(filter, onFilterChange))); + .map((filter) => getListFilterChip(filter, _onFilterChange))); if (widget.sortOptions.isNotEmpty) { widgets.add( ListSortChip( - selectedIndex: selectedSortIndex, + selectedIndex: _selectedSortIndex, sortOptions: [ ListSortOption( (context) => AppLocalizations.of(context)!.defaultLabel, @@ -389,9 +475,9 @@ class _CustomListViewState ...widget.sortOptions, ], onChange: (index) => setState(() { - selectedSortIndex = index; + _selectedSortIndex = index; widget.onChangeSortIndex?.call(index); - updateCurrentList(); + _updateCurrentList(); _notifyChangeList(); }), ), @@ -417,7 +503,7 @@ class _CustomListViewState ), ), ), - if(widget.header != null) widget.header!, + if (widget.header != null) widget.header!, Expanded( flex: 1, child: Stack(children: [ @@ -452,8 +538,8 @@ class _CustomListViewState // animator: DefaultAnimatedListAnimator, listController: _controller, scrollController: _scrollController, - addLongPressReorderable: widget.isReorderable, - reorderModel: widget.isReorderable && selectedSortIndex == 0 + addLongPressReorderable: isReorderable, + reorderModel: isReorderable && _selectedSortIndex == 0 ? AnimatedListReorderModel( onReorderStart: (index, dx, dy) => true, onReorderFeedback: (int index, int dropIndex, @@ -464,7 +550,7 @@ class _CustomListViewState ) : null, reorderDecorationBuilder: - widget.isReorderable ? reorderableListDecorator : null, + isReorderable ? reorderableListDecorator : null, footer: const SizedBox(height: 64 + 80), // header: widget.header, diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index 69a87597..c1b5231b 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -16,6 +16,7 @@ class ListItemCard extends StatefulWidget { this.onInit, this.isDeleteEnabled = true, this.isDuplicateEnabled = true, + this.isSelected = false, }); final VoidCallback? onDelete; @@ -25,6 +26,7 @@ class ListItemCard extends StatefulWidget { final VoidCallback? onInit; final bool isDeleteEnabled; final bool isDuplicateEnabled; + final bool isSelected; @override State createState() => _ListItemCardState(); @@ -32,6 +34,7 @@ class ListItemCard extends StatefulWidget { class _ListItemCardState extends State> { late Setting swipeActionSetting; + late Setting longPressActionSetting; void update(dynamic value) { setState(() {}); @@ -41,8 +44,11 @@ class _ListItemCardState extends State> { void initState() { super.initState(); widget.onInit?.call(); - swipeActionSetting = - appSettings.getGroup("General").getSetting("Swipe Action"); + final interactionSettingsGroup = + appSettings.getGroup("General").getGroup("Interactions"); + swipeActionSetting = interactionSettingsGroup.getSetting("Swipe Action"); + longPressActionSetting = + interactionSettingsGroup.getSetting("Long Press Action"); swipeActionSetting.addListener(update); } @@ -55,8 +61,11 @@ class _ListItemCardState extends State> { @override Widget build(BuildContext context) { Widget innerWidget = widget.child; + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; - if ((widget.isDeleteEnabled || widget.isDuplicateEnabled) && swipeActionSetting.value == SwipeAction.cardActions) { + if ((widget.isDeleteEnabled || widget.isDuplicateEnabled) && + swipeActionSetting.value == SwipeAction.cardActions) { ActionPane startActionPane = widget.isDuplicateEnabled ? getDuplicateActionPane(widget.onDuplicate ?? () {}, context) : getDeleteActionPane(widget.onDelete ?? () {}, context); @@ -76,6 +85,7 @@ class _ListItemCardState extends State> { width: double.infinity, child: CardContainer( onTap: widget.onTap, + isSelected: widget.isSelected, child: innerWidget, ), ); diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index fb7b3b6d..16220989 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -70,13 +70,14 @@ class PersistentListView extends StatefulWidget { this.isReorderable = true, this.isDeleteEnabled = true, this.isDuplicateEnabled = true, + this.isSelectable = false, this.reloadOnPop = false, this.shouldInsertOnTop = true, this.listFilters = const [], this.customActions = const [], this.sortOptions = const [], this.header, - this.onSaveItems = null, + this.onSaveItems , // this.initialSortIndex = 0, }); @@ -92,6 +93,7 @@ class PersistentListView extends StatefulWidget { final bool isDeleteEnabled; final bool isDuplicateEnabled; final bool reloadOnPop; + final bool isSelectable; final bool shouldInsertOnTop; final Widget? header; // final int initialSortIndex; @@ -190,6 +192,7 @@ class _PersistentListViewState onModifyList: _saveItems, isReorderable: widget.isReorderable, isDeleteEnabled: widget.isDeleteEnabled, + isSelectable: widget.isSelectable, isDuplicateEnabled: widget.isDuplicateEnabled, shouldInsertOnTop: widget.shouldInsertOnTop, listFilters: widget.listFilters, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 283a9a32..68e9e2ce 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -49,6 +49,8 @@ "@durationPickerSetting": {}, "pickerRings": "Rings", "@pickerRings": {}, + "interactionsSettingGroup": "Interactions", + "@interactionsSettingGroup": {}, "swipeActionSetting": "Swipe Action", "@swipeActionSetting": {}, "swipActionCardAction": "Card Actions", @@ -59,6 +61,12 @@ "@swipActionSwitchTabs": {}, "swipeActionSwitchTabsDescription": "Swipe between tabs", "@swipeActionSwitchTabsDescription": {}, + "longPressActionSetting": "Long Press Action", + "@longPressActionSetting": {}, + "longPressReorderAction": "Reorder", + "@longPressReorderAction": {}, + "longPressSelectAction": "Multiselect", + "@longPressSelectAction": {}, "melodiesSetting": "Melodies", "@melodiesSetting": {}, "tagsSetting": "Tags", diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index cc805423..01d2da3f 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -73,6 +73,11 @@ enum SwipeAction { switchTabs, } +enum LongPressAction { + reorder, + multiSelect, +} + final timeFormatOptions = [ SelectSettingOption( (context) => AppLocalizations.of(context)!.timeFormat12, TimeFormat.h12), @@ -194,24 +199,41 @@ SettingGroup generalSettingsSchema = SettingGroup( ]), ], ), - SelectSetting( - "Swipe Action", - (context) => AppLocalizations.of(context)!.swipeActionSetting, - [ - SelectSettingOption( - (context) => AppLocalizations.of(context)!.swipActionCardAction, - SwipeAction.cardActions, - getDescription: (context) => - AppLocalizations.of(context)!.swipeActionCardActionDescription, - ), - SelectSettingOption( - (context) => AppLocalizations.of(context)!.swipActionSwitchTabs, - SwipeAction.switchTabs, - getDescription: (context) => - AppLocalizations.of(context)!.swipeActionSwitchTabsDescription, - ) - ], - ), + SettingGroup("Interactions", + (context) => AppLocalizations.of(context)!.interactionsSettingGroup, [ + SelectSetting( + "Swipe Action", + (context) => AppLocalizations.of(context)!.swipeActionSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.swipActionCardAction, + SwipeAction.cardActions, + getDescription: (context) => + AppLocalizations.of(context)!.swipeActionCardActionDescription, + ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.swipActionSwitchTabs, + SwipeAction.switchTabs, + getDescription: (context) => + AppLocalizations.of(context)!.swipeActionSwitchTabsDescription, + ) + ], + ), + SelectSetting( + "Long Press Action", + (context) => AppLocalizations.of(context)!.longPressActionSetting, + [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.longPressReorderAction, + LongPressAction.reorder, + ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.longPressSelectAction, + LongPressAction.multiSelect, + ) + ], + ), + ]), SettingPageLink( "Melodies", (context) => AppLocalizations.of(context)!.melodiesSetting, diff --git a/lib/settings/data/settings_schema.dart b/lib/settings/data/settings_schema.dart index 8accc4b6..e9706cfa 100644 --- a/lib/settings/data/settings_schema.dart +++ b/lib/settings/data/settings_schema.dart @@ -13,7 +13,9 @@ import 'package:clock_app/settings/types/setting_link.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -const int settingsSchemaVersion = 5; + +// Increment this after every schema change +const int settingsSchemaVersion = 6; SettingGroup appSettings = SettingGroup( "Settings", @@ -39,5 +41,3 @@ SettingGroup appSettings = SettingGroup( ], ); - -// Settings appSettings = Settings(settingsItems); diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart index 8d8987f0..3b5253c4 100644 --- a/lib/settings/types/setting_group.dart +++ b/lib/settings/types/setting_group.dart @@ -100,7 +100,12 @@ class SettingGroup extends SettingItem { } SettingGroup getGroup(String name) { - return _settingGroups.firstWhere((item) => item.name == name); + try { + return _settingGroups.firstWhere((item) => item.name == name); + } catch (e) { + debugPrint("Could not find setting group $name: $e"); + rethrow; + } } Setting getSettingFromPath(List path) { From 26074ce93e9f8299633fae06ce6692d43ef60526 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 25 Aug 2024 14:27:17 +0500 Subject: [PATCH 080/177] Add multiselect chips --- lib/common/widgets/list/custom_list_view.dart | 167 ++++++++++-------- lib/common/widgets/list/list_filter_chip.dart | 47 +++++ lib/l10n/app_en.arb | 4 + 3 files changed, 146 insertions(+), 72 deletions(-) diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 8375420f..b9c20edd 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -80,7 +80,7 @@ class _CustomListViewState final _controller = AnimatedListController(); late int _selectedSortIndex = widget.initialSortIndex; late Setting _longPressActionSetting; - List _selectedIndices = []; + List _selectedIds = []; bool _isSelecting = false; @override @@ -297,33 +297,40 @@ class _CustomListViewState void _endSelection() { setState(() { _isSelecting = false; - _selectedIndices.clear(); + _selectedIds.clear(); }); _notifyChangeList(); } - void _startSelection(int index) { + void _startSelection(Item item) { setState(() { _isSelecting = true; - _selectedIndices = [index]; + _selectedIds = [item.id]; }); _notifyChangeList(); } - void _handleSelect(int index) { + void _handleSelect(Item item) { setState(() { - if (_selectedIndices.contains(index)) { - _selectedIndices.remove(index); + if (_selectedIds.contains(item.id)) { + _selectedIds.remove(item.id); } else { - _selectedIndices.add(index); + _selectedIds.add(item.id); } }); - if (_selectedIndices.isEmpty) { + if (_selectedIds.isEmpty) { _endSelection(); } _notifyChangeList(); } + void _handleSelectAll() { + setState(() { + _selectedIds = widget.items.map((e) => e.id).toList(); + }); + _notifyChangeList(); + } + _getItemBuilder() { return (BuildContext context, Item item, data) { for (var filter in widget.listFilters) { @@ -345,7 +352,7 @@ class _CustomListViewState onDuplicate: () => _handleDuplicateItem(item), isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, isDuplicateEnabled: widget.isDuplicateEnabled, - isSelected: _selectedIndices.contains(index), + isSelected: _selectedIds.contains(item.id), child: widget.itemBuilder(item), ); if (widget.isSelectable && @@ -354,14 +361,14 @@ class _CustomListViewState behavior: HitTestBehavior.opaque, onLongPress: () { if (!_isSelecting) { - _startSelection(index); + _startSelection(item); } else { - _handleSelect(index); + _handleSelect(item); } }, onTap: () { if (_isSelecting) { - _handleSelect(index); + _handleSelect(item); } }, child: AbsorbPointer(absorbing: _isSelecting, child: itemWidget), @@ -377,7 +384,7 @@ class _CustomListViewState }); } - List getCurrentList() { + List _getCurrentList() { final List items = List.from(widget.items); if (_selectedSortIndex != 0) { @@ -387,6 +394,12 @@ class _CustomListViewState return items; } + List _getActionableItems() { + return _isSelecting + ? widget.items.where((item) => _selectedIds.contains(item.id)).toList() + : widget.items; + } + @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); @@ -403,64 +416,74 @@ class _CustomListViewState List widgets = []; int activeFilterCount = widget.listFilters.where((filter) => filter.isActive).length; - if (activeFilterCount > 0 || _selectedIndices.isNotEmpty) { - widgets.add(ListFilterActionChip( - actions: [ - ListFilterAction( - name: AppLocalizations.of(context)!.clearFiltersAction, - icon: Icons.clear_rounded, - action: () { - for (var filter in widget.listFilters) { - filter.reset(); - } - _selectedIndices.clear(); - _onFilterChange(); - }, - ), - ...widget.customActions.map((action) => ListFilterAction( - name: action.name, - icon: action.icon, - action: () { - final list = _selectedIndices.isNotEmpty - ? _selectedIndices - .map((index) => widget.items[index]) - .toList() - : widget.items; - - action.action(list - .where((item) => widget.listFilters - .every((filter) => filter.filterFunction(item))) - .toList()); - _endSelection(); - }, - )), - ListFilterAction( - name: AppLocalizations.of(context)!.deleteAllFilteredAction, - icon: Icons.delete_rounded, - color: colorScheme.error, - action: () async { - Navigator.pop(context); - final result = await showDeleteAlertDialogue(context); - if (result == null || result == false) return; - - final list = _selectedIndices.isNotEmpty - ? _selectedIndices - .map((index) => widget.items[index]) - .toList() - : widget.items; - final toRemove = List.from(list.where((item) => widget - .listFilters - .every((filter) => filter.filterFunction(item)))); - _endSelection(); - await _handleDeleteItemList(toRemove); - - widget.onModifyList?.call(); - }, - ) - ], - activeFilterCount: - activeFilterCount + (_selectedIndices.isNotEmpty ? 1 : 0), - )); + if (activeFilterCount > 0 || _isSelecting) { + widgets.add( + ListFilterActionChip( + actions: [ + ListFilterAction( + name: AppLocalizations.of(context)!.clearFiltersAction, + icon: Icons.clear_rounded, + action: () { + for (var filter in widget.listFilters) { + filter.reset(); + } + _endSelection(); + _onFilterChange(); + }, + ), + ...widget.customActions.map((action) => ListFilterAction( + name: action.name, + icon: action.icon, + action: () { + final list = _getActionableItems(); + + action.action(list + .where((item) => widget.listFilters + .every((filter) => filter.filterFunction(item))) + .toList()); + _endSelection(); + }, + )), + ListFilterAction( + name: AppLocalizations.of(context)!.deleteAllFilteredAction, + icon: Icons.delete_rounded, + color: colorScheme.error, + action: () async { + Navigator.pop(context); + final result = await showDeleteAlertDialogue(context); + if (result == null || result == false) return; + + final list = _getActionableItems(); + final toRemove = List.from(list.where((item) => widget + .listFilters + .every((filter) => filter.filterFunction(item)))); + _endSelection(); + await _handleDeleteItemList(toRemove); + + widget.onModifyList?.call(); + }, + ) + ], + activeFilterCount: activeFilterCount + (_isSelecting ? 1 : 0), + ), + ); + } + if (_isSelecting) { + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)! + .selectionStatus(_selectedIds.length), + icon: Icons.clear_rounded, + onTap: _endSelection, + ), + ); + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)!.selectAll, + icon: Icons.select_all, + onTap: _handleSelectAll, + ), + ); } widgets.addAll(widget.listFilters .map((filter) => getListFilterChip(filter, _onFilterChange))); diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 38bb2890..901954c8 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -44,6 +44,53 @@ class ListFilterChip extends StatelessWidget { } } +class ListButtonChip extends StatelessWidget { + const ListButtonChip({ + super.key, + required this.label, + this.onTap, + required this.icon, + }); + + final String label; + final IconData icon; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + return CardContainer( + color: colorScheme.surface, + onTap: onTap, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 10.0, right: 6.0, top: 6.0, bottom: 6.0), + child: Icon( + icon, + color: colorScheme.onSurface, + size: 20, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + label, + style: textTheme.headlineSmall?.copyWith( + color: colorScheme.onSurface, + ), + ), + ), + ], + ), + ); + } +} + class ListFilterActionChip extends StatelessWidget { const ListFilterActionChip({ super.key, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 68e9e2ce..4399a109 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -417,6 +417,10 @@ "@pausedTimerFilter": {}, "stoppedTimerFilter": "Stopped", "@stoppedTimerFilter": {}, + "selectionStatus": "{n} selected", + "@selectionStatus": {}, + "selectAll": "Select all", + "@selectAll": {}, "sortGroup": "Sort", "@sortGroup": {}, "defaultLabel": "Default", From 90d60f3dc60dd529a5558be4c4a96ea3b313f8db Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 25 Aug 2024 14:30:51 +0500 Subject: [PATCH 081/177] Add multiselect to other screens --- lib/settings/screens/ringtones_screen.dart | 1 + lib/settings/screens/tags_screen.dart | 1 + lib/timer/screens/timer_screen.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index c34549f3..591fa25c 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -75,6 +75,7 @@ class _RingtonesScreenState extends State { isDuplicateEnabled: false, placeholderText: "No melodies", reloadOnPop: true, + isSelectable: true, ), ), ], diff --git a/lib/settings/screens/tags_screen.dart b/lib/settings/screens/tags_screen.dart index eddf2a98..36bff289 100644 --- a/lib/settings/screens/tags_screen.dart +++ b/lib/settings/screens/tags_screen.dart @@ -72,6 +72,7 @@ class _TagsScreenState extends State { // onDeleteItem: _handleDeleteTimer, placeholderText: "No tags created", reloadOnPop: true, + isSelectable: true, ), ), ], diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 1be83cd3..847c448b 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -347,6 +347,7 @@ class _TimerScreenState extends State { // _listController.changeItems((item) {}); }, onDeleteItem: _onDeleteTimer, + isSelectable: true, placeholderText: AppLocalizations.of(context)!.noTimerMessage, reloadOnPop: true, listFilters: _showFilters.value ? timerListFilters : [], From 3440114e656cc28521a188a9fc76cf0dd9e22de7 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 25 Aug 2024 15:13:07 +0500 Subject: [PATCH 082/177] Fix card colors --- lib/common/widgets/list/custom_list_view.dart | 12 ++++++++++++ lib/common/widgets/list/list_filter_chip.dart | 1 - lib/settings/data/general_settings_schema.dart | 10 +++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index b9c20edd..09b92d88 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -92,6 +92,8 @@ class _CustomListViewState .getGroup("Interactions") .getSetting("Long Press Action"); + _longPressActionSetting.addListener(_handleUpdateSettings); + widget.listController.setChangeItems(_handleChangeItems); widget.listController.setAddItem(_handleAddItem); widget.listController.setDeleteItem(_handleDeleteItem); @@ -104,6 +106,16 @@ class _CustomListViewState // widget.listController.setChangeItemWithId(_handleChangeItemWithId); } + @override + void dispose() { + _longPressActionSetting.removeListener(_handleUpdateSettings); + super.dispose(); + } + + void _handleUpdateSettings(dynamic value) { + setState(() {}); + } + void _handleReloadItems(List items) { setState(() { widget.items.clear(); diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 901954c8..5a1ba9aa 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -63,7 +63,6 @@ class ListButtonChip extends StatelessWidget { TextTheme textTheme = theme.textTheme; return CardContainer( - color: colorScheme.surface, onTap: onTap, child: Row( children: [ diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 01d2da3f..2fdcf066 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -223,15 +223,15 @@ SettingGroup generalSettingsSchema = SettingGroup( "Long Press Action", (context) => AppLocalizations.of(context)!.longPressActionSetting, [ + SelectSettingOption( + (context) => AppLocalizations.of(context)!.longPressSelectAction, + LongPressAction.multiSelect, + ), SelectSettingOption( (context) => AppLocalizations.of(context)!.longPressReorderAction, LongPressAction.reorder, ), - SelectSettingOption( - (context) => AppLocalizations.of(context)!.longPressSelectAction, - LongPressAction.multiSelect, - ) - ], + ], ), ]), SettingPageLink( From 62f328a3ec6193ba53303da24a5ec55eaaa85007 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 25 Aug 2024 23:56:17 +0500 Subject: [PATCH 083/177] Add backup options --- lib/common/widgets/list/custom_list_view.dart | 5 +- lib/settings/data/backup_settings_schema.dart | 60 ++--- lib/settings/screens/backup_screen.dart | 216 ++++++++++++++++++ 3 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 lib/settings/screens/backup_screen.dart diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 09b92d88..68337f18 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -113,7 +113,7 @@ class _CustomListViewState } void _handleUpdateSettings(dynamic value) { - setState(() {}); + _endSelection(); } void _handleReloadItems(List items) { @@ -177,8 +177,9 @@ class _CustomListViewState _getChangeWidgetBuilder(widget.items[index])(context, index, data); bool _handleReorderItems(int oldIndex, int newIndex, Object? slot) { - if (newIndex >= widget.items.length || _selectedSortIndex != 0) + if (newIndex >= widget.items.length || _selectedSortIndex != 0) { return false; + } widget.onReorderItem?.call(widget.items[oldIndex]); widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); _updateCurrentList(); diff --git a/lib/settings/data/backup_settings_schema.dart b/lib/settings/data/backup_settings_schema.dart index e8e1e6b5..a006ea26 100644 --- a/lib/settings/data/backup_settings_schema.dart +++ b/lib/settings/data/backup_settings_schema.dart @@ -4,8 +4,10 @@ import 'dart:typed_data'; import 'package:clock_app/app.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/screens/backup_screen.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:clock_app/settings/types/setting_link.dart'; import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/material.dart'; import 'package:pick_or_save/pick_or_save.dart'; @@ -22,34 +24,36 @@ SettingGroup backupSettingsSchema = SettingGroup( "Settings", (context) => AppLocalizations.of(context)!.settingsTitle, [ - SettingAction( - "Export", - (context) => AppLocalizations.of(context)!.exportSettingsSetting, - (context) async { - saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); - }, - searchTags: ["settings", "export", "backup", "save"], - getDescription: (context) => - AppLocalizations.of(context)!.exportSettingsSettingDescription, - ), - SettingAction( - "Import", - (context) => AppLocalizations.of(context)!.importSettingsSetting, - (context) async { - loadBackupFile( - (data) async { - appSettings.loadValueFromJson(json.decode(data)); - appSettings.callAllListeners(); - App.refreshTheme(context); - await appSettings.save(); - if (context.mounted) setDigitalClockWidgetData(context); - }, - ); - }, - searchTags: ["settings", "import", "backup", "load"], - getDescription: (context) => - AppLocalizations.of(context)!.importSettingsSettingDescription, - ), + SettingPageLink("Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, BackupScreen()), + SettingPageLink("Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, BackupScreen()), + // SettingAction( + // "Export", + // (context) => AppLocalizations.of(context)!.exportSettingsSetting, + // (context) async { + // saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); + // }, + // searchTags: ["settings", "export", "backup", "save"], + // getDescription: (context) => + // AppLocalizations.of(context)!.exportSettingsSettingDescription, + // ), + // SettingAction( + // "Import", + // (context) => AppLocalizations.of(context)!.importSettingsSetting, + // (context) async { + // loadBackupFile( + // (data) async { + // appSettings.loadValueFromJson(json.decode(data)); + // appSettings.callAllListeners(); + // App.refreshTheme(context); + // await appSettings.save(); + // if (context.mounted) setDigitalClockWidgetData(context); + // }, + // ); + // }, + // searchTags: ["settings", "import", "backup", "load"], + // getDescription: (context) => + // AppLocalizations.of(context)!.importSettingsSettingDescription, + // ), ], ), ], diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart new file mode 100644 index 00000000..ad2be5b2 --- /dev/null +++ b/lib/settings/screens/backup_screen.dart @@ -0,0 +1,216 @@ +import 'dart:convert'; + +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/app.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:clock_app/settings/widgets/settings_top_bar.dart'; +import 'package:clock_app/theme/types/color_scheme.dart'; +import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:clock_app/timer/types/timer.dart'; +import 'package:clock_app/widgets/logic/update_widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingCheckBox extends StatelessWidget { + const SettingCheckBox( + {super.key, + required this.settingItem, + required this.isChecked, + required this.onChanged}); + + final bool isChecked; + final void Function(bool?) onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Checkbox( + // checkColor: Colors.white, + // fillColor: MaterialStateProperty.resolveWith(getColor), + value: isChecked, + onChanged: onChanged, + ), + Text( + settingItem.displayName(context), + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ); + } +} + +class BackupExportScreen extends StatefulWidget { + const BackupExportScreen({ + super.key, + required this.settingGroup, + required this.onRestore, + }); + + final SettingGroup settingGroup; + final void Function() onRestore; + + @override + State createState() => _BackupExportScreenState(); +} + +class BackupOption { + final String Function(BuildContext context) getName; + final String key; + final dynamic Function() encode; + final Function(BuildContext context, dynamic value) decode; + bool selected = true; + + BackupOption(this.key, this.getName, + {required this.encode, required this.decode}); +} + +final backupOptions = [ + BackupOption( + "settings", + (context) => AppLocalizations.of(context)!.settings, + encode: () async { + return json.encode(appSettings.valueToJson()); + }, + decode: (context, value) async { + appSettings.loadValueFromJson(json.decode(value)); + appSettings.callAllListeners(); + App.refreshTheme(context); + await appSettings.save(); + if (context.mounted) { + setDigitalClockWidgetData(context); + } + }, + ), + BackupOption( + "color_schemes", + (context) => AppLocalizations.of(context)!.colorSchemeSetting, + encode: () async { + List colorSchemes = await loadList("color_schemes"); + List customColorSchemes = + colorSchemes.where((scheme) => !scheme.isDefault).toList(); + return json.encode(customColorSchemes); + }, + decode: (context, value) async { + List colorSchemes = + await loadList("color_schemes"); + await saveList( + "color_schemes", [...json.decode(value), ...colorSchemes]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "style_themes", + (context) => AppLocalizations.of(context)!.styleThemeSetting, + encode: () async { + List styleThemes = await loadList("style_themes"); + List customThemes = + styleThemes.where((scheme) => !scheme.isDefault).toList(); + return json.encode(customThemes); + }, + decode: (context, value) async { + List styleThemes = + await loadList("style_themes"); + await saveList( + "style_themes", [...json.decode(value), ...styleThemes]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "alarms", + (context) => AppLocalizations.of(context)!.alarmTitle, + encode: () async { + return json.encode(await loadList("alarms")); + }, + decode: (context, value) async { + await saveList("alarms", [...json.decode(value), ...await loadList("alarms")]); + await updateAlarms("Updated alarms on importing backup"); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "timers", + (context) => AppLocalizations.of(context)!.alarmTitle, + encode: () async { + return json.encode(await loadList("timers")); + }, + decode: (context, value) async { + await saveList("timers", [...json.decode(value), ...await loadList("timers")]); + await updateTimers("Updated timers on importing backup"); + if (context.mounted) App.refreshTheme(context); + }, + ), + ]; + +class _BackupExportScreenState extends State { + bool settings = false; + late final Map _settingsToRestore = { + for (var settingItem in widget.settingGroup.settingItems) + settingItem.id: true + }; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: SettingsTopBar( + title: AppLocalizations.of(context)!.restoreSettingGroup, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + CardContainer( + color: Theme.of(context).colorScheme.primary, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(AppLocalizations.of(context)!.resetButton, + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + )), + ], + ), + ), + onTap: () { + widget.settingGroup + .restoreDefaults(context, _settingsToRestore); + widget.onRestore(); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ...widget.settingGroup.settingItems.map( + (settingItem) => SettingCheckBox( + settingItem: settingItem, + isChecked: _settingsToRestore[settingItem.id] ?? false, + onChanged: (bool? value) { + setState(() { + _settingsToRestore[settingItem.id] = value!; + }); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} From 1567d7085d112f6ce4dcddba2c3f9f76af690174 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 26 Aug 2024 00:14:48 +0500 Subject: [PATCH 084/177] Change id generation --- lib/alarm/types/alarm_event.dart | 4 +- lib/alarm/types/alarm_runner.dart | 10 ++-- lib/alarm/types/alarm_task.dart | 9 +-- lib/clock/types/city.dart | 8 +-- lib/common/types/file_item.dart | 9 ++- lib/common/types/list_filter.dart | 3 +- lib/common/types/tag.dart | 7 ++- lib/common/utils/id.dart | 5 ++ lib/settings/data/backup_settings_schema.dart | 60 +++++++++---------- lib/settings/screens/backup_screen.dart | 23 ++++--- lib/stopwatch/types/stopwatch.dart | 10 ++-- lib/theme/types/theme_item.dart | 10 ++-- lib/timer/types/timer.dart | 9 +-- lib/timer/types/timer_preset.dart | 10 ++-- 14 files changed, 91 insertions(+), 86 deletions(-) create mode 100644 lib/common/utils/id.dart 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..5043ba2d 100644 --- a/lib/alarm/types/alarm_runner.dart +++ b/lib/alarm/types/alarm_runner.dart @@ -1,17 +1,17 @@ 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 { @@ -27,10 +27,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/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/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..7c60f7e4 100644 --- a/lib/common/types/list_filter.dart +++ b/lib/common/types/list_filter.dart @@ -1,5 +1,6 @@ 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:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -37,7 +38,7 @@ 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; diff --git a/lib/common/types/tag.dart b/lib/common/types/tag.dart index 9a921aeb..d665fbfe 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; diff --git a/lib/common/utils/id.dart b/lib/common/utils/id.dart new file mode 100644 index 00000000..ae54b3b1 --- /dev/null +++ b/lib/common/utils/id.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +int getId() { + return UniqueKey().hashCode + DateTime.now().microsecondsSinceEpoch; +} diff --git a/lib/settings/data/backup_settings_schema.dart b/lib/settings/data/backup_settings_schema.dart index a006ea26..956facf0 100644 --- a/lib/settings/data/backup_settings_schema.dart +++ b/lib/settings/data/backup_settings_schema.dart @@ -24,36 +24,36 @@ SettingGroup backupSettingsSchema = SettingGroup( "Settings", (context) => AppLocalizations.of(context)!.settingsTitle, [ - SettingPageLink("Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, BackupScreen()), - SettingPageLink("Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, BackupScreen()), - // SettingAction( - // "Export", - // (context) => AppLocalizations.of(context)!.exportSettingsSetting, - // (context) async { - // saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); - // }, - // searchTags: ["settings", "export", "backup", "save"], - // getDescription: (context) => - // AppLocalizations.of(context)!.exportSettingsSettingDescription, - // ), - // SettingAction( - // "Import", - // (context) => AppLocalizations.of(context)!.importSettingsSetting, - // (context) async { - // loadBackupFile( - // (data) async { - // appSettings.loadValueFromJson(json.decode(data)); - // appSettings.callAllListeners(); - // App.refreshTheme(context); - // await appSettings.save(); - // if (context.mounted) setDigitalClockWidgetData(context); - // }, - // ); - // }, - // searchTags: ["settings", "import", "backup", "load"], - // getDescription: (context) => - // AppLocalizations.of(context)!.importSettingsSettingDescription, - // ), + // SettingPageLink("Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, BackupScreen()), + // SettingPageLink("Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, BackupScreen()), + SettingAction( + "Export", + (context) => AppLocalizations.of(context)!.exportSettingsSetting, + (context) async { + saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); + }, + searchTags: ["settings", "export", "backup", "save"], + getDescription: (context) => + AppLocalizations.of(context)!.exportSettingsSettingDescription, + ), + SettingAction( + "Import", + (context) => AppLocalizations.of(context)!.importSettingsSetting, + (context) async { + loadBackupFile( + (data) async { + appSettings.loadValueFromJson(json.decode(data)); + appSettings.callAllListeners(); + App.refreshTheme(context); + await appSettings.save(); + if (context.mounted) setDigitalClockWidgetData(context); + }, + ); + }, + searchTags: ["settings", "import", "backup", "load"], + getDescription: (context) => + AppLocalizations.of(context)!.importSettingsSettingDescription, + ), ], ), ], diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index ad2be5b2..b7ff0b16 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -16,14 +16,12 @@ import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class SettingCheckBox extends StatelessWidget { - const SettingCheckBox( +class BackupOptionCheckBox extends StatelessWidget { + const BackupOptionCheckBox( {super.key, - required this.settingItem, - required this.isChecked, - required this.onChanged}); + required this.option, required this.onChanged}); - final bool isChecked; + final BackupOption option; final void Function(bool?) onChanged; @override @@ -33,11 +31,11 @@ class SettingCheckBox extends StatelessWidget { Checkbox( // checkColor: Colors.white, // fillColor: MaterialStateProperty.resolveWith(getColor), - value: isChecked, + value: option.selected, onChanged: onChanged, ), Text( - settingItem.displayName(context), + option.getName(context), style: Theme.of(context).textTheme.headlineMedium, ), ], @@ -195,13 +193,12 @@ class _BackupExportScreenState extends State { }, ), const SizedBox(height: 16), - ...widget.settingGroup.settingItems.map( - (settingItem) => SettingCheckBox( - settingItem: settingItem, - isChecked: _settingsToRestore[settingItem.id] ?? false, + ...backupOptions.map( + (option) => BackupOptionCheckBox( + option: option, onChanged: (bool? value) { setState(() { - _settingsToRestore[settingItem.id] = value!; + option.selected = value ?? false; }); }, ), diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index 6414d6c0..14f4f3c1 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -1,11 +1,11 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/common/utils/logger.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; -import 'package:flutter/material.dart'; // All time units are in milliseconds class ClockStopwatch extends JsonSerializable { @@ -54,7 +54,7 @@ class ClockStopwatch extends JsonSerializable { } ClockStopwatch() - : _id = UniqueKey().hashCode, + : _id = getId(), _elapsedMillisecondsOnPause = 0, _startTime = DateTime(0), _state = TimerState.stopped; @@ -63,7 +63,7 @@ class ClockStopwatch extends JsonSerializable { : _elapsedMillisecondsOnPause = 0, _startTime = DateTime(0), _state = TimerState.stopped, - _id = UniqueKey().hashCode; + _id = getId(); copyFrom(ClockStopwatch stopwatch) { _elapsedMillisecondsOnPause = stopwatch._elapsedMillisecondsOnPause; @@ -153,7 +153,7 @@ class ClockStopwatch extends JsonSerializable { ClockStopwatch.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } _elapsedMillisecondsOnPause = json['elapsedMillisecondsOnPause'] ?? 0; @@ -163,7 +163,7 @@ class ClockStopwatch extends JsonSerializable { _state = TimerState.values.firstWhere( (e) => e.toString() == (json['state'] ?? ''), orElse: () => TimerState.stopped); - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); // _finishedLaps = []; _laps = listFromString(json['laps'] ?? '[]'); updateFastestAndSlowestLap(); diff --git a/lib/theme/types/theme_item.dart b/lib/theme/types/theme_item.dart index e33fe42f..c8a26861 100644 --- a/lib/theme/types/theme_item.dart +++ b/lib/theme/types/theme_item.dart @@ -1,7 +1,7 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/settings/types/setting_group.dart'; -import 'package:flutter/material.dart'; abstract class ThemeItem extends CustomizableListItem { late int _id; @@ -9,12 +9,12 @@ abstract class ThemeItem extends CustomizableListItem { bool _isDefault = false; ThemeItem(SettingGroup defaultSettings, bool isDefault, [int? id]) - : _id = id ?? UniqueKey().hashCode, + : _id = id ?? getId(), _settings = defaultSettings, _isDefault = isDefault; ThemeItem.from(ThemeItem themeItem) - : _id = UniqueKey().hashCode, + : _id = getId(), _isDefault = false, _settings = themeItem.settings.copy(); @@ -45,10 +45,10 @@ abstract class ThemeItem extends CustomizableListItem { ThemeItem.fromJson(Json json, SettingGroup settings) : _settings = settings { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); _isDefault = json['isDefault'] ?? false; settings.loadValueFromJson(json['settings']); } diff --git a/lib/timer/types/timer.dart b/lib/timer/types/timer.dart index e9c63822..87f8ca2f 100644 --- a/lib/timer/types/timer.dart +++ b/lib/timer/types/timer.dart @@ -5,6 +5,7 @@ import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; @@ -80,7 +81,7 @@ class ClockTimer extends CustomizableListItem { TimerState get state => _state; ClockTimer(this._duration) - : _id = UniqueKey().hashCode, + : _id = getId(), _currentDuration = TimeDuration.from(_duration), _milliSecondsRemainingOnPause = _duration.inSeconds * 1000, _startTime = DateTime(0), @@ -93,7 +94,7 @@ class ClockTimer extends CustomizableListItem { _startTime = DateTime(0), _state = TimerState.stopped, _settings = timer._settings.copy(), - _id = UniqueKey().hashCode; + _id = getId(); void setSetting(BuildContext context, String name, dynamic value) { _settings.getSetting(name).setValue(context, value); @@ -221,7 +222,7 @@ class ClockTimer extends CustomizableListItem { ClockTimer.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } _duration = TimeDuration.fromSeconds(json['duration'] ?? 0); @@ -233,7 +234,7 @@ class ClockTimer extends CustomizableListItem { : DateTime.now(); _state = TimerState.values.firstWhere((e) => e.toString() == json['state'], orElse: () => TimerState.stopped); - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); _settings = SettingGroup( "Timer Settings", (context) => "Timer Settings", diff --git a/lib/timer/types/timer_preset.dart b/lib/timer/types/timer_preset.dart index 95fe5e94..4a15b3a6 100644 --- a/lib/timer/types/timer_preset.dart +++ b/lib/timer/types/timer_preset.dart @@ -1,13 +1,13 @@ 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/timer/types/time_duration.dart'; -import 'package:flutter/foundation.dart'; class TimerPreset extends ListItem { late int _id; String name = "Preset"; TimeDuration duration = const TimeDuration(minutes: 5); - TimerPreset(this.name, this.duration) : _id = UniqueKey().hashCode; + TimerPreset(this.name, this.duration) : _id = getId(); @override int get id => _id; @@ -15,7 +15,7 @@ class TimerPreset extends ListItem { bool get isDeletable => true; TimerPreset.from(TimerPreset preset) - : _id = UniqueKey().hashCode, + : _id = getId(), name = preset.name, duration = TimeDuration.from(preset.duration); @@ -28,10 +28,10 @@ class TimerPreset extends ListItem { TimerPreset.fromJson(Json json) { if (json == null) { - _id = UniqueKey().hashCode; + _id = getId(); return; } - _id = json['id'] ?? UniqueKey().hashCode; + _id = json['id'] ?? getId(); name = json['name'] ?? "Preset"; duration = TimeDuration.fromJson(json['duration']); } From 5044ce90d2f63979259430fc92b99c57208f5b9f Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 26 Aug 2024 10:56:09 +0500 Subject: [PATCH 085/177] Revert id generation --- lib/common/utils/id.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/common/utils/id.dart b/lib/common/utils/id.dart index ae54b3b1..36c50288 100644 --- a/lib/common/utils/id.dart +++ b/lib/common/utils/id.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; int getId() { - return UniqueKey().hashCode + DateTime.now().microsecondsSinceEpoch; + return UniqueKey().hashCode; } From 2ded5440e2bf0eb18217c4aee01a183ebae78e98 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 27 Aug 2024 00:41:50 +0500 Subject: [PATCH 086/177] Add more backup options --- lib/common/utils/snackbar.dart | 11 +- lib/settings/data/backup_options.dart | 157 +++++++++++ lib/settings/data/backup_settings_schema.dart | 93 +++---- lib/settings/logic/backup.dart | 30 +++ lib/settings/screens/backup_screen.dart | 252 ++++++++---------- lib/settings/types/backup_option.dart | 14 + lib/settings/widgets/settings_top_bar.dart | 12 +- 7 files changed, 370 insertions(+), 199 deletions(-) create mode 100644 lib/settings/data/backup_options.dart create mode 100644 lib/settings/logic/backup.dart create mode 100644 lib/settings/types/backup_option.dart diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index 267c2dfd..18c3a002 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -2,13 +2,16 @@ import 'package:clock_app/settings/data/settings_schema.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; ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context) - .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar)); + .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar, color:color)); } -SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) { +SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false, Color? color}) { double left = 20; double right = 20; double bottom = 12; @@ -44,6 +47,7 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) { constraints: const BoxConstraints(minHeight: 28), child: Container( alignment: Alignment.centerLeft, + color: color, // height: 28, child: Text(text), ), @@ -53,6 +57,7 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) { right: right, bottom: bottom, ), + elevation: 2, dismissDirection: DismissDirection.none, ); diff --git a/lib/settings/data/backup_options.dart b/lib/settings/data/backup_options.dart new file mode 100644 index 00000000..0914d7b4 --- /dev/null +++ b/lib/settings/data/backup_options.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; + +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/alarm/types/alarm.dart'; +import 'package:clock_app/app.dart'; +import 'package:clock_app/clock/types/city.dart'; +import 'package:clock_app/common/utils/json_serialize.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/backup_option.dart'; +import 'package:clock_app/theme/types/color_scheme.dart'; +import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:clock_app/timer/types/timer.dart'; +import 'package:clock_app/timer/types/timer_preset.dart'; +import 'package:clock_app/widgets/logic/update_widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +// Order of BackupOption matters +// tags should be before alarms and timers +// color_schemes and style_themes should be before settings +final backupOptions = [ + BackupOption( + "tags", + (context) => AppLocalizations.of(context)!.presetsSetting, + encode: () async { + return await loadTextFile("tags"); + }, + decode: (context, value) async { + await saveList("tags", [ + ...listFromString(value), + ...await loadList("tags") + ]); + }, + ), + BackupOption( + "color_schemes", + (context) => AppLocalizations.of(context)!.colorSchemeSetting, + encode: () async { + List colorSchemes = + await loadList("color_schemes"); + List customColorSchemes = + colorSchemes.where((scheme) => !scheme.isDefault).toList(); + return listToString(customColorSchemes); + }, + decode: (context, value) async { + await saveList("color_schemes", [ + ...listFromString(value), + ...await loadList("color_schemes") + ]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "style_themes", + (context) => AppLocalizations.of(context)!.styleThemeSetting, + encode: () async { + List styleThemes = await loadList("style_themes"); + List customThemes = + styleThemes.where((scheme) => !scheme.isDefault).toList(); + return listToString(customThemes); + }, + decode: (context, value) async { + await saveList("style_themes", [ + ...listFromString(value), + ...await loadList("style_themes") + ]); + if (context.mounted) App.refreshTheme(context); + }, + ), + BackupOption( + "settings", + (context) => AppLocalizations.of(context)!.settings, + encode: () async { + return json.encode(appSettings.valueToJson()); + }, + decode: (context, value) async { + appSettings.loadValueFromJson(json.decode(value)); + appSettings.callAllListeners(); + App.refreshTheme(context); + await appSettings.save(); + if (context.mounted) { + setDigitalClockWidgetData(context); + } + }, + ), + BackupOption( + "alarms", + (context) => AppLocalizations.of(context)!.alarmTitle, + encode: () async { + return await loadTextFile("alarms"); + }, + decode: (context, value) async { + await saveList("alarms", [ + ...listFromString(value), + ...await loadList("alarms") + ]); + await updateAlarms("Updated alarms on importing backup"); + }, + ), + BackupOption( + "timers", + (context) => AppLocalizations.of(context)!.timerTitle, + encode: () async { + return await loadTextFile("timers"); + }, + decode: (context, value) async { + await saveList("timers", [ + ...listFromString(value), + ...await loadList("timers") + ]); + await updateTimers("Updated timers on importing backup"); + }, + ), + BackupOption( + "favorite_cities", + (context) => AppLocalizations.of(context)!.clockTitle, + encode: () async { + return await loadTextFile("favorite_cities"); + }, + decode: (context, value) async { + await saveList("favorite_cities", [ + ...listFromString(value), + // ...await loadList("favorite_cities") + ]); + // await updateTimers("Updated timers on importing backup"); + }, + ), + + // BackupOption( + // "stopwatches", + // (context) => AppLocalizations.of(context)!.stopwatchTitle, + // encode: () async { + // return await loadTextFile("stopwatches"); + // }, + // decode: (context, value) async { + // await saveList("stopwatches", [ + // ...listFromString(value), + // ]); + // }, + // ), + + BackupOption( + "timer_presets", + (context) => AppLocalizations.of(context)!.presetsSetting, + encode: () async { + return await loadTextFile("timer_presets"); + }, + decode: (context, value) async { + await saveList("timer_presets", [ + ...listFromString(value), + ...await loadList("timer_presets") + ]); + }, + ), + +]; diff --git a/lib/settings/data/backup_settings_schema.dart b/lib/settings/data/backup_settings_schema.dart index 956facf0..41e6d1f0 100644 --- a/lib/settings/data/backup_settings_schema.dart +++ b/lib/settings/data/backup_settings_schema.dart @@ -1,16 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:clock_app/app.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/logic/backup.dart'; import 'package:clock_app/settings/screens/backup_screen.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; -import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/material.dart'; -import 'package:pick_or_save/pick_or_save.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; SettingGroup backupSettingsSchema = SettingGroup( @@ -18,21 +11,13 @@ SettingGroup backupSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.backupSettingGroup, getDescription: (context) => AppLocalizations.of(context)!.backupSettingGroupDescription, +showExpandedView: false, icon: Icons.restore_rounded, [ - SettingGroup( - "Settings", - (context) => AppLocalizations.of(context)!.settingsTitle, - [ - // SettingPageLink("Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, BackupScreen()), - // SettingPageLink("Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, BackupScreen()), - SettingAction( + SettingPageLink( "Export", (context) => AppLocalizations.of(context)!.exportSettingsSetting, - (context) async { - saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); - }, - searchTags: ["settings", "export", "backup", "save"], + const BackupExportScreen(), getDescription: (context) => AppLocalizations.of(context)!.exportSettingsSettingDescription, ), @@ -40,45 +25,43 @@ SettingGroup backupSettingsSchema = SettingGroup( "Import", (context) => AppLocalizations.of(context)!.importSettingsSetting, (context) async { - loadBackupFile( - (data) async { - appSettings.loadValueFromJson(json.decode(data)); - appSettings.callAllListeners(); - App.refreshTheme(context); - await appSettings.save(); - if (context.mounted) setDigitalClockWidgetData(context); - }, - ); + final data = await loadBackupFile(); + if(data == null) return; + if (context.mounted) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => BackupImportScreen(data: data))); + } }, - searchTags: ["settings", "import", "backup", "load"], getDescription: (context) => AppLocalizations.of(context)!.importSettingsSettingDescription, ), - ], - ), + // SettingAction( + // "Export", + // (context) => AppLocalizations.of(context)!.exportSettingsSetting, + // (context) async { + // saveBackupFile(json.encode(appSettings.valueToJson()), "settings"); + // }, + // searchTags: ["settings", "export", "backup", "save"], + // getDescription: (context) => + // AppLocalizations.of(context)!.exportSettingsSettingDescription, + // ), + // SettingAction( + // "Import", + // (context) => AppLocalizations.of(context)!.importSettingsSetting, + // (context) async { + // loadBackupFile( + // (data) async { + // appSettings.loadValueFromJson(json.decode(data)); + // appSettings.callAllListeners(); + // App.refreshTheme(context); + // await appSettings.save(); + // if (context.mounted) setDigitalClockWidgetData(context); + // }, + // ); + // }, + // searchTags: ["settings", "import", "backup", "load"], + // getDescription: (context) => + // AppLocalizations.of(context)!.importSettingsSettingDescription, + // ), ], ); - -saveBackupFile(String data, String label) async { - await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: Uint8List.fromList(utf8.encode(data)), - fileName: "chrono_${label}_backup_${DateTime.now().toIso8601String()}", - ) - ], - )); -} - -loadBackupFile(Function(String) onSuccess) async { - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - getCachedFilePath: true, - ), - ); - if (result != null && result.isNotEmpty) { - File file = File(result[0]); - onSuccess(utf8.decode(file.readAsBytesSync())); - } -} diff --git a/lib/settings/logic/backup.dart b/lib/settings/logic/backup.dart new file mode 100644 index 00000000..192317c0 --- /dev/null +++ b/lib/settings/logic/backup.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:pick_or_save/pick_or_save.dart'; + +void saveBackupFile(String data) async { + await PickOrSave().fileSaver( + params: FileSaverParams( + saveFiles: [ + SaveFileInfo( + fileData: Uint8List.fromList(utf8.encode(data)), + fileName: "chrono_backup_${DateTime.now().toIso8601String().split(".")[0]}.json", + ) + ], + )); +} + +Future loadBackupFile() async { + List? result = await PickOrSave().filePicker( + params: FilePickerParams( + getCachedFilePath: true, + ), + ); + if (result != null && result.isNotEmpty) { + File file = File(result[0]); + return utf8.decode(file.readAsBytesSync()); + } + return null; +} diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index b7ff0b16..98649613 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -1,25 +1,17 @@ import 'dart:convert'; -import 'package:clock_app/alarm/logic/update_alarms.dart'; -import 'package:clock_app/alarm/types/alarm.dart'; -import 'package:clock_app/app.dart'; -import 'package:clock_app/common/utils/list_storage.dart'; -import 'package:clock_app/common/widgets/card_container.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/settings/data/backup_options.dart'; +import 'package:clock_app/settings/logic/backup.dart'; +import 'package:clock_app/settings/types/backup_option.dart'; import 'package:clock_app/settings/widgets/settings_top_bar.dart'; -import 'package:clock_app/theme/types/color_scheme.dart'; -import 'package:clock_app/theme/types/style_theme.dart'; -import 'package:clock_app/timer/logic/update_timers.dart'; -import 'package:clock_app/timer/types/timer.dart'; -import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class BackupOptionCheckBox extends StatelessWidget { const BackupOptionCheckBox( - {super.key, - required this.option, required this.onChanged}); + {super.key, required this.option, required this.onChanged}); final BackupOption option; final void Function(bool?) onChanged; @@ -46,114 +38,104 @@ class BackupOptionCheckBox extends StatelessWidget { class BackupExportScreen extends StatefulWidget { const BackupExportScreen({ super.key, - required this.settingGroup, - required this.onRestore, }); - final SettingGroup settingGroup; - final void Function() onRestore; - @override State createState() => _BackupExportScreenState(); } -class BackupOption { - final String Function(BuildContext context) getName; - final String key; - final dynamic Function() encode; - final Function(BuildContext context, dynamic value) decode; - bool selected = true; +class _BackupExportScreenState extends State { + @override + void initState() { + for (var option in backupOptions) { + option.selected = true; + } + super.initState(); + } - BackupOption(this.key, this.getName, - {required this.encode, required this.decode}); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: SettingsTopBar( + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TextButton( + onPressed: () async { + try { + final backupData = {}; + for (var option in backupOptions) { + if (option.selected) { + backupData[option.key] = await option.encode(); + } + } + saveBackupFile(json.encode(backupData)); + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showSnackBar(context, "Error exporting: ${e.toString()}", + error: true); + } + } + }, + child: + Text(AppLocalizations.of(context)!.exportSettingsSetting)), + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + ...backupOptions.map( + (option) => BackupOptionCheckBox( + option: option, + onChanged: (bool? value) { + setState(() { + option.selected = value ?? false; + }); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } } -final backupOptions = [ - BackupOption( - "settings", - (context) => AppLocalizations.of(context)!.settings, - encode: () async { - return json.encode(appSettings.valueToJson()); - }, - decode: (context, value) async { - appSettings.loadValueFromJson(json.decode(value)); - appSettings.callAllListeners(); - App.refreshTheme(context); - await appSettings.save(); - if (context.mounted) { - setDigitalClockWidgetData(context); - } - }, - ), - BackupOption( - "color_schemes", - (context) => AppLocalizations.of(context)!.colorSchemeSetting, - encode: () async { - List colorSchemes = await loadList("color_schemes"); - List customColorSchemes = - colorSchemes.where((scheme) => !scheme.isDefault).toList(); - return json.encode(customColorSchemes); - }, - decode: (context, value) async { - List colorSchemes = - await loadList("color_schemes"); - await saveList( - "color_schemes", [...json.decode(value), ...colorSchemes]); - if (context.mounted) App.refreshTheme(context); - }, - ), - BackupOption( - "style_themes", - (context) => AppLocalizations.of(context)!.styleThemeSetting, - encode: () async { - List styleThemes = await loadList("style_themes"); - List customThemes = - styleThemes.where((scheme) => !scheme.isDefault).toList(); - return json.encode(customThemes); - }, - decode: (context, value) async { - List styleThemes = - await loadList("style_themes"); - await saveList( - "style_themes", [...json.decode(value), ...styleThemes]); - if (context.mounted) App.refreshTheme(context); - }, - ), - BackupOption( - "alarms", - (context) => AppLocalizations.of(context)!.alarmTitle, - encode: () async { - return json.encode(await loadList("alarms")); - }, - decode: (context, value) async { - await saveList("alarms", [...json.decode(value), ...await loadList("alarms")]); - await updateAlarms("Updated alarms on importing backup"); - if (context.mounted) App.refreshTheme(context); - }, - ), - BackupOption( - "timers", - (context) => AppLocalizations.of(context)!.alarmTitle, - encode: () async { - return json.encode(await loadList("timers")); - }, - decode: (context, value) async { - await saveList("timers", [...json.decode(value), ...await loadList("timers")]); - await updateTimers("Updated timers on importing backup"); - if (context.mounted) App.refreshTheme(context); - }, - ), - ]; +class BackupImportScreen extends StatefulWidget { + const BackupImportScreen({ + super.key, + required this.data, + }); -class _BackupExportScreenState extends State { - bool settings = false; - late final Map _settingsToRestore = { - for (var settingItem in widget.settingGroup.settingItems) - settingItem.id: true - }; + final String data; + + @override + State createState() => _BackupImportScreenState(); +} + +class _BackupImportScreenState extends State { + late final List importOptions = []; + late final Json dataJson; @override void initState() { + dataJson = json.decode(widget.data); + + if (dataJson != null) { + for (var option in backupOptions) { + option.selected = true; + if (dataJson!.keys.contains(option.key)) { + importOptions.add(option); + } + } + } + super.initState(); } @@ -161,39 +143,37 @@ class _BackupExportScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: SettingsTopBar( - title: AppLocalizations.of(context)!.restoreSettingGroup, + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TextButton( + onPressed: () async { + try { + if (dataJson == null) return; + for (var option in importOptions) { + if (option.selected && context.mounted) { + option.decode(context, dataJson![option.key]); + } + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showSnackBar(context, "Error importing: ${e.toString()}", + error: true); + } + } + }, + child: + Text(AppLocalizations.of(context)!.importSettingsSetting)), + ), + ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ - CardContainer( - color: Theme.of(context).colorScheme.primary, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(AppLocalizations.of(context)!.resetButton, - style: Theme.of(context) - .textTheme - .displaySmall - ?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - )), - ], - ), - ), - onTap: () { - widget.settingGroup - .restoreDefaults(context, _settingsToRestore); - widget.onRestore(); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ...backupOptions.map( + ...importOptions.map( (option) => BackupOptionCheckBox( option: option, onChanged: (bool? value) { diff --git a/lib/settings/types/backup_option.dart b/lib/settings/types/backup_option.dart new file mode 100644 index 00000000..fe51ea77 --- /dev/null +++ b/lib/settings/types/backup_option.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class BackupOption { + final String Function(BuildContext context) getName; + final String key; + final dynamic Function() encode; + final Function(BuildContext context, dynamic value) decode; + bool selected = true; + + BackupOption(this.key, this.getName, + {required this.encode, required this.decode}); +} + + diff --git a/lib/settings/widgets/settings_top_bar.dart b/lib/settings/widgets/settings_top_bar.dart index cbb12eee..6704aaf1 100644 --- a/lib/settings/widgets/settings_top_bar.dart +++ b/lib/settings/widgets/settings_top_bar.dart @@ -13,10 +13,11 @@ class SettingsTopBar extends StatefulWidget implements PreferredSizeWidget { {super.key, this.onSearch, this.showSearch = false, - required this.title}); + this.title, this.actions}); final void Function(List settings)? onSearch; - final String title; + final List? actions; + final String? title; final bool showSearch; @override @@ -86,14 +87,15 @@ class _SettingsTopBarState extends State { ); } else { return AppTopBar( - title: Text( - widget.title, + title: widget.title != null ? Text( + widget.title!, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6), ), - ), + ): null, actions: [ + ...?widget.actions, if (widget.showSearch) IconButton( onPressed: () { From b3d131deab3ff8685cfd1840a780119f0de4d0a2 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 27 Aug 2024 11:42:58 +0500 Subject: [PATCH 087/177] Improve backup --- lib/alarm/types/alarm.dart | 2 -- lib/settings/data/backup_options.dart | 24 ++++++++++++++---------- lib/settings/logic/backup.dart | 4 ++-- lib/settings/screens/backup_screen.dart | 12 ++++++++++-- lib/settings/types/backup_option.dart | 6 ++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 971b80ad..2f7ba5d8 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -135,7 +135,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 +147,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; diff --git a/lib/settings/data/backup_options.dart b/lib/settings/data/backup_options.dart index 0914d7b4..c6fc30f3 100644 --- a/lib/settings/data/backup_options.dart +++ b/lib/settings/data/backup_options.dart @@ -20,15 +20,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // tags should be before alarms and timers // color_schemes and style_themes should be before settings final backupOptions = [ - BackupOption( + BackupOption( "tags", - (context) => AppLocalizations.of(context)!.presetsSetting, + (context) => AppLocalizations.of(context)!.tagsSetting, encode: () async { return await loadTextFile("tags"); }, decode: (context, value) async { await saveList("tags", [ - ...listFromString(value), + ...listFromString(value) + .map((tag) => TimerPreset.from(tag)), ...await loadList("tags") ]); }, @@ -45,7 +46,8 @@ final backupOptions = [ }, decode: (context, value) async { await saveList("color_schemes", [ - ...listFromString(value), + ...listFromString(value) + .map((scheme) => ColorSchemeData.from(scheme)), ...await loadList("color_schemes") ]); if (context.mounted) App.refreshTheme(context); @@ -62,7 +64,8 @@ final backupOptions = [ }, decode: (context, value) async { await saveList("style_themes", [ - ...listFromString(value), + ...listFromString(value) + .map((theme) => StyleTheme.from(theme)), ...await loadList("style_themes") ]); if (context.mounted) App.refreshTheme(context); @@ -92,7 +95,7 @@ final backupOptions = [ }, decode: (context, value) async { await saveList("alarms", [ - ...listFromString(value), + ...listFromString(value).map((alarm) => Alarm.fromAlarm(alarm)), ...await loadList("alarms") ]); await updateAlarms("Updated alarms on importing backup"); @@ -106,7 +109,8 @@ final backupOptions = [ }, decode: (context, value) async { await saveList("timers", [ - ...listFromString(value), + ...listFromString(value) + .map((timer) => ClockTimer.from(timer)), ...await loadList("timers") ]); await updateTimers("Updated timers on importing backup"); @@ -140,7 +144,7 @@ final backupOptions = [ // }, // ), - BackupOption( + BackupOption( "timer_presets", (context) => AppLocalizations.of(context)!.presetsSetting, encode: () async { @@ -148,10 +152,10 @@ final backupOptions = [ }, decode: (context, value) async { await saveList("timer_presets", [ - ...listFromString(value), + ...listFromString(value) + .map((preset) => TimerPreset.from(preset)), ...await loadList("timer_presets") ]); }, ), - ]; diff --git a/lib/settings/logic/backup.dart b/lib/settings/logic/backup.dart index 192317c0..aad286fc 100644 --- a/lib/settings/logic/backup.dart +++ b/lib/settings/logic/backup.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:pick_or_save/pick_or_save.dart'; -void saveBackupFile(String data) async { - await PickOrSave().fileSaver( +Future?> saveBackupFile(String data) async { + return await PickOrSave().fileSaver( params: FileSaverParams( saveFiles: [ SaveFileInfo( diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index 98649613..672b769d 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -69,7 +69,12 @@ class _BackupExportScreenState extends State { backupData[option.key] = await option.encode(); } } - saveBackupFile(json.encode(backupData)); + final result = + await saveBackupFile(json.encode(backupData)); + if (result == null) return; + if (context.mounted) { + showSnackBar(context, "Export successful!"); + } } catch (e) { debugPrint(e.toString()); if (context.mounted) { @@ -152,9 +157,12 @@ class _BackupImportScreenState extends State { if (dataJson == null) return; for (var option in importOptions) { if (option.selected && context.mounted) { - option.decode(context, dataJson![option.key]); + await option.decode(context, dataJson![option.key]); } } + if (context.mounted) { + showSnackBar(context, "Import successful!"); + } } catch (e) { debugPrint(e.toString()); if (context.mounted) { diff --git a/lib/settings/types/backup_option.dart b/lib/settings/types/backup_option.dart index fe51ea77..f038bcca 100644 --- a/lib/settings/types/backup_option.dart +++ b/lib/settings/types/backup_option.dart @@ -3,12 +3,10 @@ import 'package:flutter/material.dart'; class BackupOption { final String Function(BuildContext context) getName; final String key; - final dynamic Function() encode; - final Function(BuildContext context, dynamic value) decode; + final Future Function() encode; + final Future Function(BuildContext context, dynamic value) decode; bool selected = true; BackupOption(this.key, this.getName, {required this.encode, required this.decode}); } - - From 4849cb3b0f626c1b1064a4b63d54cbb305ddc3f5 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Thu, 29 Aug 2024 18:40:47 +0500 Subject: [PATCH 088/177] Add animations --- lib/app.dart | 2 +- lib/clock/logic/timezone_database.dart | 7 +- lib/common/data/animations.dart | 18 + lib/common/logic/card_decoration.dart | 5 +- lib/common/widgets/animated_show_hide.dart | 297 ++++++ lib/common/widgets/card_container.dart | 16 +- .../select_field/select_bottom_sheet.dart | 2 +- .../animated_gridview.dart | 207 ++++ .../animated_listview.dart | 200 ++++ .../animated_reorderable_gridview.dart | 257 +++++ .../animated_reorderable_listview.dart | 273 ++++++ .../animation/animation.dart | 14 + .../animation/fade_in.dart | 23 + .../animation/flipin_x.dart | 31 + .../animation/flipin_y.dart | 32 + .../animation/landing.dart | 26 + .../animation/provider/animation_effect.dart | 75 ++ .../animation/provider/animation_type.dart | 16 + .../animation/scale_in.dart | 23 + .../animation/scale_in_bottom.dart | 25 + .../animation/scale_in_left.dart | 24 + .../animation/scale_in_right.dart | 25 + .../animation/scale_in_top.dart | 25 + .../animation/size_animation.dart | 37 + .../animation/slide_in_down.dart | 26 + .../animation/slide_in_left.dart | 22 + .../animation/slide_in_right.dart | 23 + .../animation/slide_in_up.dart | 22 + .../builder/motion_animated_builder.dart | 903 ++++++++++++++++++ .../builder/motion_list_base.dart | 253 +++++ .../builder/motion_list_impl.dart | 97 ++ .../component/drag_item.dart | 181 ++++ .../component/drag_listener.dart | 94 ++ .../component/motion_animated_content.dart | 221 +++++ ...iver_grid_with_fixed_cross_axis_count.dart | 157 +++ .../sliver_grid_with_main_axis_extent.dart | 128 +++ .../model/motion_data.dart | 30 + lib/common/widgets/list/custom_list_view.dart | 419 +++----- lib/common/widgets/list/list_filter_bar.dart | 136 +++ lib/common/widgets/list/list_filter_chip.dart | 116 ++- lib/common/widgets/list/list_item_card.dart | 31 +- lib/common/widgets/measure_size.dart | 6 +- lib/l10n/app_en.arb | 2 + lib/main.dart | 6 +- .../data/appearance_settings_schema.dart | 18 + .../data/general_settings_schema.dart | 24 +- lib/settings/logic/get_setting_widget.dart | 4 +- lib/settings/logic/initialize_settings.dart | 4 +- .../screens/settings_group_screen.dart | 4 +- pubspec.lock | 16 + pubspec.yaml | 6 +- 51 files changed, 4208 insertions(+), 401 deletions(-) create mode 100644 lib/common/data/animations.dart create mode 100644 lib/common/widgets/animated_show_hide.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animated_listview.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/animation.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/landing.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart create mode 100644 lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart create mode 100644 lib/common/widgets/list/list_filter_bar.dart diff --git a/lib/app.dart b/lib/app.dart index d69af086..3c6da52f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -69,7 +69,7 @@ class _AppState extends State { _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); diff --git a/lib/clock/logic/timezone_database.dart b/lib/clock/logic/timezone_database.dart index c6435da2..6c3cbb26 100644 --- a/lib/clock/logic/timezone_database.dart +++ b/lib/clock/logic/timezone_database.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; import 'dart:io'; +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 +15,12 @@ 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); + debugPrint('Copying timzones.db to $timezonesDatabasePath'); + // debugPrint(json.encode(bytes)); // Save copied asset to documents await File(timezonesDatabasePath).writeAsBytes(bytes); } diff --git a/lib/common/data/animations.dart b/lib/common/data/animations.dart new file mode 100644 index 00000000..86f48cf7 --- /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() + .slideY(begin: 0.15, end: 0, duration: 150.ms, curve: Curves.easeOut) + .fade(duration: 150.ms, curve: Curves.easeOut); +} diff --git a/lib/common/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart index e22c5936..71e84180 100644 --- a/lib/common/logic/card_decoration.dart +++ b/lib/common/logic/card_decoration.dart @@ -15,8 +15,9 @@ BoxDecoration getCardDecoration(BuildContext context, return BoxDecoration( border: isSelected ? Border.all( color: colorScheme.primary, - width: 2, - strokeAlign: BorderSide.strokeAlignOutside, + width: 1, + strokeAlign: BorderSide.strokeAlignOutside + ) : showLightBorder ? Border.all( color: colorScheme.outline.withOpacity(0.2), 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 d6ebb20e..0cf3bea9 100644 --- a/lib/common/widgets/card_container.dart +++ b/lib/common/widgets/card_container.dart @@ -5,13 +5,12 @@ 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]){ +Color getCardColor(BuildContext context, [Color? color]) { ColorScheme colorScheme = Theme.of(context).colorScheme; bool useMaterialYou = appSettings .getGroup("Appearance") @@ -22,10 +21,10 @@ Color getCardColor(BuildContext context, [Color? color]){ TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); return color ?? - (useMaterialYou - ? Color(tonalPalette.get( - Theme.of(context).brightness == Brightness.light ? 96 : 15)) - : colorScheme.surface); + (useMaterialYou + ? Color(tonalPalette + .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) + : colorScheme.surface); } class CardContainer extends StatelessWidget { @@ -40,7 +39,7 @@ class CardContainer extends StatelessWidget { this.showShadow = true, this.isSelected = false, this.showLightBorder = false, - this.blurStyle = BlurStyle.normal, + this.blurStyle = BlurStyle.normal, this.onLongPress, }); final Widget child; @@ -48,6 +47,7 @@ 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; @@ -69,6 +69,7 @@ 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, @@ -86,6 +87,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/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/list/animated_reorderable_list/animated_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart new file mode 100644 index 00000000..9c4c1ac1 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart @@ -0,0 +1,207 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// A Flutter AnimatedGridView that animates insertion and removal of the item. +class AnimatedGridView extends StatelessWidget { + /// The current list of items that this[MotionGridViewBuilder] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + /// Controls the layout of tiles in a grid. + /// Given the current constraints on the grid, + /// a SliverGridDelegate computes the layout for the tiles in the grid. + /// The tiles can be placed arbitrarily, + /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children. + final SliverGridDelegate sliverGridDelegate; + + ///List of [AnimationEffect] used for the appearing animation when item is added in the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect] used for the disappearing animation when item is removed from list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedGridView( + {Key? key, + required this.items, + required this.itemBuilder, + required this.sliverGridDelegate, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.shrinkWrap = false, + this.isSameItem}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl.grid( + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + insertDuration: insertDuration, + removeDuration: removeDuration, + enterTransition: enterTransition, + exitTransition: exitTransition, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + isSameItem: isSameItem), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart new file mode 100644 index 00000000..d304220e --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart @@ -0,0 +1,200 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// A Flutter AnimatedGridView that animates insertion and removal of the item. +class AnimatedListView extends StatelessWidget { + /// The current list of items that this[MotionListViewBuilder] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The `context` argument is the build context where the widget will be + /// created, the `index` is the index of the item to be built, and the + /// `animation` is an [Animation] that should be used to animate an entry + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The `context` argument is the build context where the widget will be + /// created, the `index` is the index of the item to be built, and the + /// `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedListView({ + Key? key, + required this.items, + required this.itemBuilder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.scrollDirection = Axis.vertical, + this.padding, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.shrinkWrap = false, + this.isSameItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl( + items: items, + itemBuilder: itemBuilder, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart new file mode 100644 index 00000000..98ebc732 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart @@ -0,0 +1,257 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; + +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +///A GridView that enables users to interactively reorder items through dragging, with animated insertion and removal of items. +/// +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// The [onReorder] parameter is required and will be called when a child +/// widget is dragged to a new position. +/// +/// +/// All list items must have a key. +/// +/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator] +/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is +/// created with the original list item as its child. +class AnimatedReorderableGridView extends StatelessWidget { + /// The current list of items that this[AnimatedReorderableGridView] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + /// Controls the layout of tiles in a grid. + /// Given the current constraints on the grid, + /// a SliverGridDelegate computes the layout for the tiles in the grid. + /// The tiles can be placed arbitrarily, + /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children. + final SliverGridDelegate sliverGridDelegate; + + ///List of [AnimationEffect](s) used for the appearing animation when item is added in the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when item is removed from list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// A callback used by [ReorderableList] to report that a list item has moved + /// to a new position in the list. + /// + /// Implementations should remove the corresponding list item at [oldIndex] + /// and reinsert it at [newIndex]. + final ReorderCallback onReorder; + + /// A callback that is called when an item drag has started. + /// + /// The index parameter of the callback is the index of the selected item. + final void Function(int)? onReorderStart; + + /// A callback that is called when the dragged item is dropped. + /// + /// The index parameter of the callback is the index where the item is + /// dropped. Unlike [onReorder], this is called even when the list item is + /// dropped in the same location. + final void Function(int)? onReorderEnd; + + /// {@template flutter.widgets.reorderable_list.proxyDecorator} + /// A callback that allows the app to add an animated decoration around + /// an item when it is being dragged. + /// {@endtemplate} + final ReorderItemProxyDecorator? proxyDecorator; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the items can be dragged by long pressing on them. + final bool longPressDraggable; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedReorderableGridView( + {Key? key, + required this.items, + required this.itemBuilder, + required this.sliverGridDelegate, + required this.onReorder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.onReorderStart, + this.onReorderEnd, + this.proxyDecorator, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.longPressDraggable = true, + this.shrinkWrap = false, + this.insertItemBuilder, + this.removeItemBuilder, + this.isSameItem}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl.grid( + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + insertDuration: insertDuration, + removeDuration: removeDuration, + enterTransition: enterTransition, + exitTransition: exitTransition, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + longPressDraggable: longPressDraggable, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart new file mode 100644 index 00000000..483b5c82 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart @@ -0,0 +1,273 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'builder/motion_list_base.dart'; +import 'builder/motion_list_impl.dart'; + +///A [ListView] that enables users to interactively reorder items through dragging, with animated insertion and removal of items. +/// +/// enterTransition: [FadeEffect(), ScaleEffect()], +/// +/// Effects are always run in parallel (ie. the fade and scale effects in the +/// example above would be run simultaneously), but you can apply delays to +/// offset them or run them in sequence. +/// +/// The [onReorder] parameter is required and will be called when a child +/// widget is dragged to a new position. +/// +/// By default, on [TargetPlatformVariant.desktop] platforms each item will +/// have a drag handle added on top of it that will allow the user to grab it +/// to move the item. On [TargetPlatformVariant.mobile], no drag handle will be +/// added, but when the user long presses anywhere on the item it will start +/// moving the item.Displaying drag handles can be controlled with [AnimatedReorderableListView.buildDefaultDragHandles]. +/// +/// All list items must have a key. +/// +/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator] +/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is +/// created with the original list item as its child. + +class AnimatedReorderableListView extends StatelessWidget { + /// The current list of items that this[AnimatedReorderableListView] should represent. + final List items; + + ///Called, as needed, to build list item widget + final ItemBuilder itemBuilder; + + ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list. + /// + ///Defaults to [FadeAnimation()] + final List? enterTransition; + + ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list. + /// + ///Defaults to [FadeAnimation()] + final List? exitTransition; + + /// The duration of the animation when an item was inserted into the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration]. + final Duration? insertDuration; + + /// The duration of the animation when an item was removed from the list. + /// + /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration]. + final Duration? removeDuration; + + /// A callback used by [ReorderableList] to report that a list item has moved + /// to a new position in the list. + /// + /// Implementations should remove the corresponding list item at [oldIndex] + /// and reinsert it at [newIndex]. + final ReorderCallback onReorder; + + /// A callback that is called when an item drag has started. + /// + /// The index parameter of the callback is the index of the selected item. + final void Function(int)? onReorderStart; + + /// A callback that is called when the dragged item is dropped. + /// + /// The index parameter of the callback is the index where the item is + /// dropped. Unlike [onReorder], this is called even when the list item is + /// dropped in the same location. + final void Function(int)? onReorderEnd; + + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// {@template flutter.widgets.reorderable_list.proxyDecorator} + /// A callback that allows the app to add an animated decoration around + /// an item when it is being dragged. + /// {@endtemplate} + final ReorderItemProxyDecorator? proxyDecorator; + + /// If true, on desktop platforms, a drag handle is stacked over the center of each item's trailing edge; + /// on mobile platforms, a long press anywhere on the item starts a drag. + /// + /// The default desktop drag handle is just an [Icons.drag_handle] wrapped by [ReorderableDragStartListener]. + /// On mobile platforms, the entire item is wrapped with a [ReorderableDragStartListener]. + /// + /// To change the appearance or the layout of the drag handles, make this parameter false + /// and wrap each list item, or a widget within each list item, with [ReorderableDragStartListener]or + /// a subclass of [ReorderableDragStartListener]. + /// + /// To get the idea [Flutter Example](https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html) + + final bool buildDefaultDragHandles; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// [ScrollController] to get the current scroll position. + /// + /// Must be null if [primary] is true. + /// + /// It can be used to read the current + // scroll position (see [ScrollController.offset]), or change it (see + // [ScrollController.animateTo]). + final ScrollController? controller; + + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Defaults to null. + final bool? primary; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + final ScrollPhysics? physics; + + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final String? restorationId; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// Defaults to [Clip.hardEdge]. + /// + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final Clip clipBehavior; + + /// Creates a ScrollView that creates custom scroll effects using slivers. + /// See the ScrollView constructor for more details on these arguments. + final DragStartBehavior dragStartBehavior; + + /// A custom builder that is for adding items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? insertItemBuilder; + + /// A custom builder that is for removing items with animations. + /// + /// The child argument is the widget that is returned by [itemBuilder], + /// and the `animation` is an [Animation] that should be used to animate an exit + /// transition for the widget that is built. + final AnimatedWidgetBuilder? removeItemBuilder; + + /// Whether the items can be dragged by long pressing on them. + final bool longPressDraggable; + + final bool useDefaultDragListeners; + + /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed. + final bool shrinkWrap; + + /// A function that compares two items to determine whether they are the same. + final bool Function(E a, E b)? isSameItem; + + const AnimatedReorderableListView({ + Key? key, + required this.items, + required this.itemBuilder, + required this.onReorder, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + this.onReorderStart, + this.onReorderEnd, + this.proxyDecorator, + this.scrollDirection = Axis.vertical, + this.padding, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.scrollBehavior, + this.restorationId, + this.buildDefaultDragHandles = true, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.dragStartBehavior = DragStartBehavior.start, + this.clipBehavior = Clip.hardEdge, + this.insertItemBuilder, + this.removeItemBuilder, + this.longPressDraggable = true, + this.useDefaultDragListeners = true, + this.shrinkWrap = false, + this.isSameItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + scrollBehavior: scrollBehavior, + restorationId: restorationId, + keyboardDismissBehavior: keyboardDismissBehavior, + dragStartBehavior: dragStartBehavior, + clipBehavior: clipBehavior, + shrinkWrap: shrinkWrap, + slivers: [ + SliverPadding( + padding: padding ?? EdgeInsets.zero, + sliver: MotionListImpl( + items: items, + itemBuilder: itemBuilder, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + buildDefaultDragHandles: buildDefaultDragHandles, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + isSameItem: isSameItem, + ), + ), + ]); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart new file mode 100644 index 00000000..5c50b12f --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart @@ -0,0 +1,14 @@ +export 'fade_in.dart'; +export 'flipin_x.dart'; +export 'flipin_y.dart'; +export 'landing.dart'; +export 'scale_in.dart'; +export 'scale_in_bottom.dart'; +export 'scale_in_left.dart'; +export 'scale_in_right.dart'; +export 'scale_in_top.dart'; +export 'slide_in_down.dart'; +export 'slide_in_left.dart'; +export 'slide_in_right.dart'; +export 'slide_in_up.dart'; +export 'size_animation.dart'; diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart new file mode 100644 index 00000000..d295db44 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FadeIn extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + FadeIn({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation opacity = buildAnimation( + entry, + begin: begin ?? beginValue, + end: end ?? endValue, + totalDuration) + .animate(animation); + return FadeTransition(opacity: opacity, child: child); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart new file mode 100644 index 00000000..51361c08 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart @@ -0,0 +1,31 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FlipInX extends AnimationEffect { + static const double beginValue = pi / 2; + static const double endValue = 0.0; + final double? begin; + final double? end; + + FlipInX({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation rotation = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Transform( + transform: Matrix4.rotationX(rotation.value), + alignment: Alignment.center, + child: child, + ); + }, + child: child); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart new file mode 100644 index 00000000..42844956 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart @@ -0,0 +1,32 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class FlipInY extends AnimationEffect { + static const double beginValue = pi / 2; + static const double endValue = 0.0; + final double? begin; + final double? end; + + FlipInY({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation rotation = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return AnimatedBuilder( + animation: rotation, + builder: (BuildContext context, Widget? child) { + return Transform( + transform: Matrix4.rotationY(rotation.value), + alignment: Alignment.center, + child: child, + ); + }, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart new file mode 100644 index 00000000..d2f2c777 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart @@ -0,0 +1,26 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class Landing extends AnimationEffect { + static const double beginValue = 1.5; + static const double endValue = 1.0; + final double? begin; + final double? end; + + Landing({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: scale, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart new file mode 100644 index 00000000..0e177938 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart @@ -0,0 +1,75 @@ +import 'package:flutter/cupertino.dart'; + +abstract class AnimationEffect { + /// The delay for this specific [AnimationEffect]. + final Duration? delay; + + /// The duration for the specific [AnimationEffect]. + final Duration? duration; + + /// The curve for the specific [AnimationEffect]. + final Curve? curve; + + AnimationEffect({ + this.delay = Duration.zero, + this.duration = const Duration(milliseconds: 300), + this.curve, + }); + + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + return child; + } + + Animatable buildAnimation(EffectEntry entry, Duration totalDuration, + {required T begin, required T end}) { + return Tween(begin: begin, end: end) + .chain(entry.buildAnimation(totalDuration: totalDuration)); + } +} + +@immutable +class EffectEntry { + const EffectEntry({ + required this.animationEffect, + required this.delay, + required this.duration, + required this.curve, + }); + + /// The delay for this entry. + final Duration delay; + + /// The duration for this entry. + final Duration duration; + + /// The curve used by this entry. + final Curve curve; + + /// The effect associated with this entry. + final AnimationEffect animationEffect; + + /// The begin time for this entry. + Duration get begin => delay; + + /// The end time for this entry. + Duration get end => begin + duration; + + /// Builds a sub-animation based on the properties of this entry. + CurveTween buildAnimation({ + required Duration totalDuration, + Curve? curve, + }) { + int beginT = begin.inMicroseconds, endT = end.inMicroseconds; + return CurveTween( + curve: Interval(beginT / totalDuration.inMicroseconds, + endT / totalDuration.inMicroseconds, + curve: curve ?? this.curve), + ); + } + + @override + String toString() { + return "delay: $delay, Duration: $duration, curve: $curve, begin: $begin, end: $end, Effect: $animationEffect"; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart new file mode 100644 index 00000000..339f93cb --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart @@ -0,0 +1,16 @@ +enum AnimationType { + fadeIn, + flipInY, + flipInX, + landing, + size, + scaleIn, + scaleInTop, + scaleInBottom, + scaleInLeft, + scaleInRight, + slideInLeft, + slideInRight, + slideInDown, + slideInUp, +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart new file mode 100644 index 00000000..7a158ac2 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleIn extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleIn({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart new file mode 100644 index 00000000..844a2bcf --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInBottom extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInBottom( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.bottomCenter, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart new file mode 100644 index 00000000..ed8c35d1 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart @@ -0,0 +1,24 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInLeft extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInLeft({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.centerLeft, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart new file mode 100644 index 00000000..ea322822 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class ScaleInRight extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInRight( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.centerRight, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart new file mode 100644 index 00000000..77588d9a --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart @@ -0,0 +1,25 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + + +class ScaleInTop extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + final double? begin; + final double? end; + + ScaleInTop({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation scale = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ScaleTransition( + alignment: Alignment.topCenter, + scale: scale, + child: child, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart new file mode 100644 index 00000000..62a3df0e --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart @@ -0,0 +1,37 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SizeAnimation extends AnimationEffect { + static const double beginValue = 0.0; + static const double endValue = 1.0; + static const double alignmentValue = 0.0; + final double? begin; + final double? end; + final Axis? axis; + final double? axisAlignment; + + SizeAnimation( + {super.delay, + super.duration, + super.curve, + this.begin, + this.end, + this.axis, + this.axisAlignment}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation sizeFactor = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return Align( + child: SizeTransition( + sizeFactor: sizeFactor, + axis: axis ?? Axis.horizontal, + axisAlignment: axisAlignment ?? alignmentValue, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart new file mode 100644 index 00000000..3ebe8636 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart @@ -0,0 +1,26 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInDown extends AnimationEffect { + static const Offset beginValue = Offset(0, 1); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInDown({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition( + position: position, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart new file mode 100644 index 00000000..7f21192a --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart @@ -0,0 +1,22 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInLeft extends AnimationEffect { + static const Offset beginValue = Offset(-1, 0); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInLeft({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart new file mode 100644 index 00000000..a6af2f25 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart @@ -0,0 +1,23 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInRight extends AnimationEffect { + static const Offset beginValue = Offset(1, 0); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInRight( + {super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart new file mode 100644 index 00000000..cafb2d93 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart @@ -0,0 +1,22 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; + +class SlideInUp extends AnimationEffect { + static const Offset beginValue = Offset(0, -1); + static const Offset endValue = Offset(0, 0); + final Offset? begin; + final Offset? end; + + SlideInUp({super.delay, super.duration, super.curve, this.begin, this.end}); + + @override + Widget build(BuildContext context, Widget child, Animation animation, + EffectEntry entry, Duration totalDuration) { + final Animation position = buildAnimation(entry, totalDuration, + begin: begin ?? beginValue, end: end ?? endValue) + .animate(animation); + return ClipRect( + clipBehavior: Clip.hardEdge, + child: SlideTransition(position: position, child: child)); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart new file mode 100644 index 00000000..65bd1d39 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart @@ -0,0 +1,903 @@ +import 'dart:math'; + +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import '../component/drag_listener.dart'; +import '../model/motion_data.dart'; +import 'motion_list_base.dart'; + +part '../component/drag_item.dart'; + +part '../component/motion_animated_content.dart'; + +typedef CustomAnimatedWidgetBuilder = Widget Function( + BuildContext context, Widget child, Animation animation); + +class MotionBuilder extends StatefulWidget { + final CustomAnimatedWidgetBuilder insertAnimationBuilder; + final CustomAnimatedWidgetBuilder removeAnimationBuilder; + final ReorderCallback? onReorder; + final void Function(int index)? onReorderStart; + final void Function(int index)? onReorderEnd; + + final ReorderItemProxyDecorator? proxyDecorator; + final ItemBuilder itemBuilder; + final int initialCount; + final Axis scrollDirection; + final SliverGridDelegate? delegateBuilder; + final bool buildDefaultDragHandles; + final bool longPressDraggable; + final bool useDefaultDragListeners; + + const MotionBuilder( + {Key? key, + required this.itemBuilder, + required this.insertAnimationBuilder, + required this.removeAnimationBuilder, + this.onReorder, + this.onReorderEnd, + this.onReorderStart, + this.proxyDecorator, + this.initialCount = 0, + this.delegateBuilder, + this.scrollDirection = Axis.vertical, + required this.buildDefaultDragHandles, + required this.useDefaultDragListeners, + this.longPressDraggable = false}) + : assert(initialCount >= 0), + super(key: key); + + @override + State createState() => MotionBuilderState(); + + static MotionBuilderState of(BuildContext context) { + final MotionBuilderState? result = + context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError( + 'MotionBuilderState.of() called with a context that does not contain a MotionBuilderState.\n' + 'No MotionBuilderState ancestor could be found starting from the ' + 'context that was passed to MotionBuilderState.of(). This can ' + 'happen when the context provided is from the same StatefulWidget that ' + 'built the AnimatedList.' + 'The context used was:\n' + ' $context', + ); + } + return true; + }()); + return result!; + } + + static MotionBuilderState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } +} + +class MotionBuilderState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; + final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; + int _itemsCount = 0; + + Map childrenMap = {}; + final Map _items = + {}; + + OverlayEntry? _overlayEntry; + int? _dragIndex; + _DragInfo? _dragInfo; + int? _insertIndex; + Offset? _finalDropPosition; + MultiDragGestureRecognizer? _recognizer; + int? _recognizerPointer; + EdgeDraggingAutoScroller? _autoScroller; + late ScrollableState _scrollable; + + bool autoScrolling = false; + + Axis get scrollDirection => axisDirectionToAxis(_scrollable.axisDirection); + + bool get _reverse => + _scrollable.axisDirection == AxisDirection.up || + _scrollable.axisDirection == AxisDirection.left; + + bool get isGrid => widget.delegateBuilder != null; + + @override + bool get wantKeepAlive => false; + + @override + void initState() { + _itemsCount = widget.initialCount; + for (int i = 0; i < widget.initialCount; i++) { + childrenMap[i] = MotionData(); + } + + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scrollable = Scrollable.of(context); + } + + @override + void didUpdateWidget(covariant MotionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialCount != oldWidget.initialCount) { + cancelReorder(); + } + } + + void startItemDragReorder( + {required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer}) { + assert(0 <= index && index < _itemsCount); + setState(() { + if (_dragInfo != null) { + cancelReorder(); + } else if (_recognizer != null && _recognizerPointer != event.pointer) { + _recognizer!.dispose(); + _recognizer = null; + _recognizerPointer = null; + } + if (_items.containsKey(index)) { + _dragIndex = index; + _recognizer = recognizer + ..onStart = _dragStart + ..addPointer(event); + _recognizerPointer = event.pointer; + } else { + throw Exception("Attempting ro start drag on a non-visible item"); + } + }); + } + + Drag? _dragStart(Offset position) { + assert(_dragInfo == null); + final MotionAnimatedContentState item = _items[_dragIndex]!; + item.dragging = true; + widget.onReorderStart?.call(_dragIndex!); + item.rebuild(); + _insertIndex = item.index; + _dragInfo = _DragInfo( + item: item, + initialPosition: position, + scrollDirection: scrollDirection, + gridView: isGrid, + onUpdate: _dragUpdate, + onCancel: _dragCancel, + onEnd: _dragEnd, + onDragCompleted: _dropCompleted, + proxyDecorator: widget.proxyDecorator, + tickerProvider: this); + + _dragInfo!.startDrag(); + item.dragSize = _dragInfo!.itemSize; + + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + assert(_overlayEntry == null); + _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); + overlay.insert(_overlayEntry!); + + for (final MotionAnimatedContentState childItem in _items.values) { + if (childItem == item || !childItem.mounted) { + continue; + } + item.updateForGap(false); + } + return _dragInfo; + } + + void _dragUpdate(_DragInfo item, Offset position, Offset delta) { + setState(() { + _overlayEntry?.markNeedsBuild(); + _dragUpdateItems(); + _autoScrollIfNecessary(); + }); + } + + void _dragCancel(_DragInfo item) { + setState(() { + _dragReset(); + }); + } + + Future _autoScrollIfNecessary() async { + if (autoScrolling || _dragInfo == null || _dragInfo!.scrollable == null) { + return; + } + + final position = _dragInfo!.scrollable!.position; + double? newOffset; + + const duration = Duration(milliseconds: 14); + const step = 1.0; + const overDragMax = 20.0; + const overDragCoef = 10; + + final isVertical = widget.scrollDirection == Axis.vertical; + + /// get the scroll window position on the screen + final scrollRenderBox = + _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox; + final Offset scrollPosition = scrollRenderBox.localToGlobal(Offset.zero); + + /// calculate the start and end position for the scroll window + double scrollWindowStart = + isVertical ? scrollPosition.dy : scrollPosition.dx; + double scrollWindowEnd = scrollWindowStart + + (isVertical ? scrollRenderBox.size.height : scrollRenderBox.size.width); + + /// get the proxy (dragged) object's position on the screen + final proxyObjectPosition = _dragInfo!.dragPosition - _dragInfo!.dragOffset; + + /// calculate the start and end position for the proxy object + double proxyObjectStart = + isVertical ? proxyObjectPosition.dy : proxyObjectPosition.dx; + double proxyObjectEnd = proxyObjectStart + + (isVertical ? _dragInfo!.itemSize.height : _dragInfo!.itemSize.width); + + if (!_reverse) { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels > position.minScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels < position.maxScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + } else { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels < position.maxScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels > position.minScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + } + + if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) { + autoScrolling = true; + await position.animateTo( + newOffset, + duration: duration, + curve: Curves.linear, + ); + autoScrolling = false; + if (_dragInfo != null) { + _dragUpdateItems(); + _autoScrollIfNecessary(); + } + } + } + + void _dragEnd(_DragInfo item) { + widget.onReorderEnd?.call(_insertIndex!); + setState(() => _finalDropPosition = _itemOffsetAt(_insertIndex!)); + } + + void _dropCompleted() { + final int fromIndex = _dragIndex!; + final int toIndex = _insertIndex!; + if (fromIndex != toIndex) { + widget.onReorder?.call(fromIndex, toIndex); + } + setState(() { + _dragReset(); + }); + } + + void cancelReorder() { + setState(() { + _dragReset(); + }); + } + + void _dragReset() { + if (_dragInfo != null) { + if (_dragIndex != null && _items.containsKey(_dragIndex)) { + final MotionAnimatedContentState dragItem = _items[_dragIndex]!; + dragItem.dragging = false; + dragItem.dragSize = Size.zero; + dragItem.rebuild(); + _dragIndex = null; + } + _dragInfo?.dispose(); + _dragInfo = null; + _autoScroller?.stopAutoScroll(); + _resetItemGap(); + _recognizer?.dispose(); + _recognizer = null; + _overlayEntry?.remove(); + _overlayEntry?.dispose(); + _overlayEntry = null; + _finalDropPosition = null; + } + } + + void _resetItemGap() { + for (final MotionAnimatedContentState item in _items.values) { + item.resetGap(); + } + } + + void _dragUpdateItems() { + assert(_dragInfo != null); + + int newIndex = _insertIndex!; + + final dragCenter = _dragInfo!.itemSize + .center(_dragInfo!.dragPosition - _dragInfo!.dragOffset); + + for (final MotionAnimatedContentState item in _items.values) { + if (!item.mounted) continue; + final Rect geometry = item.targetGeometryNonOffset(); + + if (geometry.contains(dragCenter)) { + newIndex = item.index; + break; + } + } + + if (newIndex == _insertIndex) return; + _insertIndex = newIndex; + + for (final MotionAnimatedContentState item in _items.values) { + if (item.index == _dragIndex) continue; + item.updateForGap(true); + } + } + + Offset calculateNextDragOffset(int index) { + int minPos = min(_dragIndex!, _insertIndex!); + int maxPos = max(_dragIndex!, _insertIndex!); + if (index < minPos || index > maxPos) return Offset.zero; + + final int direction = _insertIndex! > _dragIndex! ? -1 : 1; + if (isGrid) { + return _itemOffsetAt(index + direction) - _itemOffsetAt(index); + } else { + final Offset offset = + _extentOffset(_dragInfo!.itemExtent, scrollDirection); + return _insertIndex! > _dragIndex! ? -offset : offset; + } + } + + void registerItem(MotionAnimatedContentState item) { + _items[item.index] = item; + if (item.index == _dragInfo?.index) { + item.dragging = true; + item.dragSize = _dragInfo!.itemSize; + item.rebuild(); + } + } + + void unregisterItem(int index, MotionAnimatedContentState item) { + final MotionAnimatedContentState? currentItem = _items[index]; + if (currentItem == item) { + _items.remove(index); + } + } + + @override + void dispose() { + for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) { + item.controller?.dispose(); + } + _dragReset(); + super.dispose(); + } + + _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { + final int i = binarySearch(items, _ActiveItem.index(itemIndex)); + return i == -1 ? null : items.removeAt(i); + } + + _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) { + final int i = binarySearch(items, _ActiveItem.index(itemIndex)); + return i == -1 ? null : items[i]; + } + + int _indexToItemIndex(int index) { + int itemIndex = index; + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex <= itemIndex) { + itemIndex += 1; + } else { + break; + } + } + return itemIndex; + } + + int _itemIndexToIndex(int itemIndex) { + int index = itemIndex; + for (final _ActiveItem item in _outgoingItems) { + assert(item.itemIndex != itemIndex); + if (item.itemIndex < itemIndex) { + index -= 1; + } else { + break; + } + } + return index; + } + + void insertItem(int index, {required Duration insertDuration}) { + assert(index >= 0); + final int itemIndex = _indexToItemIndex(index); + assert(itemIndex >= 0 && itemIndex <= _itemsCount); + + for (final _ActiveItem item in _incomingItems) { + if (item.itemIndex >= itemIndex) item.itemIndex += 1; + } + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex >= itemIndex) item.itemIndex += 1; + } + + final AnimationController controller = AnimationController( + duration: insertDuration, + vsync: this, + ); + final AnimationController sizeController = AnimationController( + duration: kAnimationDuration, + vsync: this, + ); + + final _ActiveItem incomingItem = _ActiveItem.animation( + controller, + itemIndex, + sizeController, + ); + + _incomingItems + ..add(incomingItem) + ..sort(); + + final motionData = + MotionData(endOffset: Offset.zero, startOffset: Offset.zero); + + final updatedChildrenMap = {}; + + if (childrenMap.containsKey(itemIndex)) { + for (final entry in childrenMap.entries) { + if (entry.key == itemIndex) { + updatedChildrenMap[itemIndex] = motionData.copyWith(visible: false); + updatedChildrenMap[entry.key + 1] = entry.value.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: getChildOffset(entry.key)); + } else if (entry.key > itemIndex) { + updatedChildrenMap[entry.key + 1] = entry.value.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: getChildOffset(entry.key)); + } else { + updatedChildrenMap[entry.key] = entry.value; + } + } + childrenMap.clear(); + childrenMap.addAll(updatedChildrenMap); + sizeController.forward().then((value) { + controller.forward().then((_) { + _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! + .controller! + .dispose(); + }); + }); + } else { + childrenMap[itemIndex] = motionData; + sizeController.value = kAlwaysCompleteAnimation.value; + controller.forward().then((_) { + _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)! + .controller! + .dispose(); + }); + } + setState(() { + _itemsCount += 1; + }); + } + + void removeItem(int index, {required Duration removeItemDuration}) { + assert(index >= 0); + final int itemIndex = _indexToItemIndex(index); + if (itemIndex < 0 || itemIndex >= _itemsCount) { + return; + } + assert(itemIndex >= 0 && itemIndex < _itemsCount); + + assert(_activeItemAt(_outgoingItems, itemIndex) == null); + + if (childrenMap.containsKey(itemIndex)) { + final _ActiveItem? incomingItem = + _removeActiveItemAt(_incomingItems, itemIndex); + + final AnimationController sizeController = incomingItem?.sizeAnimation ?? + AnimationController( + vsync: this, duration: kAnimationDuration, value: 1.0); + final AnimationController controller = incomingItem?.controller ?? + AnimationController( + duration: removeItemDuration, value: 1.0, vsync: this) + ..addStatusListener((status) => ()); + final _ActiveItem outgoingItem = + _ActiveItem.animation(controller, itemIndex, sizeController); + _outgoingItems + ..add(outgoingItem) + ..sort(); + + controller.reverse().then((void value) { + if (controller.status == AnimationStatus.dismissed) { + if (childrenMap.containsKey(itemIndex)) { + childrenMap.update( + itemIndex, (value) => value.copyWith(visible: false)); + } + sizeController.reverse(from: 1.0).then((value) { + final removedItem = + _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!; + removedItem.controller!.dispose(); + removedItem.sizeAnimation!.dispose(); + + // Decrement the incoming and outgoing item indices to account + // for the removal. + for (final _ActiveItem item in _incomingItems) { + if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; + } + for (final _ActiveItem item in _outgoingItems) { + if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1; + } + _onItemRemoved(itemIndex, removeItemDuration); + }); + } + }); + } + } + + void _onItemRemoved(int itemIndex, Duration removeDuration) { + final updatedChildrenMap = {}; + if (childrenMap.containsKey(itemIndex)) { + for (final entry in childrenMap.entries) { + if (entry.key < itemIndex) { + updatedChildrenMap[entry.key] = childrenMap[entry.key]!; + } else if (entry.key == itemIndex) { + continue; + } else { + updatedChildrenMap[entry.key - 1] = childrenMap[entry.key]!.copyWith( + startOffset: _itemOffsetAt(entry.key), + endOffset: _itemOffsetAt(entry.key - 1)); + } + } + } + childrenMap.clear(); + childrenMap.addAll(updatedChildrenMap); + + setState(() => _itemsCount -= 1); + } + + Offset getChildOffset(int index) { + final currentOffset = _itemOffsetAt(index); + if (!isGrid) { + return currentOffset; + } + + if (widget.delegateBuilder + is SliverReorderableGridDelegateWithFixedCrossAxisCount) { + final delegateBuilder = widget.delegateBuilder + as SliverReorderableGridDelegateWithFixedCrossAxisCount; + return delegateBuilder.getOffset(index, currentOffset); + } else if (widget.delegateBuilder + is SliverReorderableGridWithMaxCrossAxisExtent) { + final delegateBuilder = + widget.delegateBuilder as SliverReorderableGridWithMaxCrossAxisExtent; + final offset = delegateBuilder.getOffset(index, currentOffset); + return offset; + } + return Offset.zero; + } + + Offset _itemOffsetAt(int index) { + final itemRenderBox = + _items[index]?.context.findRenderObject() as RenderBox?; + if (itemRenderBox == null) return Offset.zero; + return itemRenderBox.localToGlobal(Offset.zero); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.delegateBuilder != null + ? SliverGrid( + gridDelegate: widget.delegateBuilder!, delegate: _createDelegate()) + : SliverList(delegate: _createDelegate()); + } + + Widget _itemBuilder(BuildContext context, int index) { + final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, index); + final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, index); + + if (outgoingItem != null) { + final child = _items[index]!.widget; + return _removeItemBuilder(outgoingItem, child); + } + if (_dragInfo != null && index >= _itemsCount) { + return SizedBox.fromSize(size: _dragInfo!.itemSize); + } + + final Widget child = widget.onReorder != null + ? reorderableItemBuilder(context, _itemIndexToIndex(index)) + : widget.itemBuilder(context, _itemIndexToIndex(index)); + + assert(() { + if (child.key == null) { + throw FlutterError( + 'Every item of AnimatedReorderableList must have a unique key.', + ); + } + return true; + }()); + + final Key itemGlobalKey = _MotionBuilderItemGlobalKey(child.key!, this); + final Widget builder = _insertItemBuilder(incomingItem, child); + + final motionData = childrenMap[index]; + if (motionData == null) return builder; + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + + return MotionAnimatedContent( + index: index, + key: itemGlobalKey, + motionData: motionData, + isGrid: isGrid, + updateMotionData: (MotionData motionData) { + final itemOffset = _itemOffsetAt(index); + childrenMap[index] = motionData.copyWith( + startOffset: itemOffset, endOffset: itemOffset, visible: true); + }, + capturedThemes: + InheritedTheme.capture(from: context, to: overlay.context), + child: builder, + ); + } + + SliverChildDelegate _createDelegate() { + return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); + } + + Widget reorderableItemBuilder(BuildContext context, int index) { + final Widget item = widget.itemBuilder(context, index); + + assert(() { + if (item.key == null) { + throw FlutterError( + 'Every item of AnimatedReorderableList must have a key.', + ); + } + return true; + }()); + final Key itemGlobalKey = _MotionBuilderItemGlobalKey(item.key!, this); + final Widget itemWithSemantics = _wrapWithSemantics(item, index); + + // if (widget.useDefaultDragListeners) { + if (!widget.longPressDraggable) { + return _wrapWithSemantics(item, index, itemGlobalKey); + } + if (widget.buildDefaultDragHandles) { + switch (Theme.of(context).platform) { + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + switch (widget.scrollDirection) { + case Axis.horizontal: + return Stack( + key: itemGlobalKey, + children: [ + itemWithSemantics, + Positioned.directional( + textDirection: Directionality.of(context), + start: 0, + end: 0, + bottom: 8, + child: Align( + alignment: Alignment.bottomCenter, + child: ReorderableDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ), + )) + ], + ); + case Axis.vertical: + return Stack( + key: itemGlobalKey, + children: [ + itemWithSemantics, + Positioned.directional( + textDirection: Directionality.of(context), + top: 0, + bottom: 0, + end: 8, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: ReorderableGridDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ), + )) + ], + ); + } + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + return ReorderableGridDelayedDragStartListener( + key: itemGlobalKey, index: index, child: item); + } + } + + const bool enable = true; + + return ReorderableGridDelayedDragStartListener( + key: itemGlobalKey, + index: index, + enabled: enable, + child: itemWithSemantics, + ); + } + + Widget _wrapWithSemantics(Widget child, int index, [dynamic key]) { + void reorder(int startIndex, int endIndex) { + if (startIndex != endIndex) { + widget.onReorder?.call(startIndex, endIndex); + } + } + + // First, determine which semantics actions apply. + final Map semanticsActions = + {}; + + // Create the appropriate semantics actions. + void moveToStart() => reorder(index, 0); + void moveToEnd() => reorder(index, _itemsCount); + void moveBefore() => reorder(index, index - 1); + // To move after, we go to index+2 because we are moving it to the space + // before index+2, which is after the space at index+1. + void moveAfter() => reorder(index, index + 2); + + final WidgetsLocalizations localizations = WidgetsLocalizations.of(context); + + // If the item can move to before its current position in the grid. + if (index > 0) { + semanticsActions[ + CustomSemanticsAction(label: localizations.reorderItemToStart)] = + moveToStart; + String reorderItemBefore = localizations.reorderItemUp; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemBefore = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemLeft + : localizations.reorderItemRight; + } + semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = + moveBefore; + } + + // If the item can move to after its current position in the grid. + if (index < _itemsCount - 1) { + String reorderItemAfter = localizations.reorderItemDown; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemAfter = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemRight + : localizations.reorderItemLeft; + } + semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = + moveAfter; + semanticsActions[ + CustomSemanticsAction(label: localizations.reorderItemToEnd)] = + moveToEnd; + } + + // We pass toWrap with a GlobalKey into the item so that when it + // gets dragged, the accessibility framework can preserve the selected + // state of the dragging item. + // + // We also apply the relevant custom accessibility actions for moving the item + // up, down, to the start, and to the end of the grid. + return MergeSemantics( + key: key, + child: Semantics( + customSemanticsActions: semanticsActions, + child: child, + ), + ); + } + + Widget _removeItemBuilder(_ActiveItem outgoingItem, Widget child) { + final Animation animation = + outgoingItem.controller ?? kAlwaysCompleteAnimation; + final Animation sizeAnimation = + outgoingItem.sizeAnimation ?? kAlwaysCompleteAnimation; + return SizeTransition( + sizeFactor: sizeAnimation, + child: widget.removeAnimationBuilder(context, child, animation)); + } + + Widget _insertItemBuilder(_ActiveItem? incomingItem, Widget child) { + final Animation animation = + incomingItem?.controller ?? kAlwaysCompleteAnimation; + final Animation sizeAnimation = + incomingItem?.sizeAnimation ?? kAlwaysCompleteAnimation; + return SizeTransition( + axis: widget.scrollDirection, + sizeFactor: sizeAnimation, + child: widget.insertAnimationBuilder(context, child, animation)); + } +} + +Offset _extentOffset(double extent, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return Offset(extent, 0.0); + case Axis.vertical: + return Offset(0.0, extent); + } +} + +@optionalTypeArgs +class _MotionBuilderItemGlobalKey extends GlobalObjectKey { + const _MotionBuilderItemGlobalKey(this.subKey, this.state) : super(subKey); + + final Key subKey; + final State state; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _MotionBuilderItemGlobalKey && + other.subKey == subKey && + other.state == state; + } + + @override + int get hashCode => Object.hash( + subKey, + state, + ); +} + +class _ActiveItem implements Comparable<_ActiveItem> { + _ActiveItem.animation(this.controller, this.itemIndex, this.sizeAnimation); + + _ActiveItem.index(this.itemIndex) + : controller = null, + sizeAnimation = null; + + final AnimationController? controller; + final AnimationController? sizeAnimation; + int itemIndex; + + @override + int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart new file mode 100644 index 00000000..843a9e75 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart @@ -0,0 +1,253 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'motion_animated_builder.dart'; + +typedef ItemBuilder = Widget Function( + BuildContext context, int index); + +typedef AnimatedWidgetBuilder = Widget Function( + Widget child, Animation animation); + +typedef EqualityChecker = bool Function(E, E); + +const Duration kAnimationDuration = Duration(milliseconds: 300); + +abstract class MotionListBase + extends StatefulWidget { + final ItemBuilder itemBuilder; + final List items; + final ReorderCallback? onReorder; + final void Function(int)? onReorderStart; + final void Function(int)? onReorderEnd; + final ReorderItemProxyDecorator? proxyDecorator; + final List? enterTransition; + final List? exitTransition; + final Duration? insertDuration; + final Duration? removeDuration; + final Axis scrollDirection; + final SliverGridDelegate? sliverGridDelegate; + final AnimatedWidgetBuilder? insertItemBuilder; + final AnimatedWidgetBuilder? removeItemBuilder; + final bool? buildDefaultDragHandles; + final bool useDefaultDragListeners; + final bool? longPressDraggable; + final bool Function(E a, E b)? isSameItem; + + const MotionListBase( + {super.key, + required this.items, + required this.itemBuilder, + this.onReorder, + this.onReorderEnd, + this.onReorderStart, + this.proxyDecorator, + this.enterTransition, + this.exitTransition, + this.insertDuration, + this.removeDuration, + required this.scrollDirection, + this.sliverGridDelegate, + this.insertItemBuilder, + this.removeItemBuilder, + this.buildDefaultDragHandles, + this.longPressDraggable, + required this.useDefaultDragListeners, + this.isSameItem}); +} + +abstract class MotionListBaseState< + W extends Widget, + B extends MotionListBase, + E extends Object> extends State with TickerProviderStateMixin { + late List oldList; + + Duration _enterDuration = kAnimationDuration; + Duration _exitDuration = kAnimationDuration; + + List _enterAnimations = []; + List _exitAnimations = []; + + Duration get enterDuration => _enterDuration; + + Duration get exitDuration => _exitDuration; + + @protected + GlobalKey listKey = GlobalKey(); + + @nonVirtual + @protected + MotionBuilderState get list => listKey.currentState!; + + @nonVirtual + @protected + ItemBuilder get itemBuilder => widget.itemBuilder; + + @nonVirtual + @protected + SliverGridDelegate? get sliverGridDelegate => widget.sliverGridDelegate; + + @nonVirtual + @protected + ReorderCallback? get onReorder => widget.onReorder; + + @nonVirtual + @protected + void Function(int)? get onReorderStart => widget.onReorderStart; + + @nonVirtual + @protected + void Function(int)? get onReorderEnd => widget.onReorderEnd; + + @nonVirtual + @protected + ReorderItemProxyDecorator? get proxyDecorator => widget.proxyDecorator; + + @nonVirtual + @protected + Duration get insertDuration => widget.insertDuration ?? enterDuration; + + @nonVirtual + @protected + Duration get removeDuration => widget.removeDuration ?? exitDuration; + + @protected + @nonVirtual + Axis get scrollDirection => widget.scrollDirection; + + @nonVirtual + @protected + List get enterTransition => widget.enterTransition ?? []; + + @nonVirtual + @protected + List get exitTransition => widget.exitTransition ?? []; + + @nonVirtual + @protected + bool get buildDefaultDragHandles => widget.buildDefaultDragHandles ?? false; + + @nonVirtual + @protected + bool get longPressDraggable => widget.longPressDraggable ?? false; + + @nonVirtual + @protected + bool get useDefaultDragListeners => widget.useDefaultDragListeners ?? true; + + @nonVirtual + @protected + bool Function(E a, E b) get isSameItem => + widget.isSameItem ?? (a, b) => a == b; + + @override + void initState() { + super.initState(); + oldList = List.from(widget.items); + + addEffects(enterTransition, _enterAnimations, enter: true); + addEffects(exitTransition, _exitAnimations, enter: false); + } + + @override + void didUpdateWidget(covariant B oldWidget) { + final newList = widget.items; + if (!listEquals(oldWidget.enterTransition, enterTransition)) { + _enterAnimations = []; + addEffects(enterTransition, _enterAnimations, enter: true); + } + if (!listEquals(oldWidget.exitTransition, exitTransition)) { + _exitAnimations = []; + addEffects(exitTransition, _exitAnimations, enter: false); + } + calculateDiff(oldList, newList); + oldList = List.from(newList); + super.didUpdateWidget(oldWidget); + } + + void addEffects(List effects, List enteries, + {required bool enter}) { + if (effects.isNotEmpty) { + for (AnimationEffect effect in effects) { + addEffect(effect, enteries, enter: enter); + } + } else { + addEffect(FadeIn(), enteries, enter: enter); + } + } + + void addEffect(AnimationEffect effect, List enteries, + {required bool enter}) { + Duration zero = Duration.zero; + final timeForAnimation = + (effect.delay ?? zero) + (effect.duration ?? kAnimationDuration); + if (enter) { + _enterDuration = + timeForAnimation > _enterDuration ? timeForAnimation : _enterDuration; + assert(_enterDuration >= zero, "Duration can not be negative"); + } else { + _exitDuration = + timeForAnimation > _exitDuration ? timeForAnimation : _exitDuration; + assert(_exitDuration >= zero, "Duration can not be negative"); + } + + EffectEntry entry = EffectEntry( + animationEffect: effect, + delay: effect.delay ?? zero, + duration: effect.duration ?? kAnimationDuration, + curve: effect.curve ?? Curves.linear); + + enteries.add(entry); + } + + void calculateDiff(List oldList, List newList) { + // Detect removed and updated items + for (int i = oldList.length - 1; i >= 0; i--) { + if (newList.indexWhere((element) => isSameItem(oldList[i], element)) == + -1) { + listKey.currentState!.removeItem(i, removeItemDuration: removeDuration); + } + } + // Detect added items + for (int i = 0; i < newList.length; i++) { + if (oldList.indexWhere((element) => isSameItem(newList[i], element)) == + -1) { + listKey.currentState!.insertItem(i, insertDuration: insertDuration); + } + } + } + + @nonVirtual + @protected + Widget insertAnimationBuilder( + BuildContext context, Widget child, Animation animation) { + if (widget.insertItemBuilder != null) { + return widget.insertItemBuilder!(child, animation); + } else { + Widget animatedChild = child; + for (EffectEntry entry in _enterAnimations) { + animatedChild = entry.animationEffect + .build(context, animatedChild, animation, entry, insertDuration); + } + return animatedChild; + } + } + + @nonVirtual + @protected + Widget removeAnimationBuilder( + BuildContext context, Widget child, Animation animation) { + if (widget.removeItemBuilder != null) { + return widget.removeItemBuilder!(child, animation); + } else { + Widget animatedChild = child; + for (EffectEntry entry in _exitAnimations) { + animatedChild = entry.animationEffect + .build(context, animatedChild, animation, entry, removeDuration); + } + return animatedChild; + } + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart new file mode 100644 index 00000000..903e9d9d --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart @@ -0,0 +1,97 @@ +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart'; +import 'package:flutter/material.dart'; + +import 'motion_animated_builder.dart'; +import 'motion_list_base.dart'; + +class MotionListImpl extends MotionListBase { + const MotionListImpl({ + super.key, + required super.items, + required super.itemBuilder, + super.enterTransition, + super.exitTransition, + super.insertDuration, + super.removeDuration, + super.onReorder, + super.onReorderStart, + super.onReorderEnd, + super.proxyDecorator, + required super.scrollDirection, + super.insertItemBuilder, + super.removeItemBuilder, + super.buildDefaultDragHandles, + super.useDefaultDragListeners = true, + super.longPressDraggable, + super.isSameItem, + }); + + const MotionListImpl.grid({ + Key? key, + required List items, + required ItemBuilder itemBuilder, + required SliverGridDelegate sliverGridDelegate, + List? enterTransition, + List? exitTransition, + ReorderCallback? onReorder, + void Function(int)? onReorderStart, + void Function(int)? onReorderEnd, + ReorderItemProxyDecorator? proxyDecorator, + Duration? insertDuration, + Duration? removeDuration, + required Axis scrollDirection, + AnimatedWidgetBuilder? insertItemBuilder, + AnimatedWidgetBuilder? removeItemBuilder, + bool? buildDefaultDragHandles, + bool useDefaultDragListeners = true, + bool? longPressDraggable, + bool Function(E a, E b)? isSameItem, + }) : super( + key: key, + items: items, + itemBuilder: itemBuilder, + sliverGridDelegate: sliverGridDelegate, + enterTransition: enterTransition, + exitTransition: exitTransition, + insertDuration: insertDuration, + removeDuration: removeDuration, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + scrollDirection: scrollDirection, + insertItemBuilder: insertItemBuilder, + removeItemBuilder: removeItemBuilder, + buildDefaultDragHandles: buildDefaultDragHandles, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + isSameItem: isSameItem); + + @override + MotionListImplState createState() => MotionListImplState(); +} + +class MotionListImplState + extends MotionListBaseState, E> { + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + assert(debugCheckHasOverlay(context)); + return MotionBuilder( + key: listKey, + initialCount: oldList.length, + onReorder: onReorder, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + proxyDecorator: proxyDecorator, + insertAnimationBuilder: insertAnimationBuilder, + removeAnimationBuilder: removeAnimationBuilder, + itemBuilder: itemBuilder, + scrollDirection: scrollDirection, + delegateBuilder: sliverGridDelegate, + buildDefaultDragHandles: buildDefaultDragHandles, + longPressDraggable: longPressDraggable, + useDefaultDragListeners: useDefaultDragListeners, + ); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart new file mode 100644 index 00000000..7c731edf --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart @@ -0,0 +1,181 @@ +part of '../builder/motion_animated_builder.dart'; + +typedef _DragItemUpdate = void Function( + _DragInfo item, Offset position, Offset delta); +typedef _DragItemCallback = void Function(_DragInfo item); + +class _DragInfo extends Drag { + final bool gridView; + final Axis scrollDirection; + final _DragItemUpdate? onUpdate; + final _DragItemCallback? onEnd; + final _DragItemCallback? onCancel; + final VoidCallback? onDragCompleted; + final ReorderItemProxyDecorator? proxyDecorator; + final TickerProvider tickerProvider; + + late MotionBuilderState listState; + late int index; + late Widget child; + late Offset dragPosition; + late Offset dragOffset; + late Size itemSize; + late double itemExtent; + late CapturedThemes capturedThemes; + ScrollableState? scrollable; + AnimationController? _proxyAnimation; + + _DragInfo({ + required MotionAnimatedContentState item, + Offset initialPosition = Offset.zero, + required this.gridView, + this.scrollDirection = Axis.vertical, + this.onUpdate, + this.onEnd, + this.onCancel, + this.onDragCompleted, + this.proxyDecorator, + required this.tickerProvider, + }) { + final RenderBox itemRenderBox = + item.context.findRenderObject()! as RenderBox; + listState = item.listState; + index = item.index; + child = item.widget.child; + capturedThemes = item.widget.capturedThemes!; + dragPosition = initialPosition; + dragOffset = itemRenderBox.globalToLocal(initialPosition); + itemSize = item.context.size!; + itemExtent = _sizeExtent(itemSize, scrollDirection); + scrollable = Scrollable.of(item.context); + } + + void dispose() { + _proxyAnimation?.dispose(); + } + + void startDrag() { + _proxyAnimation = AnimationController( + vsync: tickerProvider, duration: const Duration(milliseconds: 250)) + ..addStatusListener((status) { + if (status == AnimationStatus.dismissed) { + _dropCompleted(); + } + }) + ..forward(); + } + + @override + void update(DragUpdateDetails details) { + final Offset delta = !gridView + ? _restrictAxis(details.delta, scrollDirection) + : details.delta; + dragPosition += delta; + onUpdate?.call(this, dragPosition, details.delta); + } + + @override + void end(DragEndDetails details) { + _proxyAnimation!.reverse(); + onEnd?.call(this); + } + + @override + void cancel() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onCancel?.call(this); + } + + void _dropCompleted() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onDragCompleted?.call(); + } + + Widget createProxy(BuildContext context) { + return capturedThemes.wrap(_DragItemProxy( + listState: listState, + index: index, + position: dragPosition - dragOffset - _overlayOrigin(context), + size: itemSize, + animation: _proxyAnimation!, + proxyDecorator: proxyDecorator, + child: child)); + } +} + +double _sizeExtent(Size size, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return size.width; + case Axis.vertical: + return size.height; + } +} + +Offset _restrictAxis(Offset offset, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return Offset(offset.dx, 0.0); + case Axis.vertical: + return Offset(0.0, offset.dy); + } +} + +Offset _overlayOrigin(BuildContext context) { + final OverlayState overlay = + Overlay.of(context, debugRequiredFor: context.widget); + final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; + return overlayBox.localToGlobal(Offset.zero); +} + +class _DragItemProxy extends StatelessWidget { + final MotionBuilderState listState; + final int index; + final Widget child; + final Offset position; + final Size size; + final AnimationController animation; + final ReorderItemProxyDecorator? proxyDecorator; + + const _DragItemProxy( + {required this.listState, + required this.index, + required this.child, + required this.position, + required this.size, + required this.animation, + required this.proxyDecorator}); + + @override + Widget build(BuildContext context) { + final Widget proxyChild = + proxyDecorator?.call(child, index, animation.view) ?? child; + final Offset overlayOrigin = _overlayOrigin(context); + return MediaQuery( + data: MediaQuery.of(context).removePadding(removeTop: true), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + Offset effectivePosition = position; + final Offset? dropPosition = listState._finalDropPosition; + if (dropPosition != null) { + effectivePosition = Offset.lerp( + dropPosition - overlayOrigin, + effectivePosition, + Curves.easeOut.transform(animation.value))!; + } + return Positioned( + left: effectivePosition.dx, + top: effectivePosition.dy, + child: SizedBox( + width: size.width, + height: size.height, + child: child, + )); + }, + child: proxyChild, + )); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart new file mode 100644 index 00000000..e791f9ff --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart @@ -0,0 +1,94 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; + +import '../builder/motion_animated_builder.dart'; + +class ReorderableGridDragStartListener extends StatelessWidget { + /// Creates a listener for a drag immediately following a pointer down + /// event over the given child widget. + /// + /// This is most commonly used to wrap part of a grid item like a drag + /// handle. + const ReorderableGridDragStartListener({ + super.key, + required this.child, + required this.index, + this.enabled = true, + }); + + /// The widget for which the application would like to respond to a tap and + /// drag gesture by starting a reordering drag on a reorderable grid. + final Widget child; + + /// The index of the associated item that will be dragged in the grid. + final int index; + + /// Whether the [child] item can be dragged and moved in the grid. + /// + /// If true, the item can be moved to another location in the grid when the + /// user taps on the child. If false, tapping on the child will be ignored. + final bool enabled; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: enabled + ? (PointerDownEvent event) => _startDragging(context, event) + : null, + child: child, + ); + } + + /// Provides the gesture recognizer used to indicate the start of a reordering + /// drag operation. + /// + /// By default this returns an [ImmediateMultiDragGestureRecognizer] but + /// subclasses can use this to customize the drag start gesture. + @protected + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(debugOwner: this,delay: const Duration(milliseconds: 1)); + } + + void _startDragging(BuildContext context, PointerDownEvent event) { + final MotionBuilderState? list = MotionBuilder.maybeOf(context); + list?.startItemDragReorder( + index: index, + event: event, + recognizer: createRecognizer(), + ); + } +} + +/// A wrapper widget that will recognize the start of a drag operation by +/// looking for a long press event. Once it is recognized, it will start +/// a drag operation on the wrapped item in the reorderable grid. +/// +/// See also: +/// +/// * [ReorderableGridDragStartListener], a similar wrapper that will +/// recognize the start of the drag immediately after a pointer down event. +/// * [ReorderableGrid], a widget grid that allows the user to reorder +/// its items. +/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder +/// its items. +/// * [ReorderableGridView], a material design grid that allows the user to +/// reorder its items. +class ReorderableGridDelayedDragStartListener + extends ReorderableGridDragStartListener { + /// Creates a listener for an drag following a long press event over the + /// given child widget. + /// + /// This is most commonly used to wrap an entire grid item in a reorderable + /// grid. + const ReorderableGridDelayedDragStartListener({ + super.key, + required super.child, + required super.index, + super.enabled, + }); + + @override + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(debugOwner: this); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart new file mode 100644 index 00000000..882aef28 --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart @@ -0,0 +1,221 @@ +part of '../builder/motion_animated_builder.dart'; + +class MotionAnimatedContent extends StatefulWidget { + final int index; + final MotionData motionData; + final Widget child; + final Function(MotionData)? updateMotionData; + final CapturedThemes? capturedThemes; + final bool isGrid; + + const MotionAnimatedContent({ + Key? key, + required this.index, + required this.motionData, + required this.child, + this.updateMotionData, + required this.capturedThemes, + required this.isGrid, + }) : super(key: key); + + @override + State createState() => MotionAnimatedContentState(); +} + +class MotionAnimatedContentState extends State + with SingleTickerProviderStateMixin { + late MotionBuilderState listState; + + Offset _targetOffset = Offset.zero; + Offset _startOffset = Offset.zero; + AnimationController? _offsetAnimation; + + bool _dragging = false; + + bool get dragging => _dragging; + + set dragging(bool dragging) { + if (mounted) { + setState(() { + _dragging = dragging; + }); + } + } + + Size _dragSize = Size.zero; + + set dragSize(Size itemSize) { + if (mounted) { + setState(() { + _dragSize = itemSize; + }); + } + } + + int get index => widget.index; + bool visible = true; + + @override + void initState() { + listState = MotionBuilder.of(context); + listState.registerItem(this); + visible = widget.motionData.visible; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + widget.updateMotionData?.call(widget.motionData); + }); + Future.delayed(kAnimationDuration).then((value) { + visible = true; + rebuild(); + }); + super.initState(); + } + + @override + void didUpdateWidget(covariant MotionAnimatedContent oldWidget) { + if (oldWidget.index != widget.index) { + listState.unregisterItem(oldWidget.index, this); + listState.registerItem(this); + } + if (oldWidget.index != widget.index && !_dragging && widget.isGrid) { + _updateAnimationTranslation(); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) { + setState(() { + visible = true; + }); + widget.updateMotionData?.call(widget.motionData); + } + }); + super.didUpdateWidget(oldWidget); + } + + void _updateAnimationTranslation() { + Offset offsetDiff = + (widget.motionData.startOffset + offset) - widget.motionData.endOffset; + _startOffset = offsetDiff; + if (offsetDiff.dx != 0 || offsetDiff.dy != 0) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: listState, + duration: kAnimationDuration, + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + widget.updateMotionData?.call(widget.motionData); + + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offsetDiff; + _offsetAnimation!.forward(from: 0.0); + } + } + } + + Offset get offset { + if (_offsetAnimation != null) { + final Offset offset = + Offset.lerp(_startOffset, _targetOffset, _offsetAnimation!.value)!; + return offset; + } + return _targetOffset; + } + + void updateForGap(bool animate) { + if (!mounted) return; + final Offset newTargetOffset = listState.calculateNextDragOffset(index); + if (newTargetOffset == _targetOffset) return; + _targetOffset = newTargetOffset; + + if (animate) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: listState, + duration: const Duration(milliseconds: 250), + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offset; + _offsetAnimation!.forward(from: 0.0); + } + } else { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = _targetOffset; + } + rebuild(); + } + + @override + Widget build(BuildContext context) { + listState.registerItem(this); + return Visibility( + maintainSize: true, + maintainAnimation: true, + maintainState: true, + visible: visible && !_dragging, + child: Transform.translate( + offset: offset, + child: + !_dragging ? widget.child : SizedBox.fromSize(size: _dragSize)), + ); + } + + Offset itemOffset() { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return Offset.zero; + return box.localToGlobal(Offset.zero); + } + + void resetGap() { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = Offset.zero; + _targetOffset = Offset.zero; + rebuild(); + } + + Rect targetGeometryNonOffset() { + final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; + final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero); + return itemPosition & itemRenderBox.size; + } + + void rebuild() { + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + listState.unregisterItem(widget.index, this); + _offsetAnimation?.dispose(); + super.dispose(); + } + + @override + void deactivate() { + listState.unregisterItem(index, this); + super.deactivate(); + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart new file mode 100644 index 00000000..188035ed --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart @@ -0,0 +1,157 @@ +import 'dart:math'; +import 'package:flutter/rendering.dart'; + +class SliverGridWithCustomGeometryLayout extends SliverGridRegularTileLayout { + final SliverGridGeometry Function( + int index, SliverGridRegularTileLayout layout) geometryBuilder; + + const SliverGridWithCustomGeometryLayout({ + required this.geometryBuilder, + required int crossAxisCount, + required double mainAxisStride, + required double crossAxisStride, + required double childMainAxisExtent, + required double childCrossAxisExtent, + required bool reverseCrossAxis, + }) : assert(crossAxisCount > 0), + assert(mainAxisStride >= 0), + assert(crossAxisStride >= 0), + assert(childMainAxisExtent >= 0), + assert(childCrossAxisExtent >= 0), + super( + crossAxisCount: crossAxisCount, + mainAxisStride: mainAxisStride, + crossAxisStride: crossAxisStride, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: reverseCrossAxis); + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + return geometryBuilder(index, this); + } +} + +/// Creates grid layouts with a fixed number of tiles in the cross axis. +/// +/// For example, if the grid is vertical, this delegate will create a layout +/// with a fixed number of columns. If the grid is horizontal, this delegate +/// will create a layout with a fixed number of rows. +/// +/// This delegate creates grids with equally sized and spaced tiles. + +class SliverReorderableGridDelegateWithFixedCrossAxisCount + extends SliverGridDelegateWithFixedCrossAxisCount { + /// The number of children in the cross axis. + final int crossAxisCount; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// The ratio of the cross-axis to the main-axis extent of each child. + final double childAspectRatio; + + /// The extent of each tile in the main axis. If provided it would define the + /// logical pixels taken by each tile in the main-axis. + /// + /// If null, [childAspectRatio] is used instead. + final double? mainAxisExtent; + + double childCrossAxisExtent = 0.0; + double childMainAxisExtent = 0.0; + + /// Creates a delegate that makes grid layouts with a fixed number of tiles in + /// the cross axis. + /// + /// The `mainAxisSpacing`, `mainAxisExtent` and `crossAxisSpacing` arguments + /// must not be negative. The `crossAxisCount` and `childAspectRatio` + /// arguments must be greater than zero. + + SliverReorderableGridDelegateWithFixedCrossAxisCount({ + required this.crossAxisCount, + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.childAspectRatio = 1.0, + this.mainAxisExtent, + }) : assert(crossAxisCount > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(childAspectRatio > 0), + super( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + ); + + bool _debugAssertIsValid() { + assert(crossAxisCount > 0); + assert(mainAxisSpacing >= 0); + assert(crossAxisSpacing >= 0); + assert(childAspectRatio > 0); + return true; + } + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + assert(_debugAssertIsValid()); + final usableCrossAxisCount = max(0.0, + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1)); + + childCrossAxisExtent = usableCrossAxisCount / crossAxisCount; + childMainAxisExtent = childCrossAxisExtent / childAspectRatio; + return SliverGridWithCustomGeometryLayout( + geometryBuilder: (index, layout) { + return SliverGridGeometry( + scrollOffset: (index ~/ crossAxisCount) * layout.mainAxisStride, + crossAxisOffset: _getOffsetFromStartInCrossAxis(index, layout), + mainAxisExtent: childMainAxisExtent, + crossAxisExtent: childCrossAxisExtent); + }, + crossAxisCount: crossAxisCount, + mainAxisStride: childMainAxisExtent + mainAxisSpacing, + crossAxisStride: childCrossAxisExtent + crossAxisSpacing, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: + axisDirectionIsReversed(constraints.crossAxisDirection)); + } + + Offset getOffset(int index, Offset currentOffset) { + final int col = index % crossAxisCount; + final crossAxisStart = crossAxisSpacing; + + if (col == crossAxisCount - 1) { + return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent); + } else { + return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy); + } + } + + double _getOffsetFromStartInCrossAxis( + int index, + SliverGridRegularTileLayout layout, + ) { + final crossAxisStart = (index % crossAxisCount) * layout.crossAxisStride; + + if (layout.reverseCrossAxis) { + return crossAxisCount * layout.crossAxisStride - + crossAxisStart - + layout.childCrossAxisExtent - + (layout.crossAxisStride - layout.childCrossAxisExtent); + } + return crossAxisStart; + } + + @override + bool shouldRelayout(SliverGridDelegateWithFixedCrossAxisCount oldDelegate) { + return oldDelegate.crossAxisCount != crossAxisCount || + oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing || + oldDelegate.childAspectRatio != childAspectRatio || + oldDelegate.mainAxisExtent != mainAxisExtent; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart new file mode 100644 index 00000000..0c06521b --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart @@ -0,0 +1,128 @@ +import 'package:flutter/rendering.dart'; + +import 'dart:math' as math; + +/// Creates grid layouts with tiles that each have a maximum cross-axis extent. +/// +/// This delegate will select a cross-axis extent for the tiles that is as +/// large as possible subject to the following conditions: +/// +/// - The extent evenly divides the cross-axis extent of the grid. +/// - The extent is at most [maxCrossAxisExtent]. +/// +/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and +/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 +/// columns that are 125.0 pixels wide. +/// +/// This delegate creates grids with equally sized and spaced tiles. + +class SliverReorderableGridWithMaxCrossAxisExtent + extends SliverGridDelegateWithMaxCrossAxisExtent { + /// The maximum extent of tiles in the cross axis. + /// + /// This delegate will select a cross-axis extent for the tiles that is as + /// large as possible subject to the following conditions: + /// + /// - The extent evenly divides the cross-axis extent of the grid. + /// - The extent is at most [maxCrossAxisExtent]. + /// + /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and + /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 + /// columns that are 125.0 pixels wide. + final double maxCrossAxisExtent; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// The ratio of the cross-axis to the main-axis extent of each child. + final double childAspectRatio; + + /// The extent of each tile in the main axis. If provided it would define the + /// logical pixels taken by each tile in the main-axis. + /// + /// If null, [childAspectRatio] is used instead. + final double? mainAxisExtent; + int crossAxisCount = 0; + double childCrossAxisExtent = 0.0; + double childMainAxisExtent = 0.0; + + /// Creates a delegate that makes grid layouts with tiles that have a maximum + /// cross-axis extent. + /// + /// The [maxCrossAxisExtent], [mainAxisExtent], [mainAxisSpacing], + /// and [crossAxisSpacing] arguments must not be negative. + /// The [childAspectRatio] argument must be greater than zero. + SliverReorderableGridWithMaxCrossAxisExtent({ + required this.maxCrossAxisExtent, + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.childAspectRatio = 1.0, + this.mainAxisExtent, + }) : assert(maxCrossAxisExtent > 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(childAspectRatio > 0), + super( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + mainAxisExtent: mainAxisExtent); + + bool _debugAssertIsValid(double crossAxisExtent) { + assert(maxCrossAxisExtent > 0); + assert(mainAxisSpacing >= 0); + assert(crossAxisSpacing >= 0); + assert(childAspectRatio > 0); + return true; + } + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + assert(_debugAssertIsValid(constraints.crossAxisExtent)); + int childCrossAxisCount = + (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) + .ceil(); + // Ensure a minimum count of 1, can be zero and result in an infinite extent + // below when the window size is 0. + crossAxisCount = math.max(1, childCrossAxisCount); + final double usableCrossAxisExtent = math.max( + 0.0, + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), + ); + childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; + childMainAxisExtent = + mainAxisExtent ?? childCrossAxisExtent / childAspectRatio; + return SliverGridRegularTileLayout( + crossAxisCount: crossAxisCount, + mainAxisStride: childMainAxisExtent + mainAxisSpacing, + crossAxisStride: childCrossAxisExtent + crossAxisSpacing, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + Offset getOffset(int index, Offset currentOffset) { + final int col = index % crossAxisCount; + final crossAxisStart = crossAxisSpacing; + + if (col == crossAxisCount - 1) { + return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent); + } else { + return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy); + } + } + + @override + bool shouldRelayout(SliverGridDelegateWithMaxCrossAxisExtent oldDelegate) { + return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || + oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing || + oldDelegate.childAspectRatio != childAspectRatio || + oldDelegate.mainAxisExtent != mainAxisExtent; + } +} diff --git a/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart new file mode 100644 index 00000000..06de5a9f --- /dev/null +++ b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart @@ -0,0 +1,30 @@ +import 'package:flutter/cupertino.dart'; + +class MotionData { + final Offset startOffset; + final Offset endOffset; + final bool visible; + + MotionData( + {this.startOffset = Offset.zero, + this.endOffset = Offset.zero, + this.visible = true}); + + MotionData copyWith({Offset? startOffset, Offset? endOffset, bool? visible}) { + return MotionData( + startOffset: startOffset ?? this.startOffset, + endOffset: endOffset ?? this.endOffset, + visible: visible ?? this.visible); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MotionData && + runtimeType == other.runtimeType && + startOffset == other.startOffset && + endOffset == other.endOffset; + + @override + int get hashCode => startOffset.hashCode ^ endOffset.hashCode; +} diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 68337f18..a1baecb2 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -1,25 +1,22 @@ +import 'package:clock_app/common/data/animations.dart'; import 'package:clock_app/common/logic/get_list_filter_chips.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/reorderable_list_decorator.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart'; import 'package:clock_app/common/widgets/list/delete_alert_dialogue.dart'; +import 'package:clock_app/common/widgets/list/list_filter_bar.dart'; import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; import 'package:clock_app/common/widgets/list/list_item_card.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:great_list_view/great_list_view.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -typedef ItemCardBuilder = Widget Function( - BuildContext context, - int index, - AnimatedWidgetBuilderData data, -); - class CustomListView extends StatefulWidget { const CustomListView({ super.key, @@ -77,11 +74,12 @@ class _CustomListViewState late List currentList = List.from(widget.items); double _itemCardHeight = 0; final _scrollController = ScrollController(); - final _controller = AnimatedListController(); + // final _controller = AnimatedListController(); late int _selectedSortIndex = widget.initialSortIndex; late Setting _longPressActionSetting; List _selectedIds = []; bool _isSelecting = false; + // bool _isReordering = false; @override void initState() { @@ -123,10 +121,6 @@ class _CustomListViewState _updateCurrentList(); }); - // TODO: MAN THIS SUCKS, WHY YOU GOTTA DO THIS - _controller.notifyRemovedRange( - 0, widget.items.length - 1, _getChangeListBuilder()); - _controller.notifyInsertedRange(0, widget.items.length); } void _updateCurrentList() { @@ -148,35 +142,11 @@ class _CustomListViewState void _updateItemHeight() { if (_itemCardHeight == 0) { - _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; + // _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; } } - void _notifyChangeList() { - _controller.notifyChangedRange( - 0, - currentList.length, - _getChangeListBuilder(), - ); - } - - ItemCardBuilder _getChangeWidgetBuilder(Item item) { - _updateItemHeight(); - return (context, index, data) => data.measuring - ? SizedBox(height: _itemCardHeight) - : ListItemCard( - key: ValueKey(item), - onTap: () {}, - onDelete: () {}, - onDuplicate: () {}, - child: widget.itemBuilder(item), - ); - } - - ItemCardBuilder _getChangeListBuilder() => (context, index, data) => - _getChangeWidgetBuilder(widget.items[index])(context, index, data); - - bool _handleReorderItems(int oldIndex, int newIndex, Object? slot) { + bool _handleReorderItems(int oldIndex, int newIndex) { if (newIndex >= widget.items.length || _selectedSortIndex != 0) { return false; } @@ -190,77 +160,32 @@ class _CustomListViewState void _handleChangeItems( ItemChangerCallback callback, bool callOnModifyList) { - final initialList = List.from(currentList); - callback(widget.items); setState(() { _updateCurrentList(); }); - final deletedItems = List.from(initialList - .where( - (element) => currentList.where((e) => e.id == element.id).isEmpty) - .toList()); - final addedItems = List.from(currentList - .where( - (element) => initialList.where((e) => e.id == element.id).isEmpty) - .toList()); - - for (var deletedItem in deletedItems) { - _controller.notifyRemovedRange( - initialList.indexWhere((element) => element.id == deletedItem.id), - 1, - _getChangeWidgetBuilder(deletedItem), - ); - } - - for (var addedItem in addedItems) { - _controller.notifyInsertedRange( - currentList.indexWhere((element) => element.id == addedItem.id), - 1, - ); - } - - _notifyChangeList(); - if (callOnModifyList) widget.onModifyList?.call(); } Future _handleDeleteItem(Item deletedItem, [bool callOnModifyList = true]) async { - int index = _getItemIndex(deletedItem); - - // print(listToString(widget.items)); - setState(() { widget.items.removeWhere((element) => element.id == deletedItem.id); _updateCurrentList(); }); - _controller.notifyRemovedRange( - index, - 1, - _getChangeWidgetBuilder(deletedItem), - ); await widget.onDeleteItem?.call(deletedItem); if (callOnModifyList) widget.onModifyList?.call(); } Future _handleDeleteItemList(List deletedItems) async { for (var item in deletedItems) { - int index = _getItemIndex(item); - setState(() { widget.items.removeWhere((element) => element.id == item.id); _updateCurrentList(); }); - - _controller.notifyRemovedRange( - index, - 1, - _getChangeWidgetBuilder(deletedItems.first), - ); } for (var item in deletedItems) { await widget.onDeleteItem?.call(item); @@ -284,12 +209,7 @@ class _CustomListViewState }); int currentListIndex = _getItemIndex(item); - _controller.notifyInsertedRange(currentListIndex, 1); - // _scrollToIndex(index); - // TODO: Remove this delay - Future.delayed(const Duration(milliseconds: 100), () { - _scrollToIndex(currentListIndex); - }); + _scrollToIndex(currentListIndex); _updateItemHeight(); widget.onModifyList?.call(); } @@ -299,9 +219,6 @@ class _CustomListViewState } void _scrollToIndex(int index) { - // if (_scrollController.offset == 0) { - // _scrollController.jumpTo(1); - // } if (_itemCardHeight == 0 && index != 0) return; _scrollController.animateTo(index * _itemCardHeight, duration: const Duration(milliseconds: 250), curve: Curves.easeIn); @@ -310,9 +227,9 @@ class _CustomListViewState void _endSelection() { setState(() { _isSelecting = false; + // _isReordering = false; _selectedIds.clear(); }); - _notifyChangeList(); } void _startSelection(Item item) { @@ -320,7 +237,6 @@ class _CustomListViewState _isSelecting = true; _selectedIds = [item.id]; }); - _notifyChangeList(); } void _handleSelect(Item item) { @@ -334,77 +250,48 @@ class _CustomListViewState if (_selectedIds.isEmpty) { _endSelection(); } - _notifyChangeList(); } - void _handleSelectAll() { + void _handleSortChange(int index) { setState(() { - _selectedIds = widget.items.map((e) => e.id).toList(); + _selectedSortIndex = index; + widget.onChangeSortIndex?.call(index); + _updateCurrentList(); }); - _notifyChangeList(); } - _getItemBuilder() { - return (BuildContext context, Item item, data) { - for (var filter in widget.listFilters) { - // print("${filter.displayName} ${filter.filterFunction}"); - if (!filter.filterFunction(item)) { - return Container(); - } - } - int index = _getItemIndex(item); - var itemWidget = data.measuring - ? SizedBox(height: _itemCardHeight) - : ListItemCard( - key: ValueKey(item), - onTap: () { - return widget.onTapItem?.call(item, index); - }, - onDelete: - widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null, - onDuplicate: () => _handleDuplicateItem(item), - isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, - isDuplicateEnabled: widget.isDuplicateEnabled, - isSelected: _selectedIds.contains(item.id), - child: widget.itemBuilder(item), - ); - if (widget.isSelectable && - _longPressActionSetting.value == LongPressAction.multiSelect) { - itemWidget = GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPress: () { - if (!_isSelecting) { - _startSelection(item); - } else { - _handleSelect(item); - } - }, - onTap: () { - if (_isSelecting) { - _handleSelect(item); - } - }, - child: AbsorbPointer(absorbing: _isSelecting, child: itemWidget), - ); - } - return itemWidget; - }; + void _handleFilterChange() { + setState(() {}); } - void _onFilterChange() { + void _handleSelectAll() { setState(() { - _notifyChangeList(); + _selectedIds = widget.items.map((e) => e.id).toList(); }); } - List _getCurrentList() { - final List items = List.from(widget.items); + void _handleCustomAction(ListFilterCustomAction action) { + final list = _getActionableItems(); - if (_selectedSortIndex != 0) { - items.sort(widget.sortOptions[_selectedSortIndex - 1].sortFunction); - } + action.action(list + .where((item) => + widget.listFilters.every((filter) => filter.filterFunction(item))) + .toList()); + _endSelection(); + } + + void _handleDeleteAction() async { + Navigator.pop(context); + final result = await showDeleteAlertDialogue(context); + if (result == null || result == false) return; + + final list = _getActionableItems(); + final toRemove = List.from(list.where((item) => + widget.listFilters.every((filter) => filter.filterFunction(item)))); + _endSelection(); + await _handleDeleteItemList(toRemove); - return items; + widget.onModifyList?.call(); } List _getActionableItems() { @@ -413,132 +300,73 @@ class _CustomListViewState : widget.items; } + _getItemBuilder() { + return (BuildContext context, int index) { + Item item = currentList[index]; + for (var filter in widget.listFilters) { + if (!filter.filterFunction(item)) { + return Container(key: ValueKey(item)); + } + } + Widget itemWidget = ListItemCard( + key: ValueKey(item.id), + onTap: () { + if (_isSelecting) { + _handleSelect(item); + } else { + return widget.onTapItem?.call(item, index); + } + }, + onLongPress: () { + if (widget.isSelectable && + _longPressActionSetting.value == LongPressAction.multiSelect) { + if (!_isSelecting) { + _startSelection(item); + } else { + _handleSelect(item); + } + } + }, + onDelete: widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null, + onDuplicate: () => _handleDuplicateItem(item), + isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled, + isDuplicateEnabled: widget.isDuplicateEnabled, + isSelected: _selectedIds.contains(item.id), + showReorderHandle: + _isSelecting && widget.isReorderable && _selectedSortIndex == 0, + index: index, + child: widget.itemBuilder(item), + ).animateCard(ValueKey(item)); + return itemWidget; + }; + } + @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; - final isReorderable = - _longPressActionSetting.value == LongPressAction.reorder && - widget.isReorderable; + TextTheme textTheme = theme.textTheme; if (_selectedSortIndex > widget.sortOptions.length) { _updateCurrentList(); } - List getFilterChips() { - List widgets = []; - int activeFilterCount = - widget.listFilters.where((filter) => filter.isActive).length; - if (activeFilterCount > 0 || _isSelecting) { - widgets.add( - ListFilterActionChip( - actions: [ - ListFilterAction( - name: AppLocalizations.of(context)!.clearFiltersAction, - icon: Icons.clear_rounded, - action: () { - for (var filter in widget.listFilters) { - filter.reset(); - } - _endSelection(); - _onFilterChange(); - }, - ), - ...widget.customActions.map((action) => ListFilterAction( - name: action.name, - icon: action.icon, - action: () { - final list = _getActionableItems(); - - action.action(list - .where((item) => widget.listFilters - .every((filter) => filter.filterFunction(item))) - .toList()); - _endSelection(); - }, - )), - ListFilterAction( - name: AppLocalizations.of(context)!.deleteAllFilteredAction, - icon: Icons.delete_rounded, - color: colorScheme.error, - action: () async { - Navigator.pop(context); - final result = await showDeleteAlertDialogue(context); - if (result == null || result == false) return; - - final list = _getActionableItems(); - final toRemove = List.from(list.where((item) => widget - .listFilters - .every((filter) => filter.filterFunction(item)))); - _endSelection(); - await _handleDeleteItemList(toRemove); - - widget.onModifyList?.call(); - }, - ) - ], - activeFilterCount: activeFilterCount + (_isSelecting ? 1 : 0), - ), - ); - } - if (_isSelecting) { - widgets.add( - ListButtonChip( - label: AppLocalizations.of(context)! - .selectionStatus(_selectedIds.length), - icon: Icons.clear_rounded, - onTap: _endSelection, - ), - ); - widgets.add( - ListButtonChip( - label: AppLocalizations.of(context)!.selectAll, - icon: Icons.select_all, - onTap: _handleSelectAll, - ), - ); - } - widgets.addAll(widget.listFilters - .map((filter) => getListFilterChip(filter, _onFilterChange))); - if (widget.sortOptions.isNotEmpty) { - widgets.add( - ListSortChip( - selectedIndex: _selectedSortIndex, - sortOptions: [ - ListSortOption( - (context) => AppLocalizations.of(context)!.defaultLabel, - (a, b) => 0), - ...widget.sortOptions, - ], - onChange: (index) => setState(() { - _selectedSortIndex = index; - widget.onChangeSortIndex?.call(index); - _updateCurrentList(); - _notifyChangeList(); - }), - ), - ); - } - return widgets; - } - - // timeDilation = 1; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: getFilterChips(), - ), - ), - ), - ), + ListFilterBar( + listFilters: widget.listFilters, + customActions: widget.customActions, + sortOptions: widget.sortOptions, + isSelecting: _isSelecting, + handleCustomAction: _handleCustomAction, + handleEndSelection: _endSelection, + handleDeleteAction: _handleDeleteAction, + handleSelectAll: _handleSelectAll, + selectedIds: _selectedIds, + handleFilterChange: _handleFilterChange, + selectedSortIndex: _selectedSortIndex, + handleSortChange: _handleSortChange), if (widget.header != null) widget.header!, Expanded( flex: 1, @@ -550,49 +378,30 @@ class _CustomListViewState child: Center( child: Text( widget.placeholderText, - style: - Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), + style: textTheme.displaySmall?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + ), ), ), ) : Container(), - SlidableAutoCloseBehavior( - child: AutomaticAnimatedListView( - list: currentList, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - comparator: AnimatedListDiffListComparator( - sameItem: (a, b) => a.id == b.id, - sameContent: (a, b) => a.id == b.id, - ), - itemBuilder: _getItemBuilder(), - // animator: DefaultAnimatedListAnimator, - listController: _controller, - scrollController: _scrollController, - addLongPressReorderable: isReorderable, - reorderModel: isReorderable && _selectedSortIndex == 0 - ? AnimatedListReorderModel( - onReorderStart: (index, dx, dy) => true, - onReorderFeedback: (int index, int dropIndex, - double offset, double dx, double dy) => - null, - onReorderMove: (int index, int dropIndex) => true, - onReorderComplete: _handleReorderItems, - ) - : null, - reorderDecorationBuilder: - isReorderable ? reorderableListDecorator : null, - footer: const SizedBox(height: 64 + 80), - // header: widget.header, - - // cacheExtent: double.infinity, - ), - ), + AnimatedReorderableListView( + longPressDraggable: false, + buildDefaultDragHandles: false, + proxyDecorator: (widget, index, animation) => + reorderableListDecorator(context, widget), + items: currentList, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + isSameItem: (a, b) => a.id == b.id, + scrollDirection: Axis.vertical, + itemBuilder: _getItemBuilder(), + enterTransition: [FadeIn()], + exitTransition: [FadeIn()], + controller: _scrollController, + insertDuration: const Duration(milliseconds: 300), + removeDuration: const Duration(milliseconds: 300), + onReorder: _handleReorderItems, + ) ]), ), ], diff --git a/lib/common/widgets/list/list_filter_bar.dart b/lib/common/widgets/list/list_filter_bar.dart new file mode 100644 index 00000000..e63c5d23 --- /dev/null +++ b/lib/common/widgets/list/list_filter_bar.dart @@ -0,0 +1,136 @@ +import 'package:clock_app/common/logic/get_list_filter_chips.dart'; +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ListFilterBar extends StatelessWidget { + const ListFilterBar( + {super.key, + required this.listFilters, + required this.customActions, + required this.sortOptions, + required this.isSelecting, + required this.handleCustomAction, + required this.handleEndSelection, + required this.handleDeleteAction, + required this.handleSelectAll, + required this.selectedIds, + required this.handleFilterChange, + required this.selectedSortIndex, + required this.handleSortChange}); + + final List> listFilters; + final List> customActions; + final List> sortOptions; + final bool isSelecting; + final Function(ListFilterCustomAction) handleCustomAction; + final Function handleEndSelection; + final void Function() handleFilterChange; + final Function handleSelectAll; + final List selectedIds; + final int selectedSortIndex; + final void Function() handleDeleteAction; + final void Function(int) handleSortChange; + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + List getFilterChips() { + List widgets = []; + int activeFilterCount = + listFilters.where((filter) => filter.isActive).length; + if (activeFilterCount > 0 || isSelecting) { + widgets.add( + ListFilterActionChip( + actions: [ + ListFilterAction( + name: AppLocalizations.of(context)!.clearFiltersAction, + icon: Icons.clear_rounded, + action: () { + for (var filter in listFilters) { + filter.reset(); + } + handleEndSelection(); + }, + ), + ...customActions.map( + (action) => ListFilterAction( + name: action.name, + icon: action.icon, + action: () => handleCustomAction(action), + ), + ), + ListFilterAction( + name: AppLocalizations.of(context)!.deleteAllFilteredAction, + icon: Icons.delete_rounded, + color: colorScheme.error, + action: handleDeleteAction, + ) + ], + activeFilterCount: activeFilterCount + (isSelecting ? 1 : 0), + ), + ); + } + + if (isSelecting) { + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)! + .selectionStatus(selectedIds.length), + icon: Icons.clear_rounded, + onTap: () => handleEndSelection(), + ), + ); + widgets.add( + ListButtonChip( + label: AppLocalizations.of(context)!.selectAll, + icon: Icons.select_all_rounded, + onTap: () => handleSelectAll(), + ), + ); + } + widgets.addAll(listFilters + .map((filter) => getListFilterChip(filter, handleFilterChange))); + if (sortOptions.isNotEmpty) { + widgets.add( + ListSortChip( + selectedIndex: selectedSortIndex, + sortOptions: [ + ListSortOption( + (context) => AppLocalizations.of(context)!.defaultLabel, + (a, b) => 0), + ...sortOptions, + ], + onChange: handleSortChange, + ), + ); + } + return widgets; + } + + return Expanded( + flex: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: AnimatedContainer( + duration: 150.ms, + height: getFilterChips().isEmpty ? 0 : 40, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: getFilterChips(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 5a1ba9aa..78ba96e7 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -2,9 +2,11 @@ import 'package:clock_app/common/logic/show_select.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/types/select_choice.dart'; +import 'package:clock_app/common/widgets/animated_show_hide.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/list/action_bottom_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ListFilterChip extends StatelessWidget { @@ -50,11 +52,13 @@ class ListButtonChip extends StatelessWidget { required this.label, this.onTap, required this.icon, + this.isActive = false, }); - final String label; - final IconData icon; + final String? label; + final IconData? icon; final Function()? onTap; + final bool isActive; @override Widget build(BuildContext context) { @@ -62,29 +66,39 @@ class ListButtonChip extends StatelessWidget { ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - return CardContainer( - onTap: onTap, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 10.0, right: 6.0, top: 6.0, bottom: 6.0), - child: Icon( - icon, - color: colorScheme.onSurface, - size: 20, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Text( - label, - style: textTheme.headlineSmall?.copyWith( - color: colorScheme.onSurface, + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + onTap: onTap, + color: isActive ? colorScheme.primary : null, + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only( + left: 10.0, right: 6.0, top: 6.0, bottom: 6.0), + child: Icon( + icon, + color: + isActive ? colorScheme.onPrimary : colorScheme.onSurface, + size: 20, + ), ), - ), - ), - ], + if (label != null) + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + label!, + style: textTheme.headlineSmall?.copyWith( + color: isActive + ? colorScheme.onPrimary + : colorScheme.onSurface, + ), + ), + ), + ], + ), ), ); } @@ -121,34 +135,38 @@ class ListFilterActionChip extends StatelessWidget { ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - return CardContainer( - color: colorScheme.primary, - onTap: () { - _showPopupMenu(context); - // listFilter.isSelected = !listFilter.isSelected; - // onChange(); - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 8.0, right: 6.0, top: 6.0, bottom: 6.0), - child: Icon( - Icons.filter_list_rounded, - color: colorScheme.onPrimary, - size: 20, + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: colorScheme.primary, + onTap: () { + _showPopupMenu(context); + // listFilter.isSelected = !listFilter.isSelected; + // onChange(); + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 8.0, right: 6.0, top: 6.0, bottom: 6.0), + child: Icon( + Icons.filter_list_rounded, + color: colorScheme.onPrimary, + size: 20, + ), ), - ), - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Text( - activeFilterCount.toString(), - style: textTheme.headlineSmall?.copyWith( - color: colorScheme.onPrimary.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + activeFilterCount.toString(), + style: textTheme.headlineSmall?.copyWith( + color: colorScheme.onPrimary.withOpacity(0.6), + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index c1b5231b..9631ed91 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -1,9 +1,12 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/action_pane.dart'; +import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/drag_listener.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; class ListItemCard extends StatefulWidget { @@ -17,16 +20,22 @@ class ListItemCard extends StatefulWidget { this.isDeleteEnabled = true, this.isDuplicateEnabled = true, this.isSelected = false, + this.showReorderHandle = false, + required this.index, + this.onLongPress, }); final VoidCallback? onDelete; final VoidCallback? onDuplicate; final VoidCallback? onTap; + final VoidCallback? onLongPress; final Widget child; final VoidCallback? onInit; final bool isDeleteEnabled; final bool isDuplicateEnabled; final bool isSelected; + final bool showReorderHandle; + final int index; @override State createState() => _ListItemCardState(); @@ -73,6 +82,7 @@ class _ListItemCardState extends State> { ? getDeleteActionPane(widget.onDelete ?? () {}, context) : getDuplicateActionPane(widget.onDuplicate ?? () {}, context); innerWidget = Slidable( + enabled: !widget.showReorderHandle, groupTag: 'list', key: widget.key, startActionPane: startActionPane, @@ -85,8 +95,27 @@ class _ListItemCardState extends State> { width: double.infinity, child: CardContainer( onTap: widget.onTap, + onLongPress: widget.onLongPress, isSelected: widget.isSelected, - child: innerWidget, + child: Row( + children: [ + AnimatedContainer( + duration: 150.ms, + width: widget.showReorderHandle ? 28 : 0, + decoration: const BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: ReorderableGridDragStartListener( + key: widget.key, + index: widget.index, + enabled: true, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Icon(Icons.drag_indicator, color: colorScheme.onSurface.withOpacity(0.6)) + )).animate().scaleX(), + ), + Expanded(child: innerWidget), + ], + ), ), ); } diff --git a/lib/common/widgets/measure_size.dart b/lib/common/widgets/measure_size.dart index 0cc621aa..37d962a8 100644 --- a/lib/common/widgets/measure_size.dart +++ b/lib/common/widgets/measure_size.dart @@ -27,10 +27,10 @@ class MeasureSize extends SingleChildRenderObjectWidget { final OnWidgetSizeChange onChange; const MeasureSize({ - Key? key, + super.key, required this.onChange, - required Widget child, - }) : super(key: key, child: child); + required Widget super.child, + }); @override RenderObject createRenderObject(BuildContext context) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4399a109..a1db94c8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -421,6 +421,8 @@ "@selectionStatus": {}, "selectAll": "Select all", "@selectAll": {}, + "reorder": "Reorder", + "@reorder": {}, "sortGroup": "Sort", "@sortGroup": {}, "defaultLabel": "Default", diff --git a/lib/main.dart b/lib/main.dart index dc5357a4..d906cb99 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,13 +3,11 @@ import 'dart:isolate'; import 'dart:ui'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; -import 'package:awesome_notifications/android_foreground_service.dart'; import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/audio/logic/audio_session.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; -import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/common/utils/debug.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; @@ -40,11 +38,13 @@ void main() async { RingtonePlayer.initialize(), initializeAudioSession(), FlutterShowWhenLocked().hide(), - initializeDatabases(), ]; await Future.wait(initializeData); + + // These rely on initializeAppDataDirectory await initializeStorage(); await initializeSettings(); + await updateAlarms("Update Alarms on Start"); await updateTimers("Update Timers on Start"); AppVisibility.initialize(); diff --git a/lib/settings/data/appearance_settings_schema.dart b/lib/settings/data/appearance_settings_schema.dart index b4a8bd38..d53a9431 100644 --- a/lib/settings/data/appearance_settings_schema.dart +++ b/lib/settings/data/appearance_settings_schema.dart @@ -198,6 +198,24 @@ SettingGroup appearanceSettingsSchema = SettingGroup( ), ], ), + SettingGroup("Animations", + (context) => AppLocalizations.of(context)!.animationSettingGroup, [ + SliderSetting( + "Animation Speed", + (context) => AppLocalizations.of(context)!.animationSpeedSetting, + 0.5, + 2, + 1, + snapLength: 0.1, + ), + SwitchSetting( + "Extra Animations", + (context) => AppLocalizations.of(context)!.extraAnimationSetting, + false, + getDescription: (context) => + AppLocalizations.of(context)!.extraAnimationSettingDescription, + ), + ]) ], icon: Icons.palette_outlined, getDescription: (context) => diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 2fdcf066..72d16aec 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -362,29 +362,7 @@ SettingGroup generalSettingsSchema = SettingGroup( ), ], ), - SettingGroup("Animations", - (context) => AppLocalizations.of(context)!.animationSettingGroup, [ - SliderSetting( - "Animation Speed", - (context) => AppLocalizations.of(context)!.animationSpeedSetting, - 0.5, - 2, - 1, - // unit: 'm', - snapLength: 0.1, - // enableConditions: [ - // ValueCondition( - // ["Show Upcoming Alarm Notifications"], (value) => value), - // ], - ), - SwitchSetting( - "Extra Animations", - (context) => AppLocalizations.of(context)!.extraAnimationSetting, - false, - getDescription: (context) => - AppLocalizations.of(context)!.extraAnimationSettingDescription, - ), - ]) + ], icon: FluxIcons.settings, getDescription: (context) => diff --git a/lib/settings/logic/get_setting_widget.dart b/lib/settings/logic/get_setting_widget.dart index f1a70fd1..78e32542 100644 --- a/lib/settings/logic/get_setting_widget.dart +++ b/lib/settings/logic/get_setting_widget.dart @@ -32,12 +32,12 @@ List getSettingWidgets( bool isAppSettings = true, }) { bool showExtraAnimations = appSettings - .getGroup("General") + .getGroup("Appearance") .getGroup("Animations") .getSetting("Extra Animations") .value; double animationSpeed = appSettings - .getGroup("General") + .getGroup("Appearance") .getGroup("Animations") .getSetting("Animation Speed") .value; diff --git a/lib/settings/logic/initialize_settings.dart b/lib/settings/logic/initialize_settings.dart index 5852ec47..026d88f6 100644 --- a/lib/settings/logic/initialize_settings.dart +++ b/lib/settings/logic/initialize_settings.dart @@ -6,6 +6,7 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/alarm_event.dart'; import 'package:clock_app/audio/logic/system_ringtones.dart'; import 'package:clock_app/clock/data/default_favorite_cities.dart'; +import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/clock/types/city.dart'; import 'package:clock_app/common/data/default_tags.dart'; import 'package:clock_app/common/data/paths.dart'; @@ -26,7 +27,6 @@ import 'package:clock_app/timer/types/timer_preset.dart'; import 'package:flutter/foundation.dart'; import 'package:get_storage/get_storage.dart'; - Future _clearSettings() async { // List timers = await loadList('timers'); // List alarms = await loadList('alarms'); @@ -75,6 +75,8 @@ Future initializeStorage([bool clearSettingsOnDebug = true]) async { await initList("timer_presets", defaultTimerPresets); await initList("ringtones", await getSystemRingtones()); await initTextFile("time_format_string", "h:mm a"); + await initializeDatabases(); + // await initTextFile("", "0"); // await initTextFile("timers-sort-index", "0"); } diff --git a/lib/settings/screens/settings_group_screen.dart b/lib/settings/screens/settings_group_screen.dart index e647bbcf..0ce13677 100644 --- a/lib/settings/screens/settings_group_screen.dart +++ b/lib/settings/screens/settings_group_screen.dart @@ -1,9 +1,11 @@ +import 'package:clock_app/common/data/animations.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/logic/get_setting_widget.dart'; import 'package:clock_app/settings/screens/restore_defaults_screen.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_item.dart'; import 'package:clock_app/settings/types/setting_link.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:clock_app/settings/widgets/search_setting_card.dart'; import 'package:clock_app/settings/widgets/setting_page_link_card.dart'; @@ -81,7 +83,7 @@ class _SettingGroupScreenState extends State { : [ ...getSearchItemWidgets(), const SizedBox(height: 16), - ], + ].animateCardList(), ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 0ea12e63..6bacfd4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -304,6 +304,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + url: "https://pub.dev" + source: hosted + version: "4.5.0" flutter_boot_receiver: dependency: "direct main" description: @@ -406,6 +414,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + url: "https://pub.dev" + source: hosted + version: "0.1.2" flutter_show_when_locked: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ec127ba3..de11ba38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.5.2-beta1+25 environment: - sdk: ">=2.18.6 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -86,6 +86,10 @@ dependencies: url: https://github.com/vicolo-dev/flutter_foreground_task ref: master logger: ^2.3.0 + flutter_animate: ^4.5.0 + # animated_reorderable_list: ^1.1.1 + # animated_reorderable_list: + # path: "../animated_reorderable_list" # dev_dependencies: From 601379a17b1d6eb5a26634fd5203fde390adc034 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 30 Aug 2024 01:16:56 +0500 Subject: [PATCH 089/177] Fix non-deletable items getting delted --- lib/common/data/animations.dart | 2 +- lib/common/widgets/list/custom_list_view.dart | 3 ++- lib/common/widgets/list/list_item_card.dart | 5 +++-- lib/settings/screens/settings_group_screen.dart | 2 +- lib/timer/screens/presets_screen.dart | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/common/data/animations.dart b/lib/common/data/animations.dart index 86f48cf7..43484c46 100644 --- a/lib/common/data/animations.dart +++ b/lib/common/data/animations.dart @@ -12,7 +12,7 @@ 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() + 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/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index a1baecb2..7d64df1a 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -287,6 +287,7 @@ class _CustomListViewState final list = _getActionableItems(); final toRemove = List.from(list.where((item) => + item.isDeletable && widget.listFilters.every((filter) => filter.filterFunction(item)))); _endSelection(); await _handleDeleteItemList(toRemove); @@ -336,7 +337,7 @@ class _CustomListViewState _isSelecting && widget.isReorderable && _selectedSortIndex == 0, index: index, child: widget.itemBuilder(item), - ).animateCard(ValueKey(item)); + ); return itemWidget; }; } diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index 9631ed91..52aaa461 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -102,14 +102,15 @@ class _ListItemCardState extends State> { AnimatedContainer( duration: 150.ms, width: widget.showReorderHandle ? 28 : 0, - decoration: const BoxDecoration(), + color: Colors.transparent, + // decoration: const BoxDecoration(), clipBehavior: Clip.hardEdge, child: ReorderableGridDragStartListener( key: widget.key, index: widget.index, enabled: true, child: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 8.0, top:16.0, bottom:16.0), child: Icon(Icons.drag_indicator, color: colorScheme.onSurface.withOpacity(0.6)) )).animate().scaleX(), ), diff --git a/lib/settings/screens/settings_group_screen.dart b/lib/settings/screens/settings_group_screen.dart index 0ce13677..2a888545 100644 --- a/lib/settings/screens/settings_group_screen.dart +++ b/lib/settings/screens/settings_group_screen.dart @@ -83,7 +83,7 @@ class _SettingGroupScreenState extends State { : [ ...getSearchItemWidgets(), const SizedBox(height: 16), - ].animateCardList(), + ], ), ), ), diff --git a/lib/timer/screens/presets_screen.dart b/lib/timer/screens/presets_screen.dart index 16fcab77..61d045ef 100644 --- a/lib/timer/screens/presets_screen.dart +++ b/lib/timer/screens/presets_screen.dart @@ -57,6 +57,7 @@ class _PresetsScreenState extends State { // onDeleteItem: _handleDeleteTimer, placeholderText: "No presets created", reloadOnPop: true, + isSelectable: true, ), ), ], From d29fc684c0ed9c2c960506aebb18b745ce0f2a51 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 30 Aug 2024 21:15:57 +0500 Subject: [PATCH 090/177] Add animations to all list filter chips --- lib/common/widgets/list/list_filter_chip.dart | 190 ++++++++++-------- lib/common/widgets/list/list_item_card.dart | 9 +- 2 files changed, 108 insertions(+), 91 deletions(-) diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 78ba96e7..4a1350ff 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -25,20 +25,24 @@ class ListFilterChip extends StatelessWidget { ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - return CardContainer( - color: listFilter.isSelected ? colorScheme.primary : null, - onTap: () { - listFilter.isSelected = !listFilter.isSelected; - onChange(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Text( - listFilter.displayName(context), - style: textTheme.headlineSmall?.copyWith( - color: listFilter.isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface, + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: listFilter.isSelected ? colorScheme.primary : null, + onTap: () { + listFilter.isSelected = !listFilter.isSelected; + onChange(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + listFilter.displayName(context), + style: textTheme.headlineSmall?.copyWith( + color: listFilter.isSelected + ? colorScheme.onPrimary + : colorScheme.onSurface, + ), ), ), ), @@ -205,34 +209,38 @@ class ListFilterSelectChip extends StatelessWidget { multiSelect: false); } - return CardContainer( - color: isFirstSelected ? null : colorScheme.primary, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - isFirstSelected - ? listFilter.displayName(context) - : listFilter.selectedFilter.displayName(context), - style: textTheme.headlineSmall?.copyWith( - color: isFirstSelected - ? colorScheme.onSurface - : colorScheme.onPrimary), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: isFirstSelected ? null : colorScheme.primary, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + isFirstSelected + ? listFilter.displayName(context) + : listFilter.selectedFilter.displayName(context), + style: textTheme.headlineSmall?.copyWith( + color: isFirstSelected + ? colorScheme.onSurface + : colorScheme.onPrimary), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon( - Icons.keyboard_arrow_down_rounded, - color: isFirstSelected - ? colorScheme.onSurface.withOpacity(0.6) - : colorScheme.onPrimary.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon( + Icons.keyboard_arrow_down_rounded, + color: isFirstSelected + ? colorScheme.onSurface.withOpacity(0.6) + : colorScheme.onPrimary.withOpacity(0.6), + ), ), - ), - ], + ], + ), ), ); } @@ -272,36 +280,40 @@ class ListFilterMultiSelectChip extends StatelessWidget { multiSelect: true); } - return CardContainer( - color: isSelected ? colorScheme.primary : null, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - !isSelected - ? listFilter.displayName(context) - : listFilter.selectedIndices.length == 1 - ? listFilter.selectedFilters[0].displayName(context) - : "${listFilter.selectedIndices.length} selected", - style: textTheme.headlineSmall?.copyWith( - color: isSelected - ? colorScheme.onPrimary - : colorScheme.onSurface), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + color: isSelected ? colorScheme.primary : null, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + !isSelected + ? listFilter.displayName(context) + : listFilter.selectedIndices.length == 1 + ? listFilter.selectedFilters[0].displayName(context) + : "${listFilter.selectedIndices.length} selected", + style: textTheme.headlineSmall?.copyWith( + color: isSelected + ? colorScheme.onPrimary + : colorScheme.onSurface), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon( - Icons.keyboard_arrow_down_rounded, - color: isSelected - ? colorScheme.onPrimary.withOpacity(0.6) - : colorScheme.onSurface.withOpacity(0.6), + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon( + Icons.keyboard_arrow_down_rounded, + color: isSelected + ? colorScheme.onPrimary.withOpacity(0.6) + : colorScheme.onSurface.withOpacity(0.6), + ), ), - ), - ], + ], + ), ), ); } @@ -340,26 +352,30 @@ class ListSortChip extends StatelessWidget { multiSelect: false); } - return CardContainer( - // color: isFirstSelected ? null : colorScheme.primary, - onTap: showSelect, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), - child: Text( - "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}", - style: textTheme.headlineSmall - ?.copyWith(color: colorScheme.onSurface), + return AnimatedShowHide( + duration: 200.ms, + axis: Axis.horizontal, + child: CardContainer( + // color: isFirstSelected ? null : colorScheme.primary, + onTap: showSelect, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, bottom: 8.0, left: 16.0, right: 2.0), + child: Text( + "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}", + style: textTheme.headlineSmall + ?.copyWith(color: colorScheme.onSurface), + ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 2.0, right: 8.0), - child: Icon(Icons.keyboard_arrow_down_rounded, - color: colorScheme.onSurface.withOpacity(0.6)), - ), - ], + Padding( + padding: const EdgeInsets.only(left: 2.0, right: 8.0), + child: Icon(Icons.keyboard_arrow_down_rounded, + color: colorScheme.onSurface.withOpacity(0.6)), + ), + ], + ), ), ); } diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index 52aaa461..f40904e0 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -100,7 +100,7 @@ class _ListItemCardState extends State> { child: Row( children: [ AnimatedContainer( - duration: 150.ms, + duration: 150.ms, width: widget.showReorderHandle ? 28 : 0, color: Colors.transparent, // decoration: const BoxDecoration(), @@ -110,9 +110,10 @@ class _ListItemCardState extends State> { index: widget.index, enabled: true, child: Padding( - padding: const EdgeInsets.only(left: 8.0, top:16.0, bottom:16.0), - child: Icon(Icons.drag_indicator, color: colorScheme.onSurface.withOpacity(0.6)) - )).animate().scaleX(), + padding: const EdgeInsets.only( + left: 8.0, top: 16.0, bottom: 16.0), + child: Icon(Icons.drag_indicator, + color: colorScheme.onSurface.withOpacity(0.6)))), ), Expanded(child: innerWidget), ], From 500faab7270ab7e0d10f5194617a21e37bd33423 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 31 Aug 2024 22:26:41 +0500 Subject: [PATCH 091/177] Fixed range weekly schedule not working --- lib/alarm/logic/alarm_time.dart | 32 ++++++++++++------- .../types/schedules/range_alarm_schedule.dart | 7 ++-- .../schedules/weekly_alarm_schedule.dart | 3 ++ lib/alarm/utils/next_alarm.dart | 5 +-- .../component/drag_listener.dart | 1 + lib/common/widgets/list/list_item_card.dart | 21 +++++++----- 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart index 568141b4..94061c5b 100644 --- a/lib/alarm/logic/alarm_time.dart +++ b/lib/alarm/logic/alarm_time.dart @@ -4,26 +4,34 @@ import 'package:clock_app/common/utils/date_time.dart'; // Calculates the DateTime when the provided `time` will next occur DateTime getDailyAlarmDate( 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); + if (scheduleStartDate != null && scheduleStartDate.isAfter(DateTime.now())) { + return DateTime(scheduleStartDate.year, scheduleStartDate.month, + scheduleStartDate.day, time.hour, time.minute, time.second); } // If a date has not been provided, assume it to be today - scheduledDate = DateTime.now(); + DateTime scheduleDate = DateTime.now(); DateTime alarmTime; - if (time.toHours() > scheduledDate.toHours()) { + if (time.toHours() > scheduleDate.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); + alarmTime = DateTime(scheduleDate.year, scheduleDate.month, + scheduleDate.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); + // If the time has already passed, set the alarm for next occurence + if (scheduleStartDate != null) { + scheduleDate = scheduleStartDate; + } + + while (scheduleDate.isBefore(DateTime.now())) { + scheduleDate = scheduleDate.add(Duration(days: interval)); + } + + alarmTime = DateTime(scheduleDate.year, scheduleDate.month, + scheduleDate.day, time.hour, time.minute, time.second); } return alarmTime; diff --git a/lib/alarm/types/schedules/range_alarm_schedule.dart b/lib/alarm/types/schedules/range_alarm_schedule.dart index e5b00105..b17dcb62 100644 --- a/lib/alarm/types/schedules/range_alarm_schedule.dart +++ b/lib/alarm/types/schedules/range_alarm_schedule.dart @@ -43,15 +43,14 @@ class RangeAlarmSchedule extends AlarmSchedule { @override Future schedule(Time time, String description) 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); - print('$alarmDate $startDate $endDate'); + DateTime alarmDate = getDailyAlarmDate(time, + scheduleStartDate: startDate, interval: intervalDays); if (alarmDate.isAfter(endDate)) { _isFinished = true; } else { - print("_____________"); await _alarmRunner.schedule(alarmDate, description); _isFinished = false; } diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart index e12c504a..222d11e1 100644 --- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart +++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart @@ -89,6 +89,9 @@ class WeeklyAlarmSchedule extends AlarmSchedule { 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 = _weekdaySchedules.map((schedule) => schedule.weekday).toList(); diff --git a/lib/alarm/utils/next_alarm.dart b/lib/alarm/utils/next_alarm.dart index ebb8eaf3..ee0658c6 100644 --- a/lib/alarm/utils/next_alarm.dart +++ b/lib/alarm/utils/next_alarm.dart @@ -1,10 +1,7 @@ - - import 'package:clock_app/alarm/types/alarm.dart'; -import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/common/utils/list_storage.dart'; -Alarm? getNextAlarm () { +Alarm? getNextAlarm() { List alarms = loadListSync('alarms'); if (alarms.isEmpty) return null; alarms.sort((a, b) { diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart index e791f9ff..8b6c7516 100644 --- a/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart +++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart @@ -32,6 +32,7 @@ class ReorderableGridDragStartListener extends StatelessWidget { @override Widget build(BuildContext context) { return Listener( + behavior: HitTestBehavior.translucent, onPointerDown: enabled ? (PointerDownEvent event) => _startDragging(context, event) : null, diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart index f40904e0..eedab955 100644 --- a/lib/common/widgets/list/list_item_card.dart +++ b/lib/common/widgets/list/list_item_card.dart @@ -106,14 +106,19 @@ class _ListItemCardState extends State> { // decoration: const BoxDecoration(), clipBehavior: Clip.hardEdge, child: ReorderableGridDragStartListener( - key: widget.key, - index: widget.index, - enabled: true, - child: Padding( - padding: const EdgeInsets.only( - left: 8.0, top: 16.0, bottom: 16.0), - child: Icon(Icons.drag_indicator, - color: colorScheme.onSurface.withOpacity(0.6)))), + + key: widget.key, + index: widget.index, + enabled: true, + child: Padding( + padding: + const EdgeInsets.only(left: 8.0, top: 16.0, bottom: 16.0), + child: Icon( + Icons.drag_indicator, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), ), Expanded(child: innerWidget), ], From e29817196beb2bc8e424fafe6da70890ddab1a85 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 31 Aug 2024 23:58:34 +0500 Subject: [PATCH 092/177] Add numpad input --- lib/l10n/app_en.arb | 2 + .../data/general_settings_schema.dart | 8 +- lib/timer/logic/get_duration_picker.dart | 19 +- lib/timer/widgets/duration_picker.dart | 24 ++- lib/timer/widgets/numpad_duration_picker.dart | 163 ++++++++++++++++++ lib/timer/widgets/timer_picker.dart | 18 +- lib/timer/widgets/timer_preset_picker.dart | 10 +- 7 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 lib/timer/widgets/numpad_duration_picker.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a1db94c8..3fa4de46 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -45,6 +45,8 @@ "@pickerInput": {}, "pickerSpinner": "Spinner", "@pickerSpinner": {}, + "pickerNumpad": "Numpad", + "@pickerNumpad": {}, "durationPickerSetting": "Duration Picker", "@durationPickerSetting": {}, "pickerRings": "Rings", diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 72d16aec..7ac16d6b 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -29,7 +29,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; enum TimePickerType { dial, input, spinner } -enum DurationPickerType { rings, spinner } +enum DurationPickerType { rings, spinner, numpad } SelectSettingOption _getDateSettingOption(String format) { return SelectSettingOption((context) { @@ -187,11 +187,17 @@ SettingGroup generalSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.pickerSpinner, DurationPickerType.spinner, ), + SelectSettingOption( + (context) => AppLocalizations.of(context)!.pickerNumpad, + DurationPickerType.numpad, + ), + ], searchTags: [ "duration", "rings", "time", + "numpad" "picker", "dial", "input", diff --git a/lib/timer/logic/get_duration_picker.dart b/lib/timer/logic/get_duration_picker.dart index fd697e3a..672575ae 100644 --- a/lib/timer/logic/get_duration_picker.dart +++ b/lib/timer/logic/get_duration_picker.dart @@ -5,19 +5,13 @@ import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer_preset.dart'; import 'package:clock_app/timer/widgets/dial_duration_picker.dart'; +import 'package:clock_app/timer/widgets/numpad_duration_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -Widget getDurationPicker(BuildContext context, TimeDuration duration, +Widget getDurationPicker(BuildContext context,DurationPickerType type, TimeDuration duration, void Function(TimeDuration) onDurationChange, {TimerPreset? preset}) { - Orientation orientation = MediaQuery.of(context).orientation; - - DurationPickerType type = appSettings - .getGroup("General") - .getGroup("Display") - .getSetting("Duration Picker") - .value; Widget picker; @@ -83,6 +77,15 @@ Widget getDurationPicker(BuildContext context, TimeDuration duration, ), ); + case DurationPickerType.numpad: + picker = NumpadDurationPicker( + duration: duration, + onChange: (TimeDuration newDuration) { + onDurationChange(newDuration); + }, + + ); + break; } diff --git a/lib/timer/widgets/duration_picker.dart b/lib/timer/widgets/duration_picker.dart index ddc71e1c..383851ef 100644 --- a/lib/timer/widgets/duration_picker.dart +++ b/lib/timer/widgets/duration_picker.dart @@ -1,11 +1,12 @@ import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/edit_duration_picker_mode.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - Future showDurationPicker( BuildContext context, { TimeDuration initialTimeDuration = @@ -14,6 +15,7 @@ Future showDurationPicker( }) async { final theme = Theme.of(context); final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; return showDialog( context: context, @@ -30,13 +32,19 @@ Future showDurationPicker( // Get available height and width of the build area of this widget. Make a choice depending on the size. Orientation orientation = MediaQuery.of(context).orientation; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + Widget title() => Row( children: [ // const SizedBox(width: 8), Text( AppLocalizations.of(context)!.durationPickerTitle, style: TimePickerTheme.of(context).helpTextStyle ?? - Theme.of(context).textTheme.labelSmall, + textTheme.labelSmall, ), const Spacer(), TextButton( @@ -44,24 +52,22 @@ Future showDurationPicker( context, () => setState(() {})), child: Text( AppLocalizations.of(context)!.timePickerModeButton, - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), + style: textTheme.titleSmall?.copyWith( + color: colorScheme.primary, + ), ), ) ], ); - Widget label() => Text( + Widget label() => type == DurationPickerType.numpad ? Container() : Text( timeDuration.toString(), style: textTheme.displayMedium, ); Widget durationPicker() => getDurationPicker( context, + type, timeDuration, (TimeDuration newDuration) { setState(() { diff --git a/lib/timer/widgets/numpad_duration_picker.dart b/lib/timer/widgets/numpad_duration_picker.dart new file mode 100644 index 00000000..aa58e71d --- /dev/null +++ b/lib/timer/widgets/numpad_duration_picker.dart @@ -0,0 +1,163 @@ +import 'package:clock_app/theme/text.dart'; +import 'package:clock_app/timer/types/time_duration.dart'; +import 'package:flutter/material.dart'; + +class NumpadDurationPicker extends StatefulWidget { + const NumpadDurationPicker( + {super.key, required this.duration, required this.onChange}); + + final TimeDuration duration; + final void Function(TimeDuration) onChange; + + @override + State createState() => _NumpadDurationPickerState(); +} + +class _NumpadDurationPickerState extends State { + // String hours = "00"; + // String minutes = "00"; + // String seconds = "00"; + // List timeInput = ["0", "0", "0", "0", "0", "0"]; + + @override + void initState() { + super.initState(); + } + + List getTimeInput() { + final hours = widget.duration.hours.toString().padLeft(2, "0"); + final minutes = widget.duration.minutes.toString().padLeft(2, "0"); + final seconds = widget.duration.seconds.toString().padLeft(2, "0"); + return [hours[0], hours[1], minutes[0], minutes[1], seconds[0], seconds[1]]; + } + + void _addDigit(String digit, [int amount = 1]) { + setState(() { + final timeInput = getTimeInput(); + for(int i = 0; i < amount; i++) { + timeInput.removeAt(0); + timeInput.add(digit); + } + _update(timeInput); + }); + } + + void _removeDigit() { + setState(() { + final timeInput = getTimeInput(); + timeInput.removeAt(5); + timeInput.insert(0, "0"); + _update(timeInput); + }); + } + + void _update(List timeInput) { + widget.onChange(TimeDuration( + hours: int.parse("${timeInput[0]}${timeInput[1]}"), + minutes: int.parse("${timeInput[2]}${timeInput[3]}"), + seconds: int.parse("${timeInput[4]}${timeInput[5]}"), + )); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + final labelStyle = textTheme.headlineLarge + ?.copyWith(color: colorScheme.onSurface, height: 1); + final labelUnitStyle = + textTheme.headlineMedium?.copyWith(color: colorScheme.onSurface); + + final hours = widget.duration.hours.toString().padLeft(2, "0"); + final minutes = widget.duration.minutes.toString().padLeft(2, "0"); + final seconds = widget.duration.seconds.toString().padLeft(2, "0"); + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(hours, style: labelStyle), + Text("h", style: labelUnitStyle), + const SizedBox(width: 10), + Text(minutes, style: labelStyle), + Text("m", style: labelUnitStyle), + const SizedBox(width: 10), + Text(seconds, style: labelStyle), + Text("s", style: labelUnitStyle), + ], + ), + SizedBox( + height: 250, + width: 200, + child: GridView.builder( + padding: const EdgeInsets.all(16), + shrinkWrap: true, + itemCount: 12, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemBuilder: (context, index) { + if (index < 9) { + return TimerButton( + label: (index + 1).toString(), + onTap: () => _addDigit((index + 1).toString()), + ); + } else if (index == 9) { + return TimerButton( + label: "00", + onTap: () { + _addDigit("0", 2); + }); + } else if (index == 10) { + return TimerButton( + label: "0", + onTap: () => _addDigit("0"), + ); + } else { + return TimerButton( + label: "⌫", + onTap: _removeDigit, + ); + } + }, + ), + ), + ], + ); + } +} + +class TimerButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + + const TimerButton({super.key, required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(50), + child: Container( + decoration: BoxDecoration( + color: colorScheme.onBackground.withOpacity(0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Center( + child: Text( + label, + style: textTheme.headlineMedium + ?.copyWith(color: colorScheme.onSurface), + ), + ), + ), + ); + } +} diff --git a/lib/timer/widgets/timer_picker.dart b/lib/timer/widgets/timer_picker.dart index a04e61a8..936af222 100644 --- a/lib/timer/widgets/timer_picker.dart +++ b/lib/timer/widgets/timer_picker.dart @@ -2,6 +2,8 @@ import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/edit_duration_picker_mode.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/screens/presets_screen.dart'; @@ -47,6 +49,12 @@ Future?> showTimerPicker( builder: (context) { var width = MediaQuery.of(context).size.width; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + Widget presetChips(double width) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -118,6 +126,7 @@ Future?> showTimerPicker( Widget durationPicker(double width) => getDurationPicker( context, + type, timer.duration, (TimeDuration newDuration) { setState(() { @@ -126,9 +135,12 @@ Future?> showTimerPicker( }, preset: selectedPreset, ); - - Widget label() => Text(timer.duration.toString(), - style: textTheme.displayMedium); + Widget label() => type == DurationPickerType.numpad + ? Container() + : Text( + timer.duration.toString(), + style: textTheme.displayMedium, + ); Widget title() => Row( children: [ diff --git a/lib/timer/widgets/timer_preset_picker.dart b/lib/timer/widgets/timer_preset_picker.dart index 999ae50b..2dfe4dc9 100644 --- a/lib/timer/widgets/timer_preset_picker.dart +++ b/lib/timer/widgets/timer_preset_picker.dart @@ -1,4 +1,6 @@ import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/settings/data/general_settings_schema.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/logic/get_duration_picker.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer_preset.dart'; @@ -29,6 +31,12 @@ Future showTimerPresetPicker(BuildContext context, // Get available height and width of the build area of this widget. Make a choice depending on the size. var width = MediaQuery.of(context).size.width; + DurationPickerType type = appSettings + .getGroup("General") + .getGroup("Display") + .getSetting("Duration Picker") + .value; + return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -53,7 +61,7 @@ Future showTimerPresetPicker(BuildContext context, Text(timerPreset.duration.toString(), style: textTheme.displayMedium), const SizedBox(height: 16), - getDurationPicker(context, timerPreset.duration, + getDurationPicker(context, type, timerPreset.duration, (TimeDuration newDuration) { setState(() { timerPreset.duration = newDuration; From 619acf97a41cf3df84b1fb0ad03fedcdab269f42 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 1 Sep 2024 00:16:56 +0500 Subject: [PATCH 093/177] Improve numpad --- lib/timer/widgets/numpad_duration_picker.dart | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/timer/widgets/numpad_duration_picker.dart b/lib/timer/widgets/numpad_duration_picker.dart index aa58e71d..acebc2f9 100644 --- a/lib/timer/widgets/numpad_duration_picker.dart +++ b/lib/timer/widgets/numpad_duration_picker.dart @@ -1,6 +1,7 @@ import 'package:clock_app/theme/text.dart'; import 'package:clock_app/timer/types/time_duration.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class NumpadDurationPicker extends StatefulWidget { const NumpadDurationPicker( @@ -34,7 +35,7 @@ class _NumpadDurationPickerState extends State { void _addDigit(String digit, [int amount = 1]) { setState(() { final timeInput = getTimeInput(); - for(int i = 0; i < amount; i++) { + for (int i = 0; i < amount; i++) { timeInput.removeAt(0); timeInput.add(digit); } @@ -69,6 +70,8 @@ class _NumpadDurationPickerState extends State { final labelUnitStyle = textTheme.headlineMedium?.copyWith(color: colorScheme.onSurface); + double originalWidth = MediaQuery.of(context).size.width; + final hours = widget.duration.hours.toString().padLeft(2, "0"); final minutes = widget.duration.minutes.toString().padLeft(2, "0"); final seconds = widget.duration.seconds.toString().padLeft(2, "0"); @@ -89,8 +92,8 @@ class _NumpadDurationPickerState extends State { ], ), SizedBox( - height: 250, - width: 200, + width: originalWidth * 0.8, + height: originalWidth * 1, child: GridView.builder( padding: const EdgeInsets.all(16), shrinkWrap: true, @@ -119,7 +122,7 @@ class _NumpadDurationPickerState extends State { ); } else { return TimerButton( - label: "⌫", + icon: Icons.backspace_outlined, onTap: _removeDigit, ); } @@ -132,10 +135,12 @@ class _NumpadDurationPickerState extends State { } class TimerButton extends StatelessWidget { - final String label; + final String? label; + final IconData? icon; final VoidCallback onTap; - const TimerButton({super.key, required this.label, required this.onTap}); + const TimerButton( + {super.key, this.label, required this.onTap, this.icon}); @override Widget build(BuildContext context) { @@ -151,12 +156,15 @@ class TimerButton extends StatelessWidget { borderRadius: BorderRadius.circular(50), ), child: Center( - child: Text( - label, - style: textTheme.headlineMedium - ?.copyWith(color: colorScheme.onSurface), - ), - ), + child: label != null + ? Text( + label!, + style: textTheme.headlineMedium + ?.copyWith(color: colorScheme.onSurface), + ) + : icon != null + ? Icon(icon, color: colorScheme.onSurface) + : Container()), ), ); } From 3da69763c6231521cea1bcd017e26e7cb0d1b52b Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 1 Sep 2024 00:25:51 +0500 Subject: [PATCH 094/177] Improve numpad --- lib/timer/widgets/duration_picker.dart | 12 ++++++----- lib/timer/widgets/numpad_duration_picker.dart | 17 ++++++++-------- lib/timer/widgets/timer_picker.dart | 20 +++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/timer/widgets/duration_picker.dart b/lib/timer/widgets/duration_picker.dart index 383851ef..226fd668 100644 --- a/lib/timer/widgets/duration_picker.dart +++ b/lib/timer/widgets/duration_picker.dart @@ -60,7 +60,7 @@ Future showDurationPicker( ], ); - Widget label() => type == DurationPickerType.numpad ? Container() : Text( + Widget label() => Text( timeDuration.toString(), style: textTheme.displayMedium, ); @@ -80,8 +80,9 @@ Future showDurationPicker( ? Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 8), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), const SizedBox(height: 16), @@ -95,8 +96,9 @@ Future showDurationPicker( // mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 16), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), ], diff --git a/lib/timer/widgets/numpad_duration_picker.dart b/lib/timer/widgets/numpad_duration_picker.dart index acebc2f9..2a617b1f 100644 --- a/lib/timer/widgets/numpad_duration_picker.dart +++ b/lib/timer/widgets/numpad_duration_picker.dart @@ -91,17 +91,18 @@ class _NumpadDurationPickerState extends State { Text("s", style: labelUnitStyle), ], ), + const SizedBox(height: 4), SizedBox( - width: originalWidth * 0.8, - height: originalWidth * 1, + width: originalWidth * 0.76, + height: originalWidth * 1.1, child: GridView.builder( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 12), shrinkWrap: true, itemCount: 12, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 10, - mainAxisSpacing: 10, + crossAxisSpacing: 6, + mainAxisSpacing: 6, ), itemBuilder: (context, index) { if (index < 9) { @@ -149,17 +150,17 @@ class TimerButton extends StatelessWidget { TextTheme textTheme = theme.textTheme; return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(50), + borderRadius: BorderRadius.circular(100), child: Container( decoration: BoxDecoration( color: colorScheme.onBackground.withOpacity(0.1), - borderRadius: BorderRadius.circular(50), + borderRadius: BorderRadius.circular(100), ), child: Center( child: label != null ? Text( label!, - style: textTheme.headlineMedium + style: textTheme.titleMedium ?.copyWith(color: colorScheme.onSurface), ) : icon != null diff --git a/lib/timer/widgets/timer_picker.dart b/lib/timer/widgets/timer_picker.dart index 936af222..81e35163 100644 --- a/lib/timer/widgets/timer_picker.dart +++ b/lib/timer/widgets/timer_picker.dart @@ -135,12 +135,10 @@ Future?> showTimerPicker( }, preset: selectedPreset, ); - Widget label() => type == DurationPickerType.numpad - ? Container() - : Text( - timer.duration.toString(), - style: textTheme.displayMedium, - ); + Widget label() => Text( + timer.duration.toString(), + style: textTheme.displayMedium, + ); Widget title() => Row( children: [ @@ -173,8 +171,9 @@ Future?> showTimerPicker( mainAxisSize: MainAxisSize.min, children: [ title(), - const SizedBox(height: 16), - label(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) label(), const SizedBox(height: 16), durationPicker(width), const SizedBox(height: 16), @@ -188,8 +187,9 @@ Future?> showTimerPicker( // mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 16), - title(), + if (type != DurationPickerType.numpad) + const SizedBox(height: 16), + if (type != DurationPickerType.numpad) title(), const SizedBox(height: 16), label(), const SizedBox(height: 16), From 33c471c2e24e93de572d811c7aae835d3d4dbec0 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 1 Sep 2024 17:13:53 +0500 Subject: [PATCH 095/177] Make tasks reorderable and selectable --- lib/alarm/logic/alarm_isolate.dart | 11 ++++++++++- lib/alarm/screens/alarm_notification_screen.dart | 6 ++++++ lib/notifications/logic/foreground_task.dart | 7 ++++--- .../types/fullscreen_notification_manager.dart | 6 +++--- lib/settings/widgets/list_setting_screen.dart | 2 ++ 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index f49642ca..aa0b2c38 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -5,7 +5,6 @@ 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/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'; @@ -21,6 +20,7 @@ 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 { @@ -118,6 +118,15 @@ void triggerAlarm(int scheduleId, Json params) async { RingtonePlayer.playAlarm(alarm); RingingManager.ringAlarm(scheduleId); + ReceivePort receivePort = ReceivePort(); + IsolateNameServer.removePortNameMapping(setAlarmVolumePortName); + IsolateNameServer.registerPortWithName( + receivePort.sendPort, setAlarmVolumePortName); + receivePort.listen((message) { + print("recieve message: $message"); + setVolume(message[0]); + }); + String timeFormatString = await loadTextFile("time_format_string"); String title = alarm.label.isEmpty ? "Alarm Ringing..." : alarm.label; diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 89ad197b..d4999068 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; + +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/alarm/utils/alarm_id.dart'; import 'package:clock_app/alarm/types/alarm.dart'; @@ -47,6 +50,9 @@ class _AlarmNotificationScreenState extends State { widget.dismissType, ScheduledNotificationType.alarm); } } else { + IsolateNameServer.lookupPortByName(setAlarmVolumePortName)?.send([0.0]); + print("sending volume 0"); + // RingtonePlayer.setVolume(0); _currentWidget = alarm.tasks[_currentIndex].builder(_setNextWidget); } diff --git a/lib/notifications/logic/foreground_task.dart b/lib/notifications/logic/foreground_task.dart index 6855f40f..b1de2b75 100644 --- a/lib/notifications/logic/foreground_task.dart +++ b/lib/notifications/logic/foreground_task.dart @@ -1,4 +1,3 @@ -import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; void initForegroundTask() { @@ -6,7 +5,8 @@ void initForegroundTask() { androidNotificationOptions: AndroidNotificationOptions( channelId: 'foreground_service', channelName: 'Foreground Service Notification', - channelDescription: 'This notification appears when the foreground service is running.', + channelDescription: + 'This notification appears when the foreground service is running.', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, iconData: const NotificationIconData( @@ -30,4 +30,5 @@ void initForegroundTask() { allowWakeLock: true, allowWifiLock: true, ), - );} + ); +} diff --git a/lib/notifications/types/fullscreen_notification_manager.dart b/lib/notifications/types/fullscreen_notification_manager.dart index 2cd40418..4dcb8499 100644 --- a/lib/notifications/types/fullscreen_notification_manager.dart +++ b/lib/notifications/types/fullscreen_notification_manager.dart @@ -12,7 +12,6 @@ import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/alarm/logic/schedule_alarm.dart'; import 'package:clock_app/navigation/types/routes.dart'; import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:move_to_background/move_to_background.dart'; @@ -161,8 +160,9 @@ class AlarmNotificationManager { static Future stopAlarm(int scheduleId, ScheduledNotificationType type, AlarmStopAction action) async { // Send a message to tell the alarm isolate to run the code to stop alarm - SendPort? sendPort = IsolateNameServer.lookupPortByName(stopAlarmPortName); - sendPort?.send([scheduleId, type.name, action.name]); + // See stopScheduledNotification in lib/alarm/logic/alarm_isolate.dart + IsolateNameServer.lookupPortByName(stopAlarmPortName) + ?.send([scheduleId, type.name, action.name]); // await closeNotification(type); } diff --git a/lib/settings/widgets/list_setting_screen.dart b/lib/settings/widgets/list_setting_screen.dart index af301660..159d7e16 100644 --- a/lib/settings/widgets/list_setting_screen.dart +++ b/lib/settings/widgets/list_setting_screen.dart @@ -76,6 +76,8 @@ class _ListSettingScreenState _handleCustomizeItem(task); }, onModifyList: () => widget.onChanged(context), + isReorderable: true, + isSelectable: true, placeholderText: "No ${widget.setting.displayName(context).toLowerCase()} added yet", ), From a7144ad2ca0564e1c9a2e1f8aa67276620f63cf8 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 2 Sep 2024 00:16:39 +0500 Subject: [PATCH 096/177] Fix custom actions not worki;ng --- lib/alarm/screens/alarm_screen.dart | 2 -- lib/common/widgets/list/custom_list_view.dart | 18 ++++++------------ lib/common/widgets/list/list_filter_bar.dart | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 27d321e3..fb344b5a 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -11,13 +11,11 @@ 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'; import 'package:clock_app/common/utils/snackbar.dart'; -import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/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/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; import 'package:great_list_view/great_list_view.dart'; diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 7d64df1a..1f978950 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -1,5 +1,3 @@ -import 'package:clock_app/common/data/animations.dart'; -import 'package:clock_app/common/logic/get_list_filter_chips.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; @@ -8,14 +6,11 @@ import 'package:clock_app/common/widgets/list/animated_reorderable_list/animated import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart'; import 'package:clock_app/common/widgets/list/delete_alert_dialogue.dart'; import 'package:clock_app/common/widgets/list/list_filter_bar.dart'; -import 'package:clock_app/common/widgets/list/list_filter_chip.dart'; import 'package:clock_app/common/widgets/list/list_item_card.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class CustomListView extends StatefulWidget { const CustomListView({ @@ -270,13 +265,12 @@ class _CustomListViewState }); } - void _handleCustomAction(ListFilterCustomAction action) { + void _handleCustomAction(ListFilterCustomAction action) { final list = _getActionableItems(); + List items = list.where((item) => + widget.listFilters.every((filter) => filter.filterFunction(item))).toList(); - action.action(list - .where((item) => - widget.listFilters.every((filter) => filter.filterFunction(item))) - .toList()); + action.action(items); _endSelection(); } @@ -286,11 +280,11 @@ class _CustomListViewState if (result == null || result == false) return; final list = _getActionableItems(); - final toRemove = List.from(list.where((item) => + final itemsToRemove = List.from(list.where((item) => item.isDeletable && widget.listFilters.every((filter) => filter.filterFunction(item)))); _endSelection(); - await _handleDeleteItemList(toRemove); + await _handleDeleteItemList(itemsToRemove); widget.onModifyList?.call(); } diff --git a/lib/common/widgets/list/list_filter_bar.dart b/lib/common/widgets/list/list_filter_bar.dart index e63c5d23..a6d87910 100644 --- a/lib/common/widgets/list/list_filter_bar.dart +++ b/lib/common/widgets/list/list_filter_bar.dart @@ -27,7 +27,7 @@ class ListFilterBar extends StatelessWidget { final List> customActions; final List> sortOptions; final bool isSelecting; - final Function(ListFilterCustomAction) handleCustomAction; + final Function(ListFilterCustomAction) handleCustomAction; final Function handleEndSelection; final void Function() handleFilterChange; final Function handleSelectAll; From aca0dd0df0327124ad75c92190bce7418b6a8dfd Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 6 Sep 2024 19:43:26 +0500 Subject: [PATCH 097/177] Add option to reduce volume while solving tasks --- lib/alarm/data/alarm_settings_schema.dart | 14 ++++++- lib/alarm/logic/alarm_isolate.dart | 2 +- .../screens/alarm_notification_screen.dart | 7 ++-- lib/alarm/types/alarm.dart | 1 + lib/common/widgets/fields/slider_field.dart | 10 ++++- lib/l10n/app_en.arb | 2 + lib/settings/widgets/slider_setting_card.dart | 1 + pubspec.lock | 42 +++++++++---------- pubspec.yaml | 1 + 9 files changed, 52 insertions(+), 28 deletions(-) diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index 46928399..d690d7d2 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -25,6 +25,7 @@ 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:clock_app/timer/types/time_duration.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -209,6 +210,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, @@ -294,7 +304,9 @@ SettingGroup alarmSettingsSchema = SettingGroup( ListSetting( "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( diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index aa0b2c38..5fe9ce70 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -146,7 +146,7 @@ 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 { diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index d4999068..be6a3cca 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -40,6 +40,8 @@ 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) { @@ -50,9 +52,8 @@ class _AlarmNotificationScreenState extends State { widget.dismissType, ScheduledNotificationType.alarm); } } else { - IsolateNameServer.lookupPortByName(setAlarmVolumePortName)?.send([0.0]); - print("sending volume 0"); - + IsolateNameServer.lookupPortByName(setAlarmVolumePortName) + ?.send([alarm.volume * alarm.volumeDuringTasks / 100]); // RingtonePlayer.setVolume(0); _currentWidget = alarm.tasks[_currentIndex].builder(_setNextWidget); } diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 2f7ba5d8..6848cce5 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -81,6 +81,7 @@ class Alarm extends CustomizableListItem { FileItem get ringtone => _settings.getSetting("Melody").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; 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/l10n/app_en.arb b/lib/l10n/app_en.arb index 3fa4de46..a6d0d137 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -301,6 +301,8 @@ "@audioChannelMedia": {}, "volumeSetting": "Volume", "@volumeSetting": {}, + "volumeWhileTasks": "Volume while solving Tasks", + "@volumeWhileTasks": {}, "risingVolumeSetting": "Rising Volume", "@risingVolumeSetting": {}, "timeToFullVolumeSetting": "Time to Full Volume", diff --git a/lib/settings/widgets/slider_setting_card.dart b/lib/settings/widgets/slider_setting_card.dart index e2be84ff..3555e960 100644 --- a/lib/settings/widgets/slider_setting_card.dart +++ b/lib/settings/widgets/slider_setting_card.dart @@ -24,6 +24,7 @@ class _SliderSettingCardState extends State { Widget build(BuildContext context) { SliderField sliderCard = SliderField( title: widget.setting.displayName(context), + description: widget.setting.displayDescription(context), value: widget.setting.value, min: widget.setting.min, max: widget.setting.max, diff --git a/pubspec.lock b/pubspec.lock index 6bacfd4f..efd70e4b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -500,12 +500,12 @@ packages: home_widget: dependency: "direct main" description: - path: "." + path: "packages/home_widget" ref: main - resolved-ref: "5788ac45f62bef72ff44c52cbd77dc1f9258f633" + resolved-ref: "28bf6db2761467209a5244971ccb803a50a8cbb0" url: "https://github.com/AhsanSarwar45/home_widget" source: git - version: "0.5.0" + version: "0.7.0" html: dependency: transitive description: @@ -590,26 +590,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -670,10 +670,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" move_to_background: dependency: "direct main" description: @@ -718,10 +718,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -734,10 +734,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -1043,10 +1043,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timer_builder: dependency: "direct main" description: @@ -1163,10 +1163,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: "direct main" description: @@ -1232,5 +1232,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.1 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.20.0-1.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index de11ba38..fc6fa2c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,7 @@ dependencies: git: url: https://github.com/AhsanSarwar45/home_widget ref: main + path: packages/home_widget/ permission_handler: ^11.3.1 device_info_plus: ^10.1.0 flutter_foreground_task: From cb2077808f08c532c4e6dda0a1275d0a32794458 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 00:02:14 +0500 Subject: [PATCH 098/177] Improve logging system --- lib/alarm/logic/alarm_isolate.dart | 7 ++-- .../screens/alarm_notification_screen.dart | 3 +- lib/clock/logic/timezone_database.dart | 4 +- lib/common/data/paths.dart | 4 ++ lib/common/logic/show_select.dart | 3 +- lib/common/types/list_filter.dart | 4 +- lib/common/utils/debug.dart | 14 ------- lib/common/utils/json_serialize.dart | 3 +- lib/common/utils/list_storage.dart | 5 ++- lib/common/utils/logger.dart | 23 ----------- lib/debug/logic/logger.dart | 21 ++++++++++ lib/debug/types/file_logger_output.dart | 36 ++++++++++++++++++ lib/l10n/app_en.arb | 2 + lib/main.dart | 6 ++- .../data/developer_settings_schema.dart | 38 ++++++++++++++----- lib/settings/screens/backup_screen.dart | 5 ++- lib/settings/types/setting_group.dart | 13 ++++--- lib/stopwatch/types/stopwatch.dart | 2 - .../screens/timer_notification_screen.dart | 3 +- lib/widgets/logic/update_widgets.dart | 3 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 22 files changed, 131 insertions(+), 74 deletions(-) delete mode 100644 lib/common/utils/debug.dart delete mode 100644 lib/common/utils/logger.dart create mode 100644 lib/debug/logic/logger.dart create mode 100644 lib/debug/types/file_logger_output.dart diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 5fe9ce70..58d1396e 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -4,6 +4,7 @@ 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/debug/logic/logger.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:flutter/foundation.dart'; @@ -24,15 +25,15 @@ const String setAlarmVolumePortName = "setAlarmVolumePort"; @pragma('vm:entry-point') void triggerScheduledNotification(int scheduleId, Json params) async { - debugPrint("Alarm triggered: $scheduleId"); + logger.i("Alarm triggered: $scheduleId"); // 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; } diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index be6a3cca..3e3e3030 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -6,6 +6,7 @@ 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/debug/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/navigation/types/alignment.dart'; @@ -88,7 +89,7 @@ class _AlarmNotificationScreenState extends State { snoozeLabel: "Snooze", ); - debugPrint(e.toString()); + logger.e(e.toString()); } _setNextWidget(); diff --git a/lib/clock/logic/timezone_database.dart b/lib/clock/logic/timezone_database.dart index 6c3cbb26..ab490e1a 100644 --- a/lib/clock/logic/timezone_database.dart +++ b/lib/clock/logic/timezone_database.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -19,8 +20,7 @@ Future initializeDatabases() async { List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - debugPrint('Copying timzones.db to $timezonesDatabasePath'); - // debugPrint(json.encode(bytes)); + logger.i('Copying timzones.db to $timezonesDatabasePath'); // Save copied asset to documents await File(timezonesDatabasePath).writeAsBytes(bytes); } diff --git a/lib/common/data/paths.dart b/lib/common/data/paths.dart index b908ad93..d670eb47 100644 --- a/lib/common/data/paths.dart +++ b/lib/common/data/paths.dart @@ -41,3 +41,7 @@ String getRingtonesDirectoryPathSync() { Future getTimezonesDatabasePath() async { return path.join(await getAppDataDirectoryPath(), 'timezones.db'); } + +Future getLogsFilePath() async { + return path.join(await getAppDataDirectoryPath(), "logs.txt"); +} diff --git a/lib/common/logic/show_select.dart b/lib/common/logic/show_select.dart index b060d6c8..c8ddcb5b 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/debug/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/list_filter.dart b/lib/common/types/list_filter.dart index 7c60f7e4..aefdafa4 100644 --- a/lib/common/types/list_filter.dart +++ b/lib/common/types/list_filter.dart @@ -1,6 +1,6 @@ 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/debug/logic/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -199,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/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/json_serialize.dart b/lib/common/utils/json_serialize.dart index c052c95f..562335aa 100644 --- a/lib/common/utils/json_serialize.dart +++ b/lib/common/utils/json_serialize.dart @@ -9,6 +9,7 @@ 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/debug/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'; @@ -51,7 +52,7 @@ List listFromString(String encodedItems) { 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 71383cf5..0743ff40 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -4,6 +4,7 @@ import 'dart:io'; 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:clock_app/debug/logic/logger.dart'; import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; import 'package:path/path.dart' as path; @@ -50,7 +51,7 @@ List loadListSync(String key) { try{ return listFromString(loadTextFileSync(key)); }catch(e){ - debugPrint("Error loading list ($key): $e"); + logger.e("Error loading list ($key): $e"); return []; } } @@ -74,7 +75,7 @@ Future initTextFile(String key, String value) async { if (GetStorage().read('init_$key') == null) { GetStorage().write('init_$key', true); if(!textFileExistsSync(key)){ - debugPrint("Initializing $key"); + logger.i("Initializing $key"); await saveTextFile(key, value); } } diff --git a/lib/common/utils/logger.dart b/lib/common/utils/logger.dart deleted file mode 100644 index 6c35fb95..00000000 --- a/lib/common/utils/logger.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:logger/logger.dart'; - -var logger = Logger(); - -logDebug(String message) { - logger.d(message); -} - -logError(String message, String error) { - logger.e(message, error: error); -} - -logInfo(String message) { - logger.i(message); -} - -logTrace(String message) { - logger.t(message); -} - -logWarning(String message) { - logger.w(message); -} diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart new file mode 100644 index 00000000..108f99db --- /dev/null +++ b/lib/debug/logic/logger.dart @@ -0,0 +1,21 @@ +import 'package:clock_app/debug/types/file_logger_output.dart'; +import 'package:logger/logger.dart'; +import 'dart:isolate'; + +var logger = Logger( + output: FileLoggerOutput(), + printer: PrettyPrinter( + methodCount: 2, // Number of method calls to be displayed + errorMethodCount: 8, // Number of method calls if stacktrace is provided + lineLength: 80, // Width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + // Should each log print contain a timestamp + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), +); + +void printIsolateInfo() { + logger.i( + "Isolate: ${Isolate.current.debugName}, id: ${Isolate.current.hashCode}"); +} diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart new file mode 100644 index 00000000..8fc13d62 --- /dev/null +++ b/lib/debug/types/file_logger_output.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:clock_app/common/data/paths.dart'; +import 'package:logger/logger.dart'; + + + +class FileLoggerOutput extends LogOutput { + FileLoggerOutput(); + + @override + void output(OutputEvent event) { + for (var line in event.lines) { + print(line); + } + + _writeLog(event.origin.message as String); + } + + Future _writeLog(String message) async { + final DateTime currentDate = DateTime.now(); + final String dateString = + "${currentDate.day}-${currentDate.month}-${currentDate.year}"; + + final File file = File(await getLogsFilePath()); + + if (!(await file.exists())) { + await file.create(recursive: true); + } + + file.writeAsStringSync( + "[$dateString | ${currentDate.hour}:${currentDate.minute}:${currentDate.second}] $message\n", + mode: FileMode.append, + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a6d0d137..be49e8bf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -169,6 +169,8 @@ "@maxLogsSetting": {}, "alarmLogSetting": "Alarm Logs", "@alarmLogSetting": {}, + "saveLogs": "Save Logs", + "@saveLogs": {}, "aboutSettingGroup": "About", "@aboutSettingGroup": {}, "restoreSettingGroup": "Restore default values", diff --git a/lib/main.dart b/lib/main.dart index d906cb99..36622c66 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:clock_app/app.dart'; import 'package:clock_app/audio/logic/audio_session.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/data/paths.dart'; -import 'package:clock_app/common/utils/debug.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; import 'package:clock_app/notifications/logic/foreground_task.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; @@ -25,6 +25,10 @@ import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { + FlutterError.onError = (FlutterErrorDetails details) { + logger.e(details.exception.toString()); + }; + WidgetsFlutterBinding.ensureInitialized(); initializeTimeZones(); diff --git a/lib/settings/data/developer_settings_schema.dart b/lib/settings/data/developer_settings_schema.dart index e32d7770..4d9937a1 100644 --- a/lib/settings/data/developer_settings_schema.dart +++ b/lib/settings/data/developer_settings_schema.dart @@ -1,18 +1,22 @@ +import 'dart:io'; + import 'package:clock_app/alarm/screens/alarm_events_screen.dart'; +import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:pick_or_save/pick_or_save.dart'; SettingGroup developerSettingsSchema = SettingGroup( "Developer Options", (context) => AppLocalizations.of(context)!.developerOptionsSettingGroup, [ - SettingGroup("Alarm", - (context) => AppLocalizations.of(context)!.alarmTitle, - [ + SettingGroup( + "Alarm", (context) => AppLocalizations.of(context)!.alarmTitle, [ SwitchSetting( "Show Instant Alarm Button", (context) => AppLocalizations.of(context)!.showIstantAlarmButtonSetting, @@ -21,9 +25,8 @@ SettingGroup developerSettingsSchema = SettingGroup( // "Show a button on the alarm screen that creates an alarm that rings one second in the future", ), ]), - SettingGroup("Logs", - (context) => AppLocalizations.of(context)!.logsSettingGroup, - [ + SettingGroup( + "Logs", (context) => AppLocalizations.of(context)!.logsSettingGroup, [ SliderSetting( "Max logs", (context) => AppLocalizations.of(context)!.maxLogsSetting, @@ -32,9 +35,26 @@ SettingGroup developerSettingsSchema = SettingGroup( 100, snapLength: 1, ), - SettingPageLink("Alarm Logs", - (context) => AppLocalizations.of(context)!.alarmLogSetting, - const AlarmEventsScreen()), + SettingPageLink( + "Alarm Logs", + (context) => AppLocalizations.of(context)!.alarmLogSetting, + const AlarmEventsScreen()), + SettingAction( + "save_logs", (context) => AppLocalizations.of(context)!.saveLogs, + (context) async { + final File file = File(await getLogsFilePath()); + + await PickOrSave().fileSaver( + params: FileSaverParams( + saveFiles: [ + SaveFileInfo( + fileData: await file.readAsBytes(), + fileName: + "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", + ) + ], + )); + }) ]), ], icon: Icons.code_rounded, diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index 672b769d..29a48a44 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/settings/data/backup_options.dart'; import 'package:clock_app/settings/logic/backup.dart'; import 'package:clock_app/settings/types/backup_option.dart'; @@ -76,7 +77,7 @@ class _BackupExportScreenState extends State { showSnackBar(context, "Export successful!"); } } catch (e) { - debugPrint(e.toString()); + logger.e(e.toString()); if (context.mounted) { showSnackBar(context, "Error exporting: ${e.toString()}", error: true); @@ -164,7 +165,7 @@ class _BackupImportScreenState extends State { showSnackBar(context, "Import successful!"); } } catch (e) { - debugPrint(e.toString()); + logger.e(e.toString()); if (context.mounted) { showSnackBar(context, "Error importing: ${e.toString()}", error: true); diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart index 3b5253c4..75b6fee4 100644 --- a/lib/settings/types/setting_group.dart +++ b/lib/settings/types/setting_group.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; @@ -103,7 +104,7 @@ class SettingGroup extends SettingItem { try { return _settingGroups.firstWhere((item) => item.name == name); } catch (e) { - debugPrint("Could not find setting group $name: $e"); + logger.e("Could not find setting group $name: $e"); rethrow; } } @@ -135,7 +136,7 @@ class SettingGroup extends SettingItem { try { return _settingItems.firstWhere((item) => item.name == name); } catch (e) { - debugPrint("Could not find setting item $name: $e"); + logger.e("Could not find setting item $name: $e"); rethrow; } } @@ -144,7 +145,7 @@ class SettingGroup extends SettingItem { try { return _settings.firstWhere((item) => item.name == name); } catch (e) { - debugPrint("Could not find setting $name: $e"); + logger.e("Could not find setting $name: $e"); rethrow; } } @@ -231,7 +232,7 @@ class SettingGroup extends SettingItem { } } } catch (e) { - debugPrint( + logger.e( "Error migrating value in setting group ($name): ${e.toString()}"); } } @@ -239,7 +240,7 @@ class SettingGroup extends SettingItem { if (value != null) setting.loadValueFromJson(value[setting.name]); } } catch (e) { - debugPrint( + logger.e( "Error loading value from json in setting group ($name): ${e.toString()}"); } } @@ -254,7 +255,7 @@ class SettingGroup extends SettingItem { try { value = loadTextFileSync(id); } catch (e) { - debugPrint("Error loading $id: $e"); + logger.e("Error loading $id: $e"); value = GetStorage().read(id); } loadValueFromJson(json.decode(value)); diff --git a/lib/stopwatch/types/stopwatch.dart b/lib/stopwatch/types/stopwatch.dart index 14f4f3c1..d1b34568 100644 --- a/lib/stopwatch/types/stopwatch.dart +++ b/lib/stopwatch/types/stopwatch.dart @@ -3,7 +3,6 @@ import 'package:clock_app/common/types/timer_state.dart'; import 'package:clock_app/common/utils/duration.dart'; import 'package:clock_app/common/utils/id.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; -import 'package:clock_app/common/utils/logger.dart'; import 'package:clock_app/stopwatch/types/lap.dart'; import 'package:clock_app/timer/types/time_duration.dart'; @@ -119,7 +118,6 @@ class ClockStopwatch extends JsonSerializable { } void addLap() { - logInfo("${laps}"); if (_laps.isNotEmpty) { if (currentLapTime.inMilliseconds == 0) return; finishLap(_laps.first); diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 4a291c79..07501141 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -1,6 +1,7 @@ import 'package:clock_app/alarm/logic/schedule_alarm.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/debug/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/widgets/notification_actions/slide_notification_action.dart'; @@ -67,7 +68,7 @@ class _TimerNotificationScreenState extends State { '+${getTimerById(widget.scheduleIds.last)?.addLength.floor()}:00', ); - debugPrint(e.toString()); + logger.e(e.toString()); } super.initState(); diff --git a/lib/widgets/logic/update_widgets.dart b/lib/widgets/logic/update_widgets.dart index a60ee62b..3b38fb69 100644 --- a/lib/widgets/logic/update_widgets.dart +++ b/lib/widgets/logic/update_widgets.dart @@ -1,4 +1,5 @@ import 'package:clock_app/common/utils/time_format.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; @@ -58,7 +59,7 @@ void setDigitalClockWidgetData(BuildContext context) async { updateDigitalClockWidget(); } catch (e) { - debugPrint("Couldn't update Digital Clock Widget: $e"); + logger.e("Couldn't update Digital Clock Widget: $e"); } } diff --git a/pubspec.lock b/pubspec.lock index efd70e4b..31f232bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -638,10 +638,10 @@ packages: dependency: "direct main" description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc6fa2c8..d5a47cf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,7 +86,7 @@ dependencies: git: url: https://github.com/vicolo-dev/flutter_foreground_task ref: master - logger: ^2.3.0 + logger: ^2.4.0 flutter_animate: ^4.5.0 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: From 6d3272f1b0fc3c76fc4919acc49cb93451832840 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 12:22:11 +0500 Subject: [PATCH 099/177] Update workflow flutter version --- .github/workflows/android-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From dda446778dbccb0c490d05bad87327ee3b157f74 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 12:22:36 +0500 Subject: [PATCH 100/177] Update build flutter version --- .github/workflows/android-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From db793afb03825a648ab298dcb7432e0a2878d8e3 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 12:24:01 +0500 Subject: [PATCH 101/177] Update pubspec flutter sdk version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d5a47cf8..578e3d1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.5.2-beta1+25 environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.20.0 <4.0.0" dependencies: flutter: From 286a7445099d9df09a208fd905edb1688b697978 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 12:45:23 +0500 Subject: [PATCH 102/177] Change sdk version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 578e3d1c..cc021efd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.5.2-beta1+25 environment: - sdk: ">=3.20.0 <4.0.0" + sdk: '>=3.4.0 <4.0.0' dependencies: flutter: From e293aa8ff323d4326c7eb4c5d0ac92217f0b0da2 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 16:48:44 +0500 Subject: [PATCH 103/177] Improve loggin --- lib/alarm/screens/alarm_screen.dart | 4 +- lib/common/utils/snackbar.dart | 10 +- lib/debug/logic/logger.dart | 2 + lib/debug/types/file_logger_output.dart | 19 +- lib/debug/types/log_filter.dart | 8 + lib/l10n/app_en.arb | 14 +- lib/main.dart | 4 +- lib/navigation/screens/nav_scaffold.dart | 200 +++++++++--------- .../data/developer_settings_schema.dart | 16 +- lib/system/logic/quick_actions.dart | 25 +++ pubspec.lock | 34 ++- pubspec.yaml | 3 +- 12 files changed, 221 insertions(+), 118 deletions(-) create mode 100644 lib/debug/types/log_filter.dart create mode 100644 lib/system/logic/quick_actions.dart diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index fb344b5a..2b503e71 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -185,7 +185,7 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } - List> getListFilterItems() { + List> _getListFilterItems() { List> listFilterItems = _showFilters.value ? [...alarmListFilters] : []; @@ -257,7 +257,7 @@ class _AlarmScreenState extends State { }, isSelectable: true, // header: getNextAlarmWidget(), - listFilters: getListFilterItems(), + listFilters: _getListFilterItems(), customActions: _showFilters.value ? [ ListFilterCustomAction( diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index 18c3a002..e990f6e6 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -8,10 +8,11 @@ void showSnackBar(BuildContext context, String text, Color? color = error ? colorScheme.error : null; ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context) - .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar, color:color)); + .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar, color: color)); } -SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false, Color? color}) { +SnackBar getSnackbar(String text, + {bool fab = false, bool navBar = false, Color? color}) { double left = 20; double right = 20; double bottom = 12; @@ -46,8 +47,9 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false, Color? content: ConstrainedBox( constraints: const BoxConstraints(minHeight: 28), child: Container( + padding: const EdgeInsets.all(12), alignment: Alignment.centerLeft, - color: color, + color: color, // height: 28, child: Text(text), ), @@ -57,7 +59,7 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false, Color? right: right, bottom: bottom, ), - + padding: EdgeInsets.zero, elevation: 2, dismissDirection: DismissDirection.none, ); diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart index 108f99db..c1c99bdc 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -1,8 +1,10 @@ import 'package:clock_app/debug/types/file_logger_output.dart'; +import 'package:clock_app/debug/types/log_filter.dart'; import 'package:logger/logger.dart'; import 'dart:isolate'; var logger = Logger( + filter: FileLogFilter(), output: FileLoggerOutput(), printer: PrettyPrinter( methodCount: 2, // Number of method calls to be displayed diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart index 8fc13d62..dcb173ed 100644 --- a/lib/debug/types/file_logger_output.dart +++ b/lib/debug/types/file_logger_output.dart @@ -1,10 +1,10 @@ import 'dart:io'; +import 'package:clock_app/app.dart'; import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; import 'package:logger/logger.dart'; - - class FileLoggerOutput extends LogOutput { FileLoggerOutput(); @@ -14,10 +14,19 @@ class FileLoggerOutput extends LogOutput { print(line); } - _writeLog(event.origin.message as String); + _writeLog(event.origin.message as String, event.level); + + Future(() { + if (event.level == Level.error && + App.navigatorKey.currentContext != null) { + showSnackBar( + App.navigatorKey.currentContext!, event.origin.message as String, + error: true, navBar: false, fab: false); + } + }); } - Future _writeLog(String message) async { + Future _writeLog(String message, Level level) async { final DateTime currentDate = DateTime.now(); final String dateString = "${currentDate.day}-${currentDate.month}-${currentDate.year}"; @@ -29,7 +38,7 @@ class FileLoggerOutput extends LogOutput { } file.writeAsStringSync( - "[$dateString | ${currentDate.hour}:${currentDate.minute}:${currentDate.second}] $message\n", + "[$dateString | ${currentDate.hour}:${currentDate.minute}:${currentDate.second}] [${level.name}] $message\n", mode: FileMode.append, ); } diff --git a/lib/debug/types/log_filter.dart b/lib/debug/types/log_filter.dart new file mode 100644 index 00000000..508f8ed4 --- /dev/null +++ b/lib/debug/types/log_filter.dart @@ -0,0 +1,8 @@ +import 'package:logger/logger.dart'; + +class FileLogFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + return true; + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index be49e8bf..be91dd2a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -159,18 +159,22 @@ "@backupSettingGroup": {}, "developerOptionsSettingGroup": "Developer Options", "@developerOptionsSettingGroup": {}, - "showIstantAlarmButtonSetting": "Show Instant Alarm Button", + "showIstantAlarmButtonSetting": "Show instant alarm button", "@showIstantAlarmButtonSetting": {}, - "showIstantTimerButtonSetting": "Show Instant Timer Button", + "showIstantTimerButtonSetting": "Show instant timer button", "@showIstantTimerButtonSetting": {}, "logsSettingGroup": "Logs", "@logsSettingGroup": {}, - "maxLogsSetting": "Max Logs", + "maxLogsSetting": "Max alarm logs", "@maxLogsSetting": {}, - "alarmLogSetting": "Alarm Logs", + "alarmLogSetting": "Alarm logs", "@alarmLogSetting": {}, - "saveLogs": "Save Logs", + "saveLogs": "Save logs", "@saveLogs": {}, + "showErrorSnackbars": "Show error snackbars", + "@showErrorSnackbars": {}, + "clearLogs": "Clear logs", + "@clearLogs": {}, "aboutSettingGroup": "About", "@aboutSettingGroup": {}, "restoreSettingGroup": "Restore default values", diff --git a/lib/main.dart b/lib/main.dart index 36622c66..afa62221 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,15 +18,17 @@ import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/system/data/device_info.dart'; import 'package:clock_app/system/logic/handle_boot.dart'; +import 'package:clock_app/system/logic/quick_actions.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_boot_receiver/flutter_boot_receiver.dart'; import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; +import 'package:quick_actions/quick_actions.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { FlutterError.onError = (FlutterErrorDetails details) { - logger.e(details.exception.toString()); + logger.f(details.exception.toString()); }; WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index fb5d2a3c..402bcb8c 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -13,6 +13,7 @@ import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/screens/settings_group_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/system/logic/handle_intents.dart'; +import 'package:clock_app/system/logic/quick_actions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; @@ -144,6 +145,7 @@ class _NavScaffoldState extends State { @override void initState() { super.initState(); + initializeQuickActions(_onTabSelected); initReceiveIntent(); useMaterialNavBarSetting = appSettings .getGroup("Appearance") @@ -179,109 +181,111 @@ class _NavScaffoldState extends State { Orientation orientation = MediaQuery.of(context).orientation; final tabs = getTabs(context); return WithForegroundTask( - child: Scaffold( - appBar: orientation == Orientation.portrait - ? AppTopBar( - title: Text( - tabs[_selectedTabIndex].title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - ), - ), - actions: [ - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - SettingGroupScreen(settingGroup: appSettings))); - }, - icon: - const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.8), + child: Scaffold( + appBar: orientation == Orientation.portrait + ? AppTopBar( + title: Text( + tabs[_selectedTabIndex].title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.6), + ), ), - ], - ) - : null, - extendBody: false, - body: SafeArea( - child: Row( - children: [ - if (orientation == Orientation.landscape) - NavigationRail( - destinations: [ - for (final tab in tabs) - NavigationRailDestination( - icon: Icon(tab.icon), - label: Text(tab.title), - ) + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingGroupScreen( + settingGroup: appSettings))); + }, + icon: const Icon(FluxIcons.settings, + semanticLabel: "Settings"), + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.8), + ), ], - leading: Text(tabs[_selectedTabIndex].title, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.6), - )), - trailing: IconButton( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - SettingGroupScreen(settingGroup: appSettings))); - }, - icon: - const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.8), - ), - selectedIndex: _selectedTabIndex, - onDestinationSelected: _onTabSelected, - ), - Expanded( - child: PageView( - controller: _controller, - onPageChanged: _handlePageViewChanged, - physics: swipeActionSetting.value == SwipeAction.cardActions - ? const NeverScrollableScrollPhysics() - : null, - children: tabs.map((tab) => tab.widget).toList()), - ), - ], - ), - ), - bottomNavigationBar: orientation == Orientation.portrait - ? useMaterialNavBarSetting.value - ? NavigationBar( - labelBehavior: - NavigationDestinationLabelBehavior.onlyShowSelected, - selectedIndex: _selectedTabIndex, - onDestinationSelected: _onTabSelected, - destinations: [ + ) + : null, + bottomNavigationBar: orientation == Orientation.portrait + ? useMaterialNavBarSetting.value + ? NavigationBar( + labelBehavior: + NavigationDestinationLabelBehavior.onlyShowSelected, + selectedIndex: _selectedTabIndex, + onDestinationSelected: _onTabSelected, + destinations: [ + for (final tab in tabs) + NavigationDestination( + icon: Icon(tab.icon), + label: tab.title, + ) + ], + ) + : AppNavigationBar( + selectedTabIndex: _selectedTabIndex, + onTabSelected: _onTabSelected, + ) + : null, + extendBody: false, + body: SafeArea( + child: Row( + children: [ + if (orientation == Orientation.landscape) + NavigationRail( + destinations: [ for (final tab in tabs) - NavigationDestination( + NavigationRailDestination( icon: Icon(tab.icon), - label: tab.title, + label: Text(tab.title), ) ], - ) - : AppNavigationBar( - selectedTabIndex: _selectedTabIndex, - onTabSelected: _onTabSelected, - ) - : null, - )); + leading: Text(tabs[_selectedTabIndex].title, + style: + Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.6), + )), + trailing: IconButton( + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingGroupScreen( + settingGroup: appSettings))); + }, + icon: const Icon(FluxIcons.settings, + semanticLabel: "Settings"), + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(0.8), + ), + selectedIndex: _selectedTabIndex, + onDestinationSelected: _onTabSelected, + ), + Expanded( + child: PageView( + controller: _controller, + onPageChanged: _handlePageViewChanged, + physics: swipeActionSetting.value == SwipeAction.cardActions + ? const NeverScrollableScrollPhysics() + : null, + children: tabs.map((tab) => tab.widget).toList()), + ), + ], + ), + ), + ), + ); } } diff --git a/lib/settings/data/developer_settings_schema.dart b/lib/settings/data/developer_settings_schema.dart index 4d9937a1..28285fd3 100644 --- a/lib/settings/data/developer_settings_schema.dart +++ b/lib/settings/data/developer_settings_schema.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:clock_app/alarm/screens/alarm_events_screen.dart'; import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; @@ -36,7 +37,7 @@ SettingGroup developerSettingsSchema = SettingGroup( snapLength: 1, ), SettingPageLink( - "Alarm Logs", + "alarm_logs", (context) => AppLocalizations.of(context)!.alarmLogSetting, const AlarmEventsScreen()), SettingAction( @@ -44,6 +45,10 @@ SettingGroup developerSettingsSchema = SettingGroup( (context) async { final File file = File(await getLogsFilePath()); + if(!(await file.exists())) { + await file.create(recursive: true); + } + await PickOrSave().fileSaver( params: FileSaverParams( saveFiles: [ @@ -54,6 +59,15 @@ SettingGroup developerSettingsSchema = SettingGroup( ) ], )); + }), + SettingAction( + "clear_logs", (context) => AppLocalizations.of(context)!.clearLogs, + (context) async { + final File file = File(await getLogsFilePath()); + + await file.writeAsString(""); + + if(context.mounted) showSnackBar(context, "Logs cleared"); }) ]), ], diff --git a/lib/system/logic/quick_actions.dart b/lib/system/logic/quick_actions.dart new file mode 100644 index 00000000..9f96d9b5 --- /dev/null +++ b/lib/system/logic/quick_actions.dart @@ -0,0 +1,25 @@ +import 'package:quick_actions/quick_actions.dart'; + +Future initializeQuickActions ( Function(int) setTab) async { + const QuickActions quickActions = QuickActions(); + await quickActions.initialize((shortcutType) { + if (shortcutType == 'action_add_alarm') { + setTab(0); + } + if (shortcutType == 'action_add_timer') { + setTab(1); + } + // More handling code... + }); + + await quickActions.setShortcutItems([ + const ShortcutItem( + type: 'action_add_alarm', + localizedTitle: 'Add alarm', + icon: 'alarm_icon'), + const ShortcutItem( + type: 'action_add_timer', + localizedTitle: 'Add timer', + icon: 'timer_icon') + ]); +} diff --git a/pubspec.lock b/pubspec.lock index 31f232bf..83d91341 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -882,6 +882,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+2" + quick_actions: + dependency: "direct main" + description: + name: quick_actions + sha256: b17da113df7a7005977f64adfa58ccc49c829d3ccc6e8e770079a8c7fbf2da9e + url: "https://pub.dev" + source: hosted + version: "1.0.7" + quick_actions_android: + dependency: transitive + description: + name: quick_actions_android + sha256: "54a581491b90ff2e1be94af84a40c05e806e232184bb32afa2df57b07c4d6882" + url: "https://pub.dev" + source: hosted + version: "1.0.15" + quick_actions_ios: + dependency: transitive + description: + name: quick_actions_ios + sha256: "402596dea62a1028960b93f7651ec22be0e2a91e4fbf92a1c62d3b95f8ff95a5" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + quick_actions_platform_interface: + dependency: transitive + description: + name: quick_actions_platform_interface + sha256: "81a1e40c519bb3cacfec38b3008b13cef665a75bd270da94f40091b57f0f9236" + url: "https://pub.dev" + source: hosted + version: "1.0.6" receive_intent: dependency: "direct main" description: @@ -1233,4 +1265,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.20.0-1.2.pre" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index cc021efd..61ae09a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: clock_app description: An alarm, clock, timer and stowatch app. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.5.2-beta1+25 +version: 0.6.0-beta1+26 environment: sdk: '>=3.4.0 <4.0.0' @@ -88,6 +88,7 @@ dependencies: ref: master logger: ^2.4.0 flutter_animate: ^4.5.0 + quick_actions: ^1.0.7 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From 46b2d2302414c6b1ee1ebc80f0abbf9f70cdfb26 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 17:01:58 +0500 Subject: [PATCH 104/177] Add additional logs --- lib/alarm/logic/alarm_isolate.dart | 16 +++++++++++----- lib/audio/types/ringtone_player.dart | 8 +++++--- lib/main.dart | 2 -- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 58d1396e..8372fcac 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -25,6 +25,10 @@ const String setAlarmVolumePortName = "setAlarmVolumePort"; @pragma('vm:entry-point') void triggerScheduledNotification(int scheduleId, Json params) async { + FlutterError.onError = (FlutterErrorDetails details) { + logger.f(details.exception.toString()); + }; + logger.i("Alarm triggered: $scheduleId"); // print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}"); if (params == null) { @@ -74,10 +78,9 @@ void stopScheduledNotification(List message) { } 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; } @@ -101,6 +104,7 @@ void triggerAlarm(int scheduleId, Json params) async { now.millisecondsSinceEpoch > alarm.currentScheduleDateTime!.millisecondsSinceEpoch + 1000 * 60 * 60) { + logger.i("Skipping alarm $scheduleId"); return; } @@ -124,7 +128,6 @@ void triggerAlarm(int scheduleId, Json params) async { IsolateNameServer.registerPortWithName( receivePort.sendPort, setAlarmVolumePortName); receivePort.listen((message) { - print("recieve message: $message"); setVolume(message[0]); }); @@ -147,10 +150,11 @@ void triggerAlarm(int scheduleId, Json params) async { } void setVolume(double volume) { - RingtonePlayer.setVolume(volume/100); + RingtonePlayer.setVolume(volume / 100); } void stopAlarm(int scheduleId, AlarmStopAction action) async { + logger.i("Stopping alarm $scheduleId with action: ${action.name}"); if (action == AlarmStopAction.snooze) { await updateAlarmById(scheduleId, (alarm) async => await alarm.snooze()); // await createSnoozeNotification(scheduleId); @@ -168,6 +172,7 @@ void stopAlarm(int scheduleId, AlarmStopAction action) async { } void triggerTimer(int scheduleId, Json params) async { + logger.i("Timer triggered $scheduleId"); ClockTimer? timer = getTimerById(scheduleId); if (timer == null || !timer.isRunning) { @@ -204,6 +209,7 @@ 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) { diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index d5742ad2..091c201e 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -1,6 +1,7 @@ import 'package:audio_session/audio_session.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/audio/types/ringtone_manager.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:just_audio/just_audio.dart'; import 'package:vibration/vibration.dart'; @@ -98,6 +99,7 @@ class RingtonePlayer { } static Future setVolume(double volume) async { + logger.t("Setting volume to $volume"); await activePlayer?.setVolume(volume); } @@ -117,15 +119,15 @@ class RingtonePlayer { await activePlayer?.stop(); await activePlayer?.setLoopMode(loopMode); await activePlayer?.setAudioSource(AudioSource.uri(Uri.parse(ringtoneUri))); - await activePlayer?.setVolume(volume); - // activePlayer.setMode + await setVolume(volume); + // Gradually increase the volume if (secondsToMaxVolume > 0) { for (int i = 0; i <= 10; i++) { Future.delayed( Duration(milliseconds: i * (secondsToMaxVolume * 100)), () { - activePlayer?.setVolume((i / 10) * volume); + setVolume((i / 10) * volume); }, ); } diff --git a/lib/main.dart b/lib/main.dart index afa62221..1d9b101c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,12 +18,10 @@ import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/system/data/device_info.dart'; import 'package:clock_app/system/logic/handle_boot.dart'; -import 'package:clock_app/system/logic/quick_actions.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_boot_receiver/flutter_boot_receiver.dart'; import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; -import 'package:quick_actions/quick_actions.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { From 3d06281155a0c5fe6ceebb116cbd90ab5a4d5542 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 7 Sep 2024 18:51:45 +0500 Subject: [PATCH 105/177] Add more logs --- lib/alarm/logic/alarm_isolate.dart | 2 +- lib/alarm/logic/schedule_alarm.dart | 11 +++++++++-- lib/common/utils/snackbar.dart | 2 +- lib/debug/logic/logger.dart | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 8372fcac..c9e1699a 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -29,7 +29,7 @@ void triggerScheduledNotification(int scheduleId, Json params) async { logger.f(details.exception.toString()); }; - logger.i("Alarm triggered: $scheduleId"); + logger.i("Alarm isolate triggered $scheduleId"); // print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}"); if (params == null) { logger.e("Params was null when triggering alarm"); diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index 43e2916c..c2a41fc7 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/debug/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; Future scheduleAlarm( @@ -68,7 +69,7 @@ Future scheduleAlarm( scheduleIds.add(ScheduleId(id: scheduleId)); await saveList(name, scheduleIds); - // + // // if (type == ScheduledNotificationType.alarm && !snooze) { // } // @@ -88,8 +89,10 @@ Future scheduleAlarm( 'type': type.name, }, ); + + logger.i('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 +116,8 @@ Future cancelAlarm(int scheduleId, ScheduledNotificationType type) async { } AndroidAlarmManager.cancel(scheduleId); + + logger.i('Canceled alarm $scheduleId of type ${type.name}'); } } @@ -128,4 +133,6 @@ Future scheduleSnoozeAlarm(int scheduleId, Duration delay, if (!Platform.environment.containsKey('FLUTTER_TEST')) { await createSnoozeNotification(scheduleId, DateTime.now().add(delay)); } + + logger.i('Scheduled snooze alarm $scheduleId for ${DateTime.now().add(delay)} with type ${type.name}: $description'); } diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index e990f6e6..f99b2a6a 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -47,7 +47,7 @@ SnackBar getSnackbar(String text, content: ConstrainedBox( constraints: const BoxConstraints(minHeight: 28), child: Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), alignment: Alignment.centerLeft, color: color, // height: 28, diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart index c1c99bdc..f0fad6f8 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -7,13 +7,13 @@ var logger = Logger( filter: FileLogFilter(), output: FileLoggerOutput(), printer: PrettyPrinter( - methodCount: 2, // Number of method calls to be displayed + methodCount: 0, // Number of method calls to be displayed errorMethodCount: 8, // Number of method calls if stacktrace is provided lineLength: 80, // Width of the output colors: true, // Colorful log messages printEmojis: true, // Print an emoji for each log message // Should each log print contain a timestamp - dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + dateTimeFormat: DateTimeFormat.none, ), ); From e5c58b6cd9c1d10d3a8e3fbbf95814dad0b0ee01 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 06:37:10 +0500 Subject: [PATCH 106/177] Add quick actions --- lib/alarm/screens/alarm_screen.dart | 68 +++++++++++-------- lib/navigation/data/tabs.dart | 34 +++++++--- lib/navigation/screens/nav_scaffold.dart | 15 +++- .../types/quick_action_controller.dart | 11 +++ lib/navigation/types/tab.dart | 2 + lib/system/logic/quick_actions.dart | 9 ++- lib/timer/screens/timer_screen.dart | 58 +++++++++------- 7 files changed, 129 insertions(+), 68 deletions(-) create mode 100644 lib/navigation/types/quick_action_controller.dart diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 2b503e71..00168fe7 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -15,6 +15,7 @@ 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'; @@ -28,7 +29,9 @@ typedef AlarmCardBuilder = Widget Function( ); class AlarmScreen extends StatefulWidget { - const AlarmScreen({super.key}); + const AlarmScreen({super.key, this.actionController}); + + final QuickActionController? actionController; @override State createState() => _AlarmScreenState(); @@ -70,6 +73,12 @@ class _AlarmScreenState extends State { nextAlarm = getNextAlarm(); + widget.actionController?.setAction((action) { + if (action == "add_alarm") { + _selectTime(); + } + }); + // ListenerManager().addListener(); } @@ -185,6 +194,12 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } + void handleAddAlarmActon(){ + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + _selectTime(); + + } + List> _getListFilterItems() { List> listFilterItems = _showFilters.value ? [...alarmListFilters] : []; @@ -203,31 +218,31 @@ class _AlarmScreenState extends State { return listFilterItems; } - @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); - } + 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); } } + } + @override + Widget build(BuildContext context) { return Stack( children: [ PersistentListView( @@ -293,11 +308,8 @@ class _AlarmScreenState extends State { sortOptions: _showSort.value ? alarmSortOptions : [], ), FAB( - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - selectTime(); - }, - ), + onPressed: handleAddAlarmActon, + ), if (_showInstantAlarmButton.value) FAB( onPressed: () { diff --git a/lib/navigation/data/tabs.dart b/lib/navigation/data/tabs.dart index dd70ab1a..e9327fe6 100644 --- a/lib/navigation/data/tabs.dart +++ b/lib/navigation/data/tabs.dart @@ -1,4 +1,5 @@ import 'package:clock_app/alarm/screens/alarm_screen.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/stopwatch/screens/stopwatch_screen.dart'; import 'package:clock_app/timer/screens/timer_screen.dart'; @@ -8,15 +9,28 @@ import 'package:clock_app/navigation/types/tab.dart'; import 'package:flutter/material.dart' hide Tab; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -List getTabs(BuildContext context) { +List getTabs(BuildContext context, + [QuickActionController? actionController]) { return [ - Tab(title: AppLocalizations.of(context)!.alarmTitle, icon: FluxIcons.alarm, widget: const AlarmScreen()), - Tab(title: AppLocalizations.of(context)!.clockTitle, icon: FluxIcons.clock, widget: const ClockScreen()), - Tab(title: AppLocalizations.of(context)!.timerTitle, icon: FluxIcons.timer, widget: const TimerScreen()), - Tab( - title: AppLocalizations.of(context)!.stopwatchTitle, - icon: FluxIcons.stopwatch, - widget: const StopwatchScreen()), -]; + Tab( + id: "alarm", + title: AppLocalizations.of(context)!.alarmTitle, + icon: FluxIcons.alarm, + widget: AlarmScreen(actionController: actionController)), + Tab( + id: "clock", + title: AppLocalizations.of(context)!.clockTitle, + icon: FluxIcons.clock, + widget: const ClockScreen()), + Tab( + id: "timer", + title: AppLocalizations.of(context)!.timerTitle, + icon: FluxIcons.timer, + widget: TimerScreen(actionController: actionController)), + Tab( + id: "stopwatch", + title: AppLocalizations.of(context)!.stopwatchTitle, + icon: FluxIcons.stopwatch, + widget: const StopwatchScreen()), + ]; } diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 402bcb8c..0a113fb1 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -6,6 +6,7 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/common/utils/snackbar.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/navigation/data/tabs.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/navigation/widgets/app_navigation_bar.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; @@ -15,6 +16,7 @@ import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/system/logic/handle_intents.dart'; import 'package:clock_app/system/logic/quick_actions.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:receive_intent/receive_intent.dart' as intent_handler; @@ -66,14 +68,21 @@ class _NavScaffoldState extends State { late Setting showForegroundSetting; late StreamSubscription _sub; late PageController _controller; + QuickActionController quickActionController = QuickActionController(); - void _onTabSelected(int index) { + void _onTabSelected(int index, [String? tabInitAction]) { ScaffoldMessenger.of(context).removeCurrentSnackBar(); setState(() { _controller.jumpToPage(index); _selectedTabIndex = index; }); + + if (tabInitAction != null) { + SchedulerBinding.instance.addPostFrameCallback((_) { + quickActionController.callAction(tabInitAction); + }); + } } void _handlePageViewChanged(int currentPageIndex) { @@ -145,7 +154,7 @@ class _NavScaffoldState extends State { @override void initState() { super.initState(); - initializeQuickActions(_onTabSelected); + initializeQuickActions(context, _onTabSelected); initReceiveIntent(); useMaterialNavBarSetting = appSettings .getGroup("Appearance") @@ -179,7 +188,7 @@ class _NavScaffoldState extends State { @override Widget build(BuildContext context) { Orientation orientation = MediaQuery.of(context).orientation; - final tabs = getTabs(context); + final tabs = getTabs(context, quickActionController); return WithForegroundTask( child: Scaffold( appBar: orientation == Orientation.portrait diff --git a/lib/navigation/types/quick_action_controller.dart b/lib/navigation/types/quick_action_controller.dart new file mode 100644 index 00000000..778188f1 --- /dev/null +++ b/lib/navigation/types/quick_action_controller.dart @@ -0,0 +1,11 @@ +class QuickActionController { + Function(String name)? _action; + + void setAction(Function(String name)? action) { + _action = action; + } + + void callAction(String name) { + _action?.call(name); + } +} diff --git a/lib/navigation/types/tab.dart b/lib/navigation/types/tab.dart index 9f91654e..7fdfed3c 100644 --- a/lib/navigation/types/tab.dart +++ b/lib/navigation/types/tab.dart @@ -1,11 +1,13 @@ import 'package:flutter/widgets.dart'; class Tab { + final String id; final String title; final IconData icon; final Widget widget; Tab({ + required this.id, required this.title, required this.icon, required this.widget, diff --git a/lib/system/logic/quick_actions.dart b/lib/system/logic/quick_actions.dart index 9f96d9b5..b2e1aeee 100644 --- a/lib/system/logic/quick_actions.dart +++ b/lib/system/logic/quick_actions.dart @@ -1,13 +1,16 @@ +import 'package:clock_app/navigation/data/tabs.dart'; +import 'package:flutter/material.dart'; import 'package:quick_actions/quick_actions.dart'; -Future initializeQuickActions ( Function(int) setTab) async { +Future initializeQuickActions( + BuildContext context, Function(int, [String?]) setTab) async { const QuickActions quickActions = QuickActions(); await quickActions.initialize((shortcutType) { if (shortcutType == 'action_add_alarm') { - setTab(0); + setTab(getTabs(context).indexWhere((tab) => tab.id == "alarm"), "add_alarm"); } if (shortcutType == 'action_add_timer') { - setTab(1); + setTab(getTabs(context).indexWhere((tab) => tab.id == "timer"), "add_timer"); } // More handling code... }); diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 847c448b..2c4292be 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -6,6 +6,8 @@ import 'package:clock_app/common/logic/customize_screen.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; +import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; @@ -112,7 +114,9 @@ typedef TimerCardBuilder = Widget Function( ); class TimerScreen extends StatefulWidget { - const TimerScreen({super.key}); + const TimerScreen({super.key, this.actionController}); + + final QuickActionController? actionController; @override State createState() => _TimerScreenState(); @@ -123,7 +127,6 @@ class _TimerScreenState extends State { late Setting _showFilters; late Setting _showSort; late Setting _showNotification; - ReceivePort? _receivePort; void update(value) { setState(() {}); @@ -186,6 +189,12 @@ class _TimerScreenState extends State { _showSort.addListener(update); _showNotification.addListener(update); ListenerManager.addOnChangeListener("timers", onTimerUpdate); + widget.actionController?.setAction((action) { + logger.i("Received action: $action"); + if (action == "add_timer") { + handleAddTimerAction(); + } + }); // showProgressNotification(); } @@ -298,6 +307,28 @@ class _TimerScreenState extends State { ); } + Future handleAddTimerAction() async { + PickerResult? pickerResult = await showTimerPicker(context); + if (pickerResult != null) { + ClockTimer timer = ClockTimer.from(pickerResult.value); + if (pickerResult.isCustomize) { + await _openCustomizeTimerScreen( + timer, + onSave: (timer) async { + await timer.start(); + _listController.addItem(timer); + }, + isNewTimer: true, + ); + } else { + await timer.start(); + _listController.addItem(timer); + } + _updateTimerNotification(); + // showProgressNotification(); + } + } + Future _handleCustomizeTimer(ClockTimer timer) async { await _openCustomizeTimerScreen(timer, onSave: (newTimer) async { // Timer id gets reset after copyFrom, so we have to cancel the old one @@ -376,28 +407,7 @@ class _TimerScreenState extends State { ], ), FAB( - onPressed: () async { - PickerResult? pickerResult = - await showTimerPicker(context); - if (pickerResult != null) { - ClockTimer timer = ClockTimer.from(pickerResult.value); - if (pickerResult.isCustomize) { - await _openCustomizeTimerScreen( - timer, - onSave: (timer) async { - await timer.start(); - _listController.addItem(timer); - }, - isNewTimer: true, - ); - } else { - await timer.start(); - _listController.addItem(timer); - } - _updateTimerNotification(); - // showProgressNotification(); - } - }, + onPressed: handleAddTimerAction, ) ]); } From cd7a675f0aa3ceb27b9464f47a76790c1a1e61b9 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 29 Jun 2024 20:25:09 +0000 Subject: [PATCH 107/177] Translated using Weblate (Spanish) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/es/ --- lib/l10n/app_es.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5736ae8d..ce27f7d7 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -696,5 +696,13 @@ "lessThanOneMinute": "menos de 1 minuto", "@lessThanOneMinute": {}, "showNextAlarm": "Mostrar la siguiente alarma", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "notificationPermissionDescription": "Permitir que se muestren notificaciones", + "@notificationPermissionDescription": {}, + "showForegroundNotification": "Mostrar notificación en primer plano", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Mostrar una notificación persistente para mantener activa la aplicación", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "Muestra animaciones que no están pulidas y que pueden provocar caídas de cuadros en dispositivos de gama baja", + "@extraAnimationSettingDescription": {} } From 452049e6333f7c127a77098c70d18baa291eb312 Mon Sep 17 00:00:00 2001 From: hopefutuuuuure886 Date: Sat, 29 Jun 2024 16:39:35 +0000 Subject: [PATCH 108/177] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/zh_Hans/ --- lib/l10n/app_zh.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 814a26b8..40207dc3 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -704,5 +704,13 @@ "monthsString": "{count, plural, =0{} =1{1 个月} other{{count} 个月}}", "@monthsString": {}, "combinedTime": "{hours}与{minutes}", - "@combinedTime": {} + "@combinedTime": {}, + "extraAnimationSettingDescription": "显示完整动画(这可能会导致低端设备出现动画卡顿现象)", + "@extraAnimationSettingDescription": {}, + "showForegroundNotification": "允许前台显示通知", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "显示持久通知以保持应用运行", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "允许显示通知", + "@notificationPermissionDescription": {} } From 55b4f71cd069ade1c91f7391340132d4d8337f94 Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Mon, 1 Jul 2024 05:10:06 +0000 Subject: [PATCH 109/177] Translated using Weblate (Russian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8b497d52..8d317be7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -690,5 +690,27 @@ "shortSecondsString": "{seconds}с", "@shortSecondsString": {}, "showNextAlarm": "Показать следующий будильник", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "extraAnimationSettingDescription": "Показывать анимации, которые не оптимизированы и могут вызывать снижение частоты кадров на устройствах с низкой производительностью", + "@extraAnimationSettingDescription": {}, + "showForegroundNotification": "Показать уведомление на переднем плане", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Показывать постоянное уведомление, чтобы приложение оставалось активным", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Разрешить вывод уведомлений", + "@notificationPermissionDescription": {}, + "hoursString": "{count, plural, =0{} =1{1 час} other{{count} часа(ов)}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 минута} other{{count} минут(ы)}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунд(ы)}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 день} other{{count} дня(ей)}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 неделя} other{{count} недель(и)}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 месяц} other{{count} месяца(ев)}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 год} other{{count} года(лет)}}", + "@yearsString": {} } From 28e0fbe0c23a2ebb41fd3709ba98aa7e0570f894 Mon Sep 17 00:00:00 2001 From: Zoda Date: Mon, 1 Jul 2024 14:50:37 +0000 Subject: [PATCH 110/177] Translated using Weblate (Turkish) Currently translated at 96.3% (340 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/tr/ --- lib/l10n/app_tr.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 4f1d269a..a07ff149 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -674,5 +674,17 @@ "noLapsMessage": "Henüz hiç tur yok", "@noLapsMessage": {}, "searchSettingPlaceholder": "Bir ayar ara", - "@searchSettingPlaceholder": {} + "@searchSettingPlaceholder": {}, + "notificationPermissionDescription": "Bildirimlerin görüntülenmesine izin ver", + "@notificationPermissionDescription": {}, + "lessThanOneMinute": "1 dakikadan daha az", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Alarm {duration} içerisinde çalacak", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Sıradaki:{duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} ve {minutes}", + "@combinedTime": {}, + "showNextAlarm": "Sıradaki Alarmı Göster", + "@showNextAlarm": {} } From 014aab896de01af16b22bb5ac2d83d3e31223a0d Mon Sep 17 00:00:00 2001 From: leccro Date: Wed, 3 Jul 2024 12:56:59 +0200 Subject: [PATCH 111/177] Added translation using Weblate (Persian) --- lib/l10n/app_fa.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/app_fa.arb diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lib/l10n/app_fa.arb @@ -0,0 +1 @@ +{} From c5ace0c420b57e9241beafb448ca82d6615134d3 Mon Sep 17 00:00:00 2001 From: leccro Date: Wed, 3 Jul 2024 12:33:51 +0000 Subject: [PATCH 112/177] Translated using Weblate (Persian) Currently translated at 32.8% (116 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 243 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 0967ef42..817386cc 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -1 +1,242 @@ -{} +{ + "system": "سامانه", + "@system": {}, + "generalSettingGroup": "فراگیر", + "@generalSettingGroup": {}, + "generalSettingGroupDescription": "کارگذاردن پیکربندی‌های سراسری مانند تپنگ زمان", + "@generalSettingGroupDescription": {}, + "timerTitle": "زمان‌سنج", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "longDateFormatSetting": "تپنگ تاریخ گسترده", + "@longDateFormatSetting": {}, + "timeFormatSetting": "تپنگ زمان", + "@timeFormatSetting": {}, + "timeFormat12": "۱۲ ساعته", + "@timeFormat12": {}, + "timeFormatDevice": "پیکربندی دستگاه", + "@timeFormatDevice": {}, + "pickerDial": "شماره‌گیر", + "@pickerDial": {}, + "pickerInput": "نهادن", + "@pickerInput": {}, + "pickerSpinner": "گردان", + "@pickerSpinner": {}, + "durationPickerSetting": "گاه گزین", + "@durationPickerSetting": {}, + "pickerRings": "زُرفین", + "@pickerRings": {}, + "swipeActionSetting": "کنش کشیدن", + "@swipeActionSetting": {}, + "swipActionCardAction": "کنش کادر", + "@swipActionCardAction": {}, + "swipActionSwitchTabs": "تغییر زبانه‌ها", + "@swipActionSwitchTabs": {}, + "melodiesSetting": "نوا‌ها", + "@melodiesSetting": {}, + "tagsSetting": "برچسب‌ها", + "@tagsSetting": {}, + "allowNotificationSetting": "اجازه به همه‌ی اعلان‌ها به صورت دستی", + "@allowNotificationSetting": {}, + "autoStartSetting": "روشن‌شدن خودکار", + "@autoStartSetting": {}, + "permissionsSettingGroup": "مجوز‌ها", + "@permissionsSettingGroup": {}, + "notificationPermissionSetting": "مجوز اعلان‌ها", + "@notificationPermissionSetting": {}, + "animationSpeedSetting": "تندی انیمیشن‌ها", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "انیمیشن‌های بیشتر", + "@extraAnimationSetting": {}, + "ignoreBatteryOptimizationSetting": "رد‌کردن بهینه‌سازی باتری", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionAlreadyGranted": "اجازه به اعلان از پیش داده شده است", + "@notificationPermissionAlreadyGranted": {}, + "animationSettingGroup": "انیمیشن‌ها", + "@animationSettingGroup": {}, + "appearanceSettingGroup": "نمایه", + "@appearanceSettingGroup": {}, + "nameField": "نام", + "@nameField": {}, + "colorSchemeNamePlaceholder": "الگو رنگ", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "پس‌زمینه", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeAccentSettingGroup": "رنگ مهند", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "خطا", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "چهارچوب", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "کرانه", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "سایه", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsOutlineSetting": "بکارگیری رنگ مهند برای کرانه", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "بکارگیری رنگ مهند برای سایه", + "@colorSchemeUseAccentAsShadowSetting": {}, + "styleThemeNamePlaceholder": "فتن تم", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "سایه", + "@styleThemeShadowSettingGroup": {}, + "styleThemeElevationSetting": "بلندا", + "@styleThemeElevationSetting": {}, + "styleThemeRadiusSetting": "گردی گوشه", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "شفافیت", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "گسترش", + "@styleThemeSpreadSetting": {}, + "styleThemeOutlineSettingGroup": "کرانه", + "@styleThemeOutlineSettingGroup": {}, + "accessibilitySettingGroup": "دسترسی", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "پشتیبان‌گیری", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "برای برنامه‌نویسان", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "نمایش دکمه‌ی هشدار آنی", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "نمایش دکمه‌ی زمان‌سنج آنی", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "گزارش‌ها", + "@logsSettingGroup": {}, + "restoreSettingGroup": "بازگردانی به پیش فرض", + "@restoreSettingGroup": {}, + "resetButton": "بازنشانی", + "@resetButton": {}, + "previewLabel": "پیش‌پرده", + "@previewLabel": {}, + "accentLabel": "رنگ مهند", + "@accentLabel": {}, + "cardLabel": "کادر", + "@cardLabel": {}, + "errorLabel": "خطا", + "@errorLabel": {}, + "displaySettingGroup": "نمایش", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "قابل اعتماد", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "رنگ‌ها", + "@colorsSettingGroup": {}, + "styleSettingGroup": "فتن", + "@styleSettingGroup": {}, + "useMaterialYouColorSetting": "بکارگیری متریال یو", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "روشنایی", + "@materialBrightnessSetting": {}, + "materialBrightnessSystem": "سامانه", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "روشن", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "تیره", + "@materialBrightnessDark": {}, + "overrideAccentSetting": "برتر کردن رنگ مهند", + "@overrideAccentSetting": {}, + "accentColorSetting": "رنگ مهند", + "@accentColorSetting": {}, + "systemDarkModeSetting": "حالت تیره سامانه", + "@systemDarkModeSetting": {}, + "colorSchemeSetting": "الگو رنگ", + "@colorSchemeSetting": {}, + "darkColorSchemeSetting": "الگو رنگ تیره", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "ساعت", + "@clockSettingGroup": {}, + "timerSettingGroup": "زمان‌سنج", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "گاه‌شمار", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "برون‌برد یا درون‌برد پیکربندی برزنی خود", + "@backupSettingGroupDescription": {}, + "alarmWeekdaysSetting": "روزهای کاری", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "تاریخ‌ها", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "دامنه‌ی تاریخ", + "@alarmRangeSetting": {}, + "alarmIntervalDaily": "روزانه", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "هفته‌ها", + "@alarmIntervalWeekly": {}, + "selectTime": "گزینش زمان", + "@selectTime": {}, + "timePickerModeButton": "سان", + "@timePickerModeButton": {}, + "cancelButton": "وازدن", + "@cancelButton": {}, + "customizeButton": "سفارشی سازی", + "@customizeButton": {}, + "saveButton": "ذخیره", + "@saveButton": {}, + "labelFieldPlaceholder": "افزودن عنوان", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "برنامه", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "نوع", + "@scheduleTypeField": {}, + "scheduleTypeOnce": "یکبار", + "@scheduleTypeOnce": {}, + "clockTitle": "ساعت", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "alarmTitle": "هشدار", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "stopwatchTitle": "گاه‌شمار", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "languageSetting": "زبان", + "@languageSetting": {}, + "dateFormatSetting": "تپنگ تاریخ", + "@dateFormatSetting": {}, + "showSecondsSetting": "نمایش ثانیه‌ها", + "@showSecondsSetting": {}, + "timePickerSetting": "زمان گزین", + "@timePickerSetting": {}, + "timeFormat24": "۲۴ ساعته", + "@timeFormat24": {}, + "swipeActionCardActionDescription": "روی کادر به چپ یا راست بکشید تا کنش انجام گیرد", + "@swipeActionCardActionDescription": {}, + "swipeActionSwitchTabsDescription": "کشیدن میان زبانه‌ها", + "@swipeActionSwitchTabsDescription": {}, + "vendorSetting": "پیکربندی فرآور", + "@vendorSetting": {}, + "vendorSettingDescription": "خاموش کردن دستی بهینه‌سازی‌های ویژه فرآور", + "@vendorSettingDescription": {}, + "labelField": "سرنام", + "@labelField": {}, + "batteryOptimizationSetting": "خاموش کردن بهینه‌سازی باتری به روش دستی", + "@batteryOptimizationSetting": {}, + "appearanceSettingGroupDescription": "نشاندن تم‌ها، رنگ‌ها و دگرش لایه‌ها", + "@appearanceSettingGroupDescription": {}, + "colorSetting": "رنگ", + "@colorSetting": {}, + "textColorSetting": "نویسه", + "@textColorSetting": {}, + "styleThemeShapeSettingGroup": "کرپ(شکل)", + "@styleThemeShapeSettingGroup": {}, + "styleThemeBlurSetting": "تاری", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineWidthSetting": "پهنا", + "@styleThemeOutlineWidthSetting": {}, + "maxLogsSetting": "بیشینه‌ی گزارش‌ها", + "@maxLogsSetting": {}, + "alarmLogSetting": "گزارش‌های هشدار", + "@alarmLogSetting": {}, + "aboutSettingGroup": "درباره برنامه", + "@aboutSettingGroup": {}, + "useMaterialStyleSetting": "بکارگیری فتن متریال یو", + "@useMaterialStyleSetting": {}, + "styleThemeSetting": "فتن تم", + "@styleThemeSetting": {}, + "alarmDeleteAfterFinishingSetting": "پاک‌کردن پس‌از پایان یافتن", + "@alarmDeleteAfterFinishingSetting": {}, + "alarmDeleteAfterRingingSetting": "پاک‌کردن پس‌از ردکردن", + "@alarmDeleteAfterRingingSetting": {} +} From 6534b8860fef574098a0feace85867b4108db673 Mon Sep 17 00:00:00 2001 From: leccro Date: Thu, 4 Jul 2024 11:09:21 +0000 Subject: [PATCH 113/177] Translated using Weblate (Persian) Currently translated at 52.9% (187 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 146 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 817386cc..13cba79f 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -159,7 +159,7 @@ "@alarmRangeSetting": {}, "alarmIntervalDaily": "روزانه", "@alarmIntervalDaily": {}, - "alarmIntervalWeekly": "هفته‌ها", + "alarmIntervalWeekly": "هفتگی", "@alarmIntervalWeekly": {}, "selectTime": "گزینش زمان", "@selectTime": {}, @@ -238,5 +238,147 @@ "alarmDeleteAfterFinishingSetting": "پاک‌کردن پس‌از پایان یافتن", "@alarmDeleteAfterFinishingSetting": {}, "alarmDeleteAfterRingingSetting": "پاک‌کردن پس‌از ردکردن", - "@alarmDeleteAfterRingingSetting": {} + "@alarmDeleteAfterRingingSetting": {}, + "scheduleTypeDaily": "روزانه", + "@scheduleTypeDaily": {}, + "scheduleTypeDailyDescription": "هر روز زنگ بخورد", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "در روز‌های کاری مشخص", + "@scheduleTypeWeek": {}, + "scheduleTypeDate": "در تاریخ‌های مشخص", + "@scheduleTypeDate": {}, + "scheduleTypeRange": "دامنه‌ی تاریخ", + "@scheduleTypeRange": {}, + "soundAndVibrationSettingGroup": "صدا و لرزش", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "صدا", + "@soundSettingGroup": {}, + "settingGroupMore": "بیشتر", + "@settingGroupMore": {}, + "melodySetting": "نوا", + "@melodySetting": {}, + "audioChannelNotification": "اعلان", + "@audioChannelNotification": {}, + "audioChannelRingtone": "صدای‌زنگ", + "@audioChannelRingtone": {}, + "audioChannelMedia": "رسانه", + "@audioChannelMedia": {}, + "volumeSetting": "بلندی صدا", + "@volumeSetting": {}, + "risingVolumeSetting": "صدای فزاینده", + "@risingVolumeSetting": {}, + "snoozeLengthSetting": "مدت", + "@snoozeLengthSetting": {}, + "maxSnoozesSetting": "بیشینه چُرت زدن", + "@maxSnoozesSetting": {}, + "snoozeEnableSetting": "روشن", + "@snoozeEnableSetting": {}, + "tasksSetting": "گماران (کار‌ها)", + "@tasksSetting": {}, + "noItemMessage": "هنوز {items}ی افزوده نشده است", + "@noItemMessage": {}, + "chooseTaskTitle": "گزینش گمار(کار) برای افزودن", + "@chooseTaskTitle": {}, + "mathEasyDifficulty": "آسان (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "میانه (X + Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "سخت (X × Y + Z)", + "@mathHardDifficulty": {}, + "taskTryButton": "آزمودن", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "دشواری", + "@mathTaskDifficultySetting": {}, + "retypeLowercaseSetting": "گنجاندن حروف کوچک", + "@retypeLowercaseSetting": {}, + "numberOfProblemsSetting": "شمار پرسش‌ها", + "@numberOfProblemsSetting": {}, + "yesButton": "بله", + "@yesButton": {}, + "noAlarmMessage": "هشداری ساخته نشده است", + "@noAlarmMessage": {}, + "noTagsMessage": "برچسبی ساخته نشده است", + "@noTagsMessage": {}, + "noPresetsMessage": "پیش‌ساختی ایجاد نشده است", + "@noPresetsMessage": {}, + "duplicateButton": "همانند‌سازی", + "@duplicateButton": {}, + "skipAlarmButton": "رد کردن هشدار پسین", + "@skipAlarmButton": {}, + "dismissAlarmButton": "رها‌کردن", + "@dismissAlarmButton": {}, + "allFilter": "همه", + "@allFilter": {}, + "scheduleDateFilterGroup": "برنامه‌ریزی تاریخ", + "@scheduleDateFilterGroup": {}, + "createdDateFilterGroup": "تاریخ ایجاد شدن", + "@createdDateFilterGroup": {}, + "todayFilter": "امروز", + "@todayFilter": {}, + "tomorrowFilter": "فردا", + "@tomorrowFilter": {}, + "stateFilterGroup": "چگونگی", + "@stateFilterGroup": {}, + "snoozedFilter": "در خواب", + "@snoozedFilter": {}, + "completedFilter": "پایان یافته", + "@completedFilter": {}, + "disabledFilter": "خاموش", + "@disabledFilter": {}, + "pausedTimerFilter": "متوقف", + "@pausedTimerFilter": {}, + "alarmDescriptionWeekly": "هر {days}", + "@alarmDescriptionWeekly": {}, + "vibrationSetting": "لرزش", + "@vibrationSetting": {}, + "audioChannelAlarm": "هشدار", + "@audioChannelAlarm": {}, + "timeToFullVolumeSetting": "زمان تا رسیدن به نهایت حجم صدا", + "@timeToFullVolumeSetting": {}, + "snoozePreventDeletionSetting": "بازداشتن از پاک کردن", + "@snoozePreventDeletionSetting": {}, + "snoozeSettingGroup": "چُرت زدن", + "@snoozeSettingGroup": {}, + "whileSnoozedSettingGroup": "هنگام چُرت زدن", + "@whileSnoozedSettingGroup": {}, + "snoozePreventDisablingSetting": "بازداشتن از غیرفعال کردن", + "@snoozePreventDisablingSetting": {}, + "settings": "پیکربندی", + "@settings": {}, + "mathTask": "پرسش‌های ریاضی", + "@mathTask": {}, + "mathVeryHardDifficulty": "دشوار (X × Y + Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "بازنویسی نوشتار", + "@retypeTask": {}, + "sequenceGridSizeSetting": "اندازه‌ی تکه", + "@sequenceGridSizeSetting": {}, + "retypeIncludeNumSetting": "گنجاندن شماره‌ها", + "@retypeIncludeNumSetting": {}, + "saveReminderAlert": "آیا می خواهید بدون ذخیره بیرون شوید؟", + "@saveReminderAlert": {}, + "noButton": "خیر", + "@noButton": {}, + "noTimerMessage": "زمان‌سنجی ساخته نشده است", + "@noTimerMessage": {}, + "noStopwatchMessage": "گاه‌شماری ساخته نشده است", + "@noStopwatchMessage": {}, + "noTaskMessage": "گماری ایجاد نشده است", + "@noTaskMessage": {}, + "noLogsMessage": "گزارش زنگ هشدار وجود ندارد", + "@noLogsMessage": {}, + "deleteButton": "پاک‌کردن", + "@deleteButton": {}, + "cancelSkipAlarmButton": "وازدن ردشدن", + "@cancelSkipAlarmButton": {}, + "dateFilterGroup": "تاریخ", + "@dateFilterGroup": {}, + "logTypeFilterGroup": "نوع", + "@logTypeFilterGroup": {}, + "activeFilter": "فعال", + "@activeFilter": {}, + "inactiveFilter": "غیرفعال", + "@inactiveFilter": {}, + "runningTimerFilter": "درحال انجام", + "@runningTimerFilter": {} } From db5755366549f6a7c13e34bd96e3dee526566f9b Mon Sep 17 00:00:00 2001 From: Francesco Zuco Date: Fri, 5 Jul 2024 23:15:47 +0000 Subject: [PATCH 114/177] Translated using Weblate (Italian) Currently translated at 97.4% (344 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 00cb99a4..a22eb7d5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -465,17 +465,17 @@ "@defaultPageSetting": {}, "showMeridiemSetting": "Modo AM/PM", "@showMeridiemSetting": {}, - "editPresetsTitle": "Editare impostazioni predeterminate", + "editPresetsTitle": "Modifica i Preset", "@editPresetsTitle": {}, "firstDayOfWeekSetting": "Primo giorno della settimana", "@firstDayOfWeekSetting": {}, "translateLink": "Traduci", "@translateLink": {}, - "translateDescription": "Aiuta a tradurre l'applicazione", + "translateDescription": "Aiuta a tradurre l'app", "@translateDescription": {}, "separatorSetting": "Separatore", "@separatorSetting": {}, - "editTagLabel": "Editare etichetta", + "editTagLabel": "Modifica Etichetta", "@editTagLabel": {}, "tagNamePlaceholder": "Nome dell'etichetta", "@tagNamePlaceholder": {}, @@ -670,5 +670,31 @@ "layoutSettingGroup": "Disposizione", "@layoutSettingGroup": {}, "searchSettingPlaceholder": "Cerca impostazione", - "@searchSettingPlaceholder": {} + "@searchSettingPlaceholder": {}, + "combinedTime": "{hours} e {minutes}", + "@combinedTime": {}, + "alarmRingInMessage": "L'allarme suonerà tra {duration}", + "@alarmRingInMessage": {}, + "showForegroundNotification": "Mostra Notifica in Primo Piano", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Mostra una notifica persistente per mantenere in esecuzione l'app", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Consenti la visualizzazione delle notifiche", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Mostra animazioni che non sono ottimizzate e potrebbero causare perdita di frame nei dispositivi di fascia bassa", + "@extraAnimationSettingDescription": {}, + "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "@daysString": {}, + "noLapsMessage": "Nessun giro", + "@noLapsMessage": {}, + "nextAlarmIn": "Prossimo: {duration}", + "@nextAlarmIn": {}, + "showNextAlarm": "Mostra il Prossimo Allarme", + "@showNextAlarm": {} } From 7332bff081c22c092a434d379786989fd373b540 Mon Sep 17 00:00:00 2001 From: Miki utn Date: Fri, 5 Jul 2024 23:13:33 +0000 Subject: [PATCH 115/177] Translated using Weblate (Italian) Currently translated at 97.4% (344 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a22eb7d5..2077c49d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -696,5 +696,7 @@ "nextAlarmIn": "Prossimo: {duration}", "@nextAlarmIn": {}, "showNextAlarm": "Mostra il Prossimo Allarme", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "comparisonLapBarsSettingGroup": "Barre di confronto dei giri", + "@comparisonLapBarsSettingGroup": {} } From 4b8d3e28771f47a6059ac4e9a293a19282a04c97 Mon Sep 17 00:00:00 2001 From: Diego Beraldin Date: Sun, 7 Jul 2024 09:34:17 +0000 Subject: [PATCH 116/177] Translated using Weblate (Italian) Currently translated at 98.5% (348 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/it/ --- lib/l10n/app_it.arb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2077c49d..38888dec 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -698,5 +698,13 @@ "showNextAlarm": "Mostra il Prossimo Allarme", "@showNextAlarm": {}, "comparisonLapBarsSettingGroup": "Barre di confronto dei giri", - "@comparisonLapBarsSettingGroup": {} + "@comparisonLapBarsSettingGroup": {}, + "lessThanOneMinute": "meno di 1 minuto", + "@lessThanOneMinute": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}min", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {} } From df600d7ac37496865a52766740327570f7ef7f0c Mon Sep 17 00:00:00 2001 From: ulracte Date: Mon, 8 Jul 2024 17:40:16 +0000 Subject: [PATCH 117/177] Translated using Weblate (Persian) Currently translated at 65.7% (232 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 100 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 13cba79f..62460ace 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -3,15 +3,15 @@ "@system": {}, "generalSettingGroup": "فراگیر", "@generalSettingGroup": {}, - "generalSettingGroupDescription": "کارگذاردن پیکربندی‌های سراسری مانند تپنگ زمان", + "generalSettingGroupDescription": "کارگذاردن پیکربندی‌های سراسری مانند کارپ زمان", "@generalSettingGroupDescription": {}, "timerTitle": "زمان‌سنج", "@timerTitle": { "description": "Title of the timer screen" }, - "longDateFormatSetting": "تپنگ تاریخ گسترده", + "longDateFormatSetting": "کارپ تاریخ گسترده", "@longDateFormatSetting": {}, - "timeFormatSetting": "تپنگ زمان", + "timeFormatSetting": "کارپ زمان", "@timeFormatSetting": {}, "timeFormat12": "۱۲ ساعته", "@timeFormat12": {}, @@ -193,7 +193,7 @@ }, "languageSetting": "زبان", "@languageSetting": {}, - "dateFormatSetting": "تپنگ تاریخ", + "dateFormatSetting": "کارپ تاریخ", "@dateFormatSetting": {}, "showSecondsSetting": "نمایش ثانیه‌ها", "@showSecondsSetting": {}, @@ -380,5 +380,95 @@ "inactiveFilter": "غیرفعال", "@inactiveFilter": {}, "runningTimerFilter": "درحال انجام", - "@runningTimerFilter": {} + "@runningTimerFilter": {}, + "allowNotificationSettingDescription": "اجازه به اعلان", + "@allowNotificationSettingDescription": {}, + "alarmIntervalSetting": "درنگ", + "@alarmIntervalSetting": {}, + "scheduleTypeOnceDescription": "دفعه پسین زنگ میزند", + "@scheduleTypeOnceDescription": {}, + "stopwatchTimeFormatSettingGroup": "کارپ زمان", + "@stopwatchTimeFormatSettingGroup": {}, + "editButton": "ویرایش", + "@editButton": {}, + "mondayFull": "دوشنبه", + "@mondayFull": {}, + "wednesdayFull": "چهارشنبه", + "@wednesdayFull": {}, + "thursdayFull": "پنج‌شنبه", + "@thursdayFull": {}, + "saturdayFull": "شنبه", + "@saturdayFull": {}, + "mondayShort": "دش", + "@mondayShort": {}, + "sundayLetter": "ی", + "@sundayLetter": {}, + "textSettingGroup": "نویسه", + "@textSettingGroup": {}, + "lessThanOneMinute": "کمتر از یک دقیقه", + "@lessThanOneMinute": {}, + "showDateSetting": "نمایش تاریخ", + "@showDateSetting": {}, + "alignmentTop": "بالا", + "@alignmentTop": {}, + "alignmentBottom": "پایین", + "@alignmentBottom": {}, + "alignmentLeft": "چپ‌", + "@alignmentLeft": {}, + "alignmentCenter": "میانه", + "@alignmentCenter": {}, + "alignmentRight": "راست", + "@alignmentRight": {}, + "alignmentJustify": "ترازبندی", + "@alignmentJustify": {}, + "fontWeightSetting": "ستبرای هخام", + "@fontWeightSetting": {}, + "dateSettingGroup": "تاریخ", + "@dateSettingGroup": {}, + "timeSettingGroup": "زمان", + "@timeSettingGroup": {}, + "editPresetsTitle": "ویرایش پیش‌گزیده‌ها", + "@editPresetsTitle": {}, + "showMeridiemSetting": "نمایش بامداد/نیمروز", + "@showMeridiemSetting": {}, + "firstDayOfWeekSetting": "روز نخست هفته", + "@firstDayOfWeekSetting": {}, + "translateLink": "ترزبانی", + "@translateLink": {}, + "translateDescription": "کمک‌به ترزبانی برنامه", + "@translateDescription": {}, + "saturdayLetter": "ش", + "@saturdayLetter": {}, + "fridayShort": "آد", + "@fridayShort": {}, + "tuesdayLetter": "س", + "@tuesdayLetter": {}, + "tuesdayFull": "سه‌شنبه", + "@tuesdayFull": {}, + "wednesdayShort": "چ‌ش", + "@wednesdayShort": {}, + "thursdayShort": "پ‌ش", + "@thursdayShort": {}, + "fridayFull": "آدینه", + "@fridayFull": {}, + "sundayFull": "یک‌شنبه", + "@sundayFull": {}, + "tuesdayShort": "س‌ش", + "@tuesdayShort": {}, + "saturdayShort": "ش", + "@saturdayShort": {}, + "sundayShort": "ی‌ش", + "@sundayShort": {}, + "wednesdayLetter": "چ", + "@wednesdayLetter": {}, + "thursdayLetter": "پ", + "@thursdayLetter": {}, + "fridayLetter": "آ", + "@fridayLetter": {}, + "mondayLetter": "د", + "@mondayLetter": {}, + "sizeSetting": "اندازه", + "@sizeSetting": {}, + "defaultPageSetting": "برگه‌ی پیش‌گزیده", + "@defaultPageSetting": {} } From 2bf70e5f1dc7049f4f680dbdc8433a31afb98c96 Mon Sep 17 00:00:00 2001 From: ulracte Date: Tue, 9 Jul 2024 16:25:36 +0000 Subject: [PATCH 118/177] Translated using Weblate (Persian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 270 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 256 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 62460ace..6b3f9bdf 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -37,13 +37,13 @@ "@melodiesSetting": {}, "tagsSetting": "برچسب‌ها", "@tagsSetting": {}, - "allowNotificationSetting": "اجازه به همه‌ی اعلان‌ها به صورت دستی", + "allowNotificationSetting": "اجازه به همه‌ی آگاهنده‌ها به صورت دستی", "@allowNotificationSetting": {}, "autoStartSetting": "روشن‌شدن خودکار", "@autoStartSetting": {}, "permissionsSettingGroup": "مجوز‌ها", "@permissionsSettingGroup": {}, - "notificationPermissionSetting": "مجوز اعلان‌ها", + "notificationPermissionSetting": "مجوز آگاهنده", "@notificationPermissionSetting": {}, "animationSpeedSetting": "تندی انیمیشن‌ها", "@animationSpeedSetting": {}, @@ -51,7 +51,7 @@ "@extraAnimationSetting": {}, "ignoreBatteryOptimizationSetting": "رد‌کردن بهینه‌سازی باتری", "@ignoreBatteryOptimizationSetting": {}, - "notificationPermissionAlreadyGranted": "اجازه به اعلان از پیش داده شده است", + "notificationPermissionAlreadyGranted": "اجازه به آگاهنده‌ها از پیش داده شده است", "@notificationPermissionAlreadyGranted": {}, "animationSettingGroup": "انیمیشن‌ها", "@animationSettingGroup": {}, @@ -103,7 +103,7 @@ "@showIstantTimerButtonSetting": {}, "logsSettingGroup": "گزارش‌ها", "@logsSettingGroup": {}, - "restoreSettingGroup": "بازگردانی به پیش فرض", + "restoreSettingGroup": "بازگردانی به پیش‌گزیده", "@restoreSettingGroup": {}, "resetButton": "بازنشانی", "@resetButton": {}, @@ -213,7 +213,7 @@ "@labelField": {}, "batteryOptimizationSetting": "خاموش کردن بهینه‌سازی باتری به روش دستی", "@batteryOptimizationSetting": {}, - "appearanceSettingGroupDescription": "نشاندن تم‌ها، رنگ‌ها و دگرش لایه‌ها", + "appearanceSettingGroupDescription": "نشاندن تم‌ها، رنگ‌ها و دگرش رونگارها", "@appearanceSettingGroupDescription": {}, "colorSetting": "رنگ", "@colorSetting": {}, @@ -257,7 +257,7 @@ "@settingGroupMore": {}, "melodySetting": "نوا", "@melodySetting": {}, - "audioChannelNotification": "اعلان", + "audioChannelNotification": "آگاهنده", "@audioChannelNotification": {}, "audioChannelRingtone": "صدای‌زنگ", "@audioChannelRingtone": {}, @@ -269,7 +269,7 @@ "@risingVolumeSetting": {}, "snoozeLengthSetting": "مدت", "@snoozeLengthSetting": {}, - "maxSnoozesSetting": "بیشینه چُرت زدن", + "maxSnoozesSetting": "بیشینه واپَس انداختن‌ها", "@maxSnoozesSetting": {}, "snoozeEnableSetting": "روشن", "@snoozeEnableSetting": {}, @@ -319,7 +319,7 @@ "@tomorrowFilter": {}, "stateFilterGroup": "چگونگی", "@stateFilterGroup": {}, - "snoozedFilter": "در خواب", + "snoozedFilter": "واپَس افتاده", "@snoozedFilter": {}, "completedFilter": "پایان یافته", "@completedFilter": {}, @@ -335,13 +335,13 @@ "@audioChannelAlarm": {}, "timeToFullVolumeSetting": "زمان تا رسیدن به نهایت حجم صدا", "@timeToFullVolumeSetting": {}, - "snoozePreventDeletionSetting": "بازداشتن از پاک کردن", + "snoozePreventDeletionSetting": "بازداشتن از پاک‌کردن", "@snoozePreventDeletionSetting": {}, - "snoozeSettingGroup": "چُرت زدن", + "snoozeSettingGroup": "واپَساندن", "@snoozeSettingGroup": {}, - "whileSnoozedSettingGroup": "هنگام چُرت زدن", + "whileSnoozedSettingGroup": "هنگام واپَسانِش", "@whileSnoozedSettingGroup": {}, - "snoozePreventDisablingSetting": "بازداشتن از غیرفعال کردن", + "snoozePreventDisablingSetting": "بازداشتن از خاموش کردن", "@snoozePreventDisablingSetting": {}, "settings": "پیکربندی", "@settings": {}, @@ -381,7 +381,7 @@ "@inactiveFilter": {}, "runningTimerFilter": "درحال انجام", "@runningTimerFilter": {}, - "allowNotificationSettingDescription": "اجازه به اعلان", + "allowNotificationSettingDescription": "نا", "@allowNotificationSettingDescription": {}, "alarmIntervalSetting": "درنگ", "@alarmIntervalSetting": {}, @@ -470,5 +470,247 @@ "sizeSetting": "اندازه", "@sizeSetting": {}, "defaultPageSetting": "برگه‌ی پیش‌گزیده", - "@defaultPageSetting": {} + "@defaultPageSetting": {}, + "batteryOptimizationSettingDescription": "خاموش کردن بهینه‌سازی باتری برای این برنامه تا از دیرکرد هشدار‌ها جلوگیری شود", + "@batteryOptimizationSettingDescription": {}, + "autoStartSettingDescription": "برخی دستگاه ها نیاز به باز شدن خودکار برنامه دارند تا هشدار‌ها هنگامی که برنامه بسته شده است زنگ بخورند", + "@autoStartSettingDescription": {}, + "ignoreBatteryOptimizationAlreadyGranted": "اجازه‌ی نادیده گرفتن بهینه سازی باتری از پیش داده شده است.", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "scheduleTypeWeekDescription": "در روزهای مشخصی از هفته واکرد خواهد شد", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeRangeDescription": "در بازه‌ی تاریخ مشخص شده واکرد می شود", + "@scheduleTypeRangeDescription": {}, + "scheduleTypeDateDescription": "در تاریخ‌های مشخص واکرد خواهد شد", + "@scheduleTypeDateDescription": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "نمی‌توان هشدار را هنگامی واپس‌افتاده است، خاموش کرد", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "audioChannelSetting": "کانال صدا", + "@audioChannelSetting": {}, + "sequenceTask": "دنباله", + "@sequenceTask": {}, + "retypeNumberChars": "شمار واچ‌ها", + "@retypeNumberChars": {}, + "sequenceLengthSetting": "درازنای دنباله", + "@sequenceLengthSetting": {}, + "defaultLabel": "پیش‌گزیده", + "@defaultLabel": {}, + "stoppedTimerFilter": "ایستاده", + "@stoppedTimerFilter": {}, + "remainingTimeDesc": "کمترین زمان مانده", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "بیشترین زمان مانده", + "@remainingTimeAsc": {}, + "durationAsc": "کوتاه‌ترین", + "@durationAsc": {}, + "durationDesc": "بلند‌ترین", + "@durationDesc": {}, + "nameAsc": "سرنام الف-ی", + "@nameAsc": {}, + "nameDesc": "سرنام ی- الف", + "@nameDesc": {}, + "timeOfDayAsc": "نخست، ساعات آغازین", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "نخست، ساعات واپسین", + "@timeOfDayDesc": {}, + "filterActions": "پالایه‌ی کنش‌ها", + "@filterActions": {}, + "clearFiltersAction": "پاک‌کردن همه‌ی پالایه‌ها", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "روشن کردن همه‌ی هشدار‌های پالایش شده", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "خاموش کردن همه‌ی هشدار‌های پالایش شده", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "رد کردن همه‌ی هشدار‌های پالایش شده", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "وازدن همه‌ی هشدار‌های پالایش شده", + "@cancelSkipAllFilteredAlarmsAction": {}, + "alarmDescriptionSnooze": "واپَس افتاده تا {date}", + "@alarmDescriptionSnooze": {}, + "deleteAllFilteredAction": "پاک‌کردن همه‌ی مورد‌های پالایش شده", + "@deleteAllFilteredAction": {}, + "skippingDescriptionSuffix": "(پرش به رویداد پسین)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionNotScheduled": "برنامه‌ریزی نشده", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionToday": "تنها امروز", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "تنها فردا", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "هر روزه", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "هر پایان هفته", + "@alarmDescriptionWeekend": {}, + "stopwatchPrevious": "پیشین", + "@stopwatchPrevious": {}, + "stopwatchFastest": "تندترین", + "@stopwatchFastest": {}, + "defaultSettingGroup": "پیکربندی پیش‌گزیده", + "@defaultSettingGroup": {}, + "alarmDescriptionDays": "در {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{روزانه} weekly{هفتگی} other{دیگر}} from {startDate} to {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "کندترین", + "@stopwatchSlowest": {}, + "stopwatchAverage": "میانگین", + "@stopwatchAverage": {}, + "alarmsDefaultSettingGroupDescription": "پیش‌گزیده‌ها را برای هشدارهای نو بنشانید", + "@alarmsDefaultSettingGroupDescription": {}, + "notificationsSettingGroup": "آگاهنده‌ها", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "نمایش آگاهنده‌های هشدار پیش‌رو", + "@showUpcomingAlarmNotificationSetting": {}, + "timerDefaultSettingGroupDescription": "پیش‌گزیده‌ها را برای زمان‌سنج‌های نو بنشانید", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "پالایه‌ها", + "@filtersSettingGroup": {}, + "showNotificationSetting": "نمایش آگاهنده", + "@showNotificationSetting": {}, + "showSnoozeNotificationSetting": "نمایش آگاهنده‌ی واپَساندن", + "@showSnoozeNotificationSetting": {}, + "presetsSetting": "پیش‌ساخته‌ها", + "@presetsSetting": {}, + "newPresetPlaceholder": "پیش‌ساخت نو", + "@newPresetPlaceholder": {}, + "upcomingLeadTimeSetting": "زمان یادآوری پیش از هشدار", + "@upcomingLeadTimeSetting": {}, + "dismissActionSetting": "سردگ کنش ردکردن", + "@dismissActionSetting": {}, + "dismissActionSlide": "لغزنده", + "@dismissActionSlide": {}, + "dismissActionButtons": "دکمه‌ای", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "دکمه‌های کرانه‌ای", + "@dismissActionAreaButtons": {}, + "stopwatchShowMillisecondsSetting": "نمایش هزارم ثانیه", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "همسنگی نوارهای چرخه", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "نمایش چرخه پیشین", + "@showPreviousLapSetting": {}, + "showSlowestLapSetting": "نمایش کندترین چرخه", + "@showSlowestLapSetting": {}, + "exportSettingsSettingDescription": "برون‌برد پیکربندی‌ها به پرونده‌ای برزنی", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "درون‌برد", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "درون‌برد پیکربندی‌ها از پرونده‌ای برزنی", + "@importSettingsSettingDescription": {}, + "versionLabel": "نسخه", + "@versionLabel": {}, + "packageNameLabel": "نام بسته", + "@packageNameLabel": {}, + "licenseLabel": "پروانه‌", + "@licenseLabel": {}, + "emailLabel": "ایمیل", + "@emailLabel": {}, + "viewOnGithubLabel": "دیدن در گیت‌هاب", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "پروانه‌های متن‌باز", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "هم‌دستان", + "@contributorsSetting": {}, + "donorsSetting": "پشتیبانان مالی", + "@donorsSetting": {}, + "donateButton": "پشتیبانی مالی کنید", + "@donateButton": {}, + "addLengthSetting": "افزودن درازنا", + "@addLengthSetting": {}, + "sameTime": "هم‌زمان", + "@sameTime": {}, + "searchSettingPlaceholder": "جستجو در پیکربندی‌ها", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "جستجو برای شهر", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "این شهر از پیش در برگزیدگان است", + "@cityAlreadyInFavorites": {}, + "elapsedTime": "زمان گذشته شده", + "@elapsedTime": {}, + "durationPickerTitle": "گزیدن مدت", + "@durationPickerTitle": {}, + "noLapsMessage": "هنوز چرخه‌ای نیست", + "@noLapsMessage": {}, + "donateDescription": "پشتیبانی مالی کنید تا برنامه پیشرفت کند", + "@donateDescription": {}, + "donorsDescription": "پشتیبانان بخشنده‌ی ما", + "@donorsDescription": {}, + "contributorsDescription": "مردمانی که این برنامه را شدنی کردند", + "@contributorsDescription": {}, + "widgetsSettingGroup": "ابزارک‌ها", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "ساعت دیجیتالی", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "رونگار", + "@layoutSettingGroup": {}, + "alarmRingInMessage": "هشدار در {duration} زنگ می‌خورد", + "@alarmRingInMessage": {}, + "nextAlarmIn": "پسین: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} و {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours}س", + "@shortHoursString": {}, + "settingsTitle": "پیکربندی‌ها", + "@settingsTitle": {}, + "verticalAlignmentSetting": "چیدمان ستونی", + "@verticalAlignmentSetting": {}, + "horizontalAlignmentSetting": "چیدمان ستانی", + "@horizontalAlignmentSetting": {}, + "separatorSetting": "جدا‌کننده", + "@separatorSetting": {}, + "editTagLabel": "ویرایش برچسب", + "@editTagLabel": {}, + "tagNamePlaceholder": "نام برچسب", + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{۱ ساعت} other{{count} ساعت}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{۱ دقیقه} other{{count} دقیقه}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{۱ ثانیه} other{{count} ثانیه}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{۱ روز} other{{count} روز}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{۱ هفته} other{{count} هفته}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{۱ ماه} other{{count} ماه}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{۱ سال} other{{count} سال}}", + "@yearsString": {}, + "shortMinutesString": "{minutes}د", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}ث", + "@shortSecondsString": {}, + "showNextAlarm": "نمایش هشدار پسین", + "@showNextAlarm": {}, + "showForegroundNotification": "نمایش آگاهنده‌ها در پیش زمینه", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "نمایش آگاهنده ‌های پایا برای بکارگیری پایدار برنامه", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "نمایش انیمیشن‌هایی که بهینه نیستند و می‌تواند مایه کاهش نرخ فریم در دستگاه‌های با کارکرد پایین شوند", + "@extraAnimationSettingDescription": {}, + "notificationPermissionDescription": "اجازه به آگاهنده‌ها که نشان‌داده شوند", + "@notificationPermissionDescription": {}, + "sortGroup": "چینش", + "@sortGroup": {}, + "showFastestLapSetting": "نمایش تندترین چرخه", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "نمایش چرخه‌ی مِدییَه", + "@showAverageLapSetting": {}, + "leftHandedSetting": "سان چپ دست", + "@leftHandedSetting": {}, + "exportSettingsSetting": "برون‌برد", + "@exportSettingsSetting": {}, + "alarmDescriptionFinished": "بی واکرد در آینده", + "@alarmDescriptionFinished": {}, + "alarmDescriptionWeekday": "هر روز‌کاری هفته", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionDates": "On {date}{count, plural, =0{} =1{ و یک تاریخ دیگر} other{ و {count} دیگر تاریخ‌ها}}", + "@alarmDescriptionDates": {}, + "showFiltersSetting": "نمایش پالایه‌ها", + "@showFiltersSetting": {}, + "showSortSetting": "نمایش چینش", + "@showSortSetting": {}, + "relativeTime": "{hours}س {relative, select, ahead{جلو است} behind{عقب است} other{دیگر}}", + "@relativeTime": {} } From 044b4a52e194720dcacda3370393cdf58f1d8e18 Mon Sep 17 00:00:00 2001 From: ulracte Date: Thu, 11 Jul 2024 14:45:05 +0000 Subject: [PATCH 119/177] Translated using Weblate (Persian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 6b3f9bdf..8d80ba57 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -137,7 +137,7 @@ "@overrideAccentSetting": {}, "accentColorSetting": "رنگ مهند", "@accentColorSetting": {}, - "systemDarkModeSetting": "حالت تیره سامانه", + "systemDarkModeSetting": "سان تیره سامانه", "@systemDarkModeSetting": {}, "colorSchemeSetting": "الگو رنگ", "@colorSchemeSetting": {}, From 81cdc7f8575b5222a7d0851b1eeaa84e6199cd43 Mon Sep 17 00:00:00 2001 From: ulracte Date: Mon, 15 Jul 2024 11:20:05 +0000 Subject: [PATCH 120/177] Translated using Weblate (Persian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 8d80ba57..1012f07b 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -597,7 +597,7 @@ "@importSettingsSetting": {}, "importSettingsSettingDescription": "درون‌برد پیکربندی‌ها از پرونده‌ای برزنی", "@importSettingsSettingDescription": {}, - "versionLabel": "نسخه", + "versionLabel": "نگارش", "@versionLabel": {}, "packageNameLabel": "نام بسته", "@packageNameLabel": {}, From 1c40a00ca0c6df72b01a6899b0dafc7ec3363ea2 Mon Sep 17 00:00:00 2001 From: ulracte Date: Tue, 16 Jul 2024 18:52:20 +0000 Subject: [PATCH 121/177] Translated using Weblate (Persian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fa/ --- lib/l10n/app_fa.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 1012f07b..946d1a00 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -293,7 +293,7 @@ "@retypeLowercaseSetting": {}, "numberOfProblemsSetting": "شمار پرسش‌ها", "@numberOfProblemsSetting": {}, - "yesButton": "بله", + "yesButton": "آری", "@yesButton": {}, "noAlarmMessage": "هشداری ساخته نشده است", "@noAlarmMessage": {}, From 54fea5dccaeb161ae494251a8d3045f475ae09e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABlle=20van=20Essen?= Date: Thu, 18 Jul 2024 22:16:18 +0200 Subject: [PATCH 122/177] Added translation using Weblate (Dutch) --- lib/l10n/app_nl.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/app_nl.arb diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lib/l10n/app_nl.arb @@ -0,0 +1 @@ +{} From 22e85df8db4726900d445a060fbeae71b04fd66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABlle=20van=20Essen?= Date: Thu, 18 Jul 2024 20:17:38 +0000 Subject: [PATCH 123/177] Translated using Weblate (Dutch) Currently translated at 10.7% (38 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/nl/ --- lib/l10n/app_nl.arb | 87 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 0967ef42..53183ef1 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1 +1,86 @@ -{} +{ + "timerTitle": "Timer", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "generalSettingGroupDescription": "App-brede instellingen zoals tijdsformaat", + "@generalSettingGroupDescription": {}, + "longDateFormatSetting": "Lange datumformaat", + "@longDateFormatSetting": {}, + "timeFormatSetting": "Tijdsformaat", + "@timeFormatSetting": {}, + "timeFormat12": "12 uur", + "@timeFormat12": {}, + "timeFormat24": "24 uur", + "@timeFormat24": {}, + "pickerInput": "Invoerveld", + "@pickerInput": {}, + "timePickerSetting": "Tijdsselectie", + "@timePickerSetting": {}, + "pickerDial": "Wijzerplaat", + "@pickerDial": {}, + "durationPickerSetting": "Duurselectie", + "@durationPickerSetting": {}, + "pickerRings": "Ringen", + "@pickerRings": {}, + "swipeActionSetting": "Swipeactie", + "@swipeActionSetting": {}, + "swipActionCardAction": "Kaartacties", + "@swipActionCardAction": {}, + "swipeActionCardActionDescription": "Swipe naar links of rechts op de kaart om acties uit te voeren", + "@swipeActionCardActionDescription": {}, + "swipeActionSwitchTabsDescription": "Swipe tussen tabbladen", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Melodieën", + "@melodiesSetting": {}, + "vendorSetting": "Fabrikantsinstellingen", + "@vendorSetting": {}, + "allowNotificationSetting": "Alle notificaties handmatig toestaan", + "@allowNotificationSetting": {}, + "permissionsSettingGroup": "Rechten", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Batterijoptimalisaties negeren", + "@ignoreBatteryOptimizationSetting": {}, + "clockTitle": "Klok", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "languageSetting": "Taal", + "@languageSetting": {}, + "alarmTitle": "Wekker", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "stopwatchTitle": "Stopwatch", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "system": "Systeem", + "@system": {}, + "generalSettingGroup": "Algemeen", + "@generalSettingGroup": {}, + "dateFormatSetting": "Datumformaat", + "@dateFormatSetting": {}, + "timeFormatDevice": "Apparaatinstellingen", + "@timeFormatDevice": {}, + "swipActionSwitchTabs": "Wissel tabbladen", + "@swipActionSwitchTabs": {}, + "showSecondsSetting": "Toon seconden", + "@showSecondsSetting": {}, + "pickerSpinner": "Spinner", + "@pickerSpinner": {}, + "tagsSetting": "Tags", + "@tagsSetting": {}, + "vendorSettingDescription": "Fabrikantsspecifieke optimalisaties handmatig uitschakelen", + "@vendorSettingDescription": {}, + "batteryOptimizationSetting": "Batterijoptimalisaties handmatig uitschakelen", + "@batteryOptimizationSetting": {}, + "batteryOptimizationSettingDescription": "Batterijoptimalisaties uitschakelen voor deze app om vertraging van de wekker te voorkomen", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Meldingen op het vergrendelscherm voor wekkers en timers toestaan", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Sommige toestellen hebben Autostart nodig om wekkers af te laten gaan terwijl de app gesloten is", + "@autoStartSettingDescription": {}, + "autoStartSetting": "Autostart", + "@autoStartSetting": {} +} From 966fd2c0c5baab965627d7bb3c03e4e0d32f7f77 Mon Sep 17 00:00:00 2001 From: "Francisco (F4VSE)" Date: Mon, 22 Jul 2024 21:30:48 +0000 Subject: [PATCH 124/177] Translated using Weblate (Portuguese) Currently translated at 96.3% (340 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pt/ --- lib/l10n/app_pt.arb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7215dd3b..751545f9 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -660,5 +660,31 @@ "tagNamePlaceholder": "Nome da etiqueta", "@tagNamePlaceholder": {}, "longDateFormatSetting": "Formato longo de data", - "@longDateFormatSetting": {} + "@longDateFormatSetting": {}, + "showForegroundNotification": "Mostrar notificação de primeiro plano", + "@showForegroundNotification": {}, + "notificationPermissionDescription": "Permitir a exibição de notificações", + "@notificationPermissionDescription": {}, + "showForegroundNotificationDescription": "Mostrar uma notificação persistente para manter a aplicação ativa", + "@showForegroundNotificationDescription": {}, + "upcomingLeadTimeSetting": "Próximos prazos de entrega", + "@upcomingLeadTimeSetting": {}, + "editPresetsTitle": "Editar predefinições", + "@editPresetsTitle": {}, + "remainingTimeDesc": "Menos tempo restante", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Mais tempo restante", + "@remainingTimeAsc": {}, + "styleThemeSpreadSetting": "Espalhar", + "@styleThemeSpreadSetting": {}, + "lessThanOneMinute": "menos de 1 minuto", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "O alarme tocará em {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Próximo: {duration}", + "@nextAlarmIn": {}, + "combinedTime": "{hours} e {minutes}", + "@combinedTime": {}, + "showNextAlarm": "Mostrar o próximo alarme", + "@showNextAlarm": {} } From fd032546811f6c135a2817910a702d8b067f99d6 Mon Sep 17 00:00:00 2001 From: Nazar Date: Wed, 24 Jul 2024 14:25:21 +0000 Subject: [PATCH 125/177] Translated using Weblate (Ukrainian) Currently translated at 95.4% (337 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/uk/ --- lib/l10n/app_uk.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index bf2a64ab..a4ce233c 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -674,5 +674,17 @@ "relativeTime": "{hours} г. {relative, select, ahead{попереду} behind{позаду} other{Other}}", "@relativeTime": {}, "alignmentJustify": "Вирівняти по ширині", - "@alignmentJustify": {} + "@alignmentJustify": {}, + "hoursString": "{count, plural, =0{} =1{1 година} other{{count} години}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 хвилина} other{{count} хвилини}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунди}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 день} other{{count} дні}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 тиждень} other{{count} тижнів}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 місяць} other{{count} місяці}}", + "@monthsString": {} } From 84d66251d89519a8572bd0000d2ad4b5c2d6cd51 Mon Sep 17 00:00:00 2001 From: DSH Date: Wed, 24 Jul 2024 12:12:50 +0000 Subject: [PATCH 126/177] Translated using Weblate (Dutch) Currently translated at 14.4% (51 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/nl/ --- lib/l10n/app_nl.arb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 53183ef1..46e9d7eb 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -82,5 +82,31 @@ "autoStartSettingDescription": "Sommige toestellen hebben Autostart nodig om wekkers af te laten gaan terwijl de app gesloten is", "@autoStartSettingDescription": {}, "autoStartSetting": "Autostart", - "@autoStartSetting": {} + "@autoStartSetting": {}, + "animationSettingGroup": "Animaties", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Animatie Snelheid", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Extra Animaties", + "@extraAnimationSetting": {}, + "colorSchemeAccentSettingGroup": "Accent", + "@colorSchemeAccentSettingGroup": {}, + "appearanceSettingGroup": "Uiterlijk", + "@appearanceSettingGroup": {}, + "appearanceSettingGroupDescription": "Thema's, kleuren en lay-out wijzigen", + "@appearanceSettingGroupDescription": {}, + "nameField": "Naam", + "@nameField": {}, + "colorSetting": "Kleur", + "@colorSetting": {}, + "textColorSetting": "Tekst", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Kleuren schema", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Achtergrond", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Fout", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Kaart", + "@colorSchemeCardSettingGroup": {} } From 7faedbc48ef86821f0e2bf90dae75e5cf1db4f0c Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sat, 27 Jul 2024 20:26:35 +0000 Subject: [PATCH 127/177] Translated using Weblate (German) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/de/ --- lib/l10n/app_de.arb | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 71ca6c88..341547e6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -675,23 +675,23 @@ "@cityAlreadyInFavorites": {}, "durationPickerTitle": "Dauer wählen", "@durationPickerTitle": {}, - "hoursString": "{count, plural, =0{} =1{1 hour} other{{count} hours}}", + "hoursString": "{count, plural, =0{} =1{1 Stunde} other{{count} Stunden}}", "@hoursString": {}, - "secondsString": "{count, plural, =0{} =1{1 second} other{{count} seconds}}", + "secondsString": "{count, plural, =0{} =1{1 Sekunde} other{{count} Sekunden}}", "@secondsString": {}, - "daysString": "{count, plural, =0{} =1{1 day} other{{count} days}}", + "daysString": "{count, plural, =0{} =1{1 Tag} other{{count} Tage}}", "@daysString": {}, - "weeksString": "{count, plural, =0{} =1{1 week} other{{count} weeks}}", + "weeksString": "{count, plural, =0{} =1{1 Woche} other{{count} Wochen}}", "@weeksString": {}, - "monthsString": "{count, plural, =0{} =1{1 month} other{{count} months}}", + "monthsString": "{count, plural, =0{} =1{1 Monat} other{{count} Monate}}", "@monthsString": {}, - "yearsString": "{count, plural, =0{} =1{1 year} other{{count} years}}", + "yearsString": "{count, plural, =0{} =1{1 Jahr} other{{count} Jahre}}", "@yearsString": {}, "lessThanOneMinute": "weniger als 1 Minute", "@lessThanOneMinute": {}, "alarmRingInMessage": "Der Alarm ertönt in {duration}", "@alarmRingInMessage": {}, - "minutesString": "{count, plural, =0{} =1{1 minute} other{{count} minutes}}", + "minutesString": "{count, plural, =0{} =1{1 Minute} other{{count} Minuten}}", "@minutesString": {}, "nextAlarmIn": "Nächste: {duration}", "@nextAlarmIn": {}, @@ -704,5 +704,13 @@ "shortSecondsString": "{seconds}s", "@shortSecondsString": {}, "showNextAlarm": "Nächsten Alarm anzeigen", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "showForegroundNotification": "Vordergrund-Benachrichtigung anzeigen", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Eine dauerhafte Benachrichtigung anzeigen, um die App am Leben zu erhalten", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Anzeigen von Benachrichtigungen zulassen", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Animationen anzeigen, die nicht optimiert sind und bei leistungsschwachen Geräten zu Bildaussetzern führen können", + "@extraAnimationSettingDescription": {} } From ad46a0cd8822f50ea42964f5c122106e317dee01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliberk=20Sand=C4=B1k=C3=A7=C4=B1?= Date: Sun, 28 Jul 2024 16:33:24 +0000 Subject: [PATCH 128/177] Translated using Weblate (Turkish) Currently translated at 96.8% (342 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/tr/ --- lib/l10n/app_tr.arb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index a07ff149..59712c34 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -686,5 +686,11 @@ "combinedTime": "{hours} ve {minutes}", "@combinedTime": {}, "showNextAlarm": "Sıradaki Alarmı Göster", - "@showNextAlarm": {} + "@showNextAlarm": {}, + "showForegroundNotification": "Ön Plan Bildirimini Göster", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Uygulamayı aktif tutmak için kalıcı bir bildirim gösterin", + "@showForegroundNotificationDescription": {}, + "extraAnimationSettingDescription": "Düşük kaliteli cihazlarda kare düşüşlerine neden olabilecek animasyonlar gösterin", + "@extraAnimationSettingDescription": {} } From 91e9a7fe3f54c8bf3329a201b41e51c2d147bfe1 Mon Sep 17 00:00:00 2001 From: Patricio Carrau Date: Mon, 29 Jul 2024 21:26:29 +0000 Subject: [PATCH 129/177] Translated using Weblate (French) Currently translated at 96.3% (340 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fr/ --- lib/l10n/app_fr.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 6ad478df..e3ad7536 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -674,5 +674,17 @@ "noTagsMessage": "Aucun tag créé", "@noTagsMessage": {}, "separatorSetting": "Séparateur", - "@separatorSetting": {} + "@separatorSetting": {}, + "combinedTime": "{hours} et {minutes}", + "@combinedTime": {}, + "lessThanOneMinute": "moins de 1 minute", + "@lessThanOneMinute": {}, + "nextAlarmIn": "Suivant : {duration}", + "@nextAlarmIn": {}, + "shortHoursString": "{hours}h", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {} } From 6bc1daefd43555c4853d452a38b99342dbb23e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sat, 3 Aug 2024 08:04:58 +0000 Subject: [PATCH 130/177] Translated using Weblate (Turkish) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/tr/ --- lib/l10n/app_tr.arb | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 59712c34..6a17a780 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -691,6 +691,26 @@ "@showForegroundNotification": {}, "showForegroundNotificationDescription": "Uygulamayı aktif tutmak için kalıcı bir bildirim gösterin", "@showForegroundNotificationDescription": {}, - "extraAnimationSettingDescription": "Düşük kaliteli cihazlarda kare düşüşlerine neden olabilecek animasyonlar gösterin", - "@extraAnimationSettingDescription": {} + "extraAnimationSettingDescription": "Düşük özellikli aygıtlarda kare düşüşlerine neden olabilecek animasyonlar göster", + "@extraAnimationSettingDescription": {}, + "shortHoursString": "{hours}sa", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}dak", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}sn", + "@shortSecondsString": {}, + "secondsString": "{count, plural, =0{} =1{1 saniye} other{{count} saniye}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 gün} other{{count} gün}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 hafta} other{{count} hafta}}", + "@weeksString": {}, + "hoursString": "{count, plural, =0{} =1{1 saat} other{{count} saat}}", + "@hoursString": {}, + "minutesString": "{count, plural, =0{} =1{1 dakika} other{{count} dakika}}", + "@minutesString": {}, + "monthsString": "{count, plural, =0{} =1{1 ay} other{{count} ay}}", + "@monthsString": {}, + "yearsString": "{count, plural, =0{} =1{1 yıl} other{{count} yıl}}", + "@yearsString": {} } From f4e9fb5b250afbe3439d85cb7d8535cc6bd14459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D1=80=D0=B5=D0=B2=20=28BurnBird=29?= Date: Thu, 22 Aug 2024 07:57:28 +0000 Subject: [PATCH 131/177] Translated using Weblate (Russian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8d317be7..c08ac51d 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -33,7 +33,7 @@ "@useMaterialYouColorSetting": {}, "overrideAccentSetting": "Изменить цвет акцента", "@overrideAccentSetting": {}, - "materialBrightnessSetting": "Регулировка яркости", + "materialBrightnessSetting": "Тема", "@materialBrightnessSetting": {}, "styleThemeSetting": "Тема", "@styleThemeSetting": {}, From 8a4f60d7aa6d42b304c8085411f81712b2dcbfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9ane=20GRASSER?= Date: Wed, 21 Aug 2024 07:42:22 +0000 Subject: [PATCH 132/177] Translated using Weblate (French) Currently translated at 98.0% (346 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/fr/ --- lib/l10n/app_fr.arb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e3ad7536..6a71c90c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -686,5 +686,17 @@ "shortMinutesString": "{minutes}m", "@shortMinutesString": {}, "shortSecondsString": "{seconds}s", - "@shortSecondsString": {} + "@shortSecondsString": {}, + "showForegroundNotification": "Afficher une notification au premier plan", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Afficher une notification persistante pour empêcher l'appli d'être tuée", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Autorisez les notifications à être affichées", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Afficher des animations en cours de développement qui pourraient causer des chutes de fréquence d'images sur les appareils d'entrée de gamme", + "@extraAnimationSettingDescription": {}, + "alarmRingInMessage": "L'alarme sonnera dans {duration}", + "@alarmRingInMessage": {}, + "showNextAlarm": "Afficher la prochaine alarme", + "@showNextAlarm": {} } From 23423b1a83e98d40288f882fdf7d580d7217dd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Karpi=C5=84ski?= Date: Fri, 23 Aug 2024 03:31:22 +0000 Subject: [PATCH 133/177] Translated using Weblate (Polish) Currently translated at 78.1% (276 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 404 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 385 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 30d81c3f..83955a0c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -3,11 +3,11 @@ "@restoreSettingGroup": {}, "colorsSettingGroup": "Kolory", "@colorsSettingGroup": {}, - "styleSettingGroup": "Styl", + "styleSettingGroup": "Style", "@styleSettingGroup": {}, "useMaterialYouColorSetting": "Użyj Material You", "@useMaterialYouColorSetting": {}, - "materialBrightnessSetting": "Jasność", + "materialBrightnessSetting": "Motyw", "@materialBrightnessSetting": {}, "colorSchemeSetting": "Paleta kolorów", "@colorSchemeSetting": {}, @@ -51,7 +51,7 @@ "@backupSettingGroup": {}, "developerOptionsSettingGroup": "Opcje programistyczne", "@developerOptionsSettingGroup": {}, - "styleThemeSetting": "Motyw", + "styleThemeSetting": "Style motywu", "@styleThemeSetting": {}, "clockSettingGroup": "Zegar", "@clockSettingGroup": {}, @@ -101,9 +101,9 @@ "@todayFilter": {}, "tomorrowFilter": "Jutro", "@tomorrowFilter": {}, - "disabledFilter": "Wyłączono", + "disabledFilter": "Wyłączony", "@disabledFilter": {}, - "completedFilter": "Zakończone", + "completedFilter": "Zakończony", "@completedFilter": {}, "timeFormatDevice": "Ustawienia urządzenia", "@timeFormatDevice": {}, @@ -159,23 +159,23 @@ "@longDateFormatSetting": {}, "timePickerSetting": "Selektor czasu", "@timePickerSetting": {}, - "pickerDial": "Zegar", + "pickerDial": "Tarcza", "@pickerDial": {}, - "pickerInput": "wprowadź", + "pickerInput": "Wprowadzanie", "@pickerInput": {}, - "pickerRings": "dzwonki", + "pickerRings": "Dzwonki", "@pickerRings": {}, - "noTaskMessage": "nie utworzono zadań", + "noTaskMessage": "Nie utworzono zadań", "@noTaskMessage": {}, - "vendorSetting": "ustawienia producenta", + "vendorSetting": "Ustawienia producenta", "@vendorSetting": {}, - "melodiesSetting": "melodie", + "melodiesSetting": "Melodie", "@melodiesSetting": {}, - "autoStartSettingDescription": "niektóre urządzenia wymagają uaktywnienia funkcji auto startu dla alarmów podczas gdy aplikacja jest zamknięta", + "autoStartSettingDescription": "Niektóre urządzenia wymagają włączenia funkcji auto-startu dla alarmów, podczas gdy aplikacja jest zamknięta", "@autoStartSettingDescription": {}, - "colorSchemeBackgroundSettingGroup": "tło", + "colorSchemeBackgroundSettingGroup": "Tło", "@colorSchemeBackgroundSettingGroup": {}, - "nameField": "nazwa", + "nameField": "Nazwa", "@nameField": {}, "colorSetting": "Kolor", "@colorSetting": {}, @@ -187,16 +187,382 @@ "@colorSchemeNamePlaceholder": {}, "tagsSetting": "znaczniki", "@tagsSetting": {}, - "vendorSettingDescription": "ręczne wyłączenie ustawień producenta", + "vendorSettingDescription": "Ręcznie wyłącz optymalizacje specyficzne dla danego producenta urządzenia", "@vendorSettingDescription": {}, - "pickerSpinner": "Wybór", + "pickerSpinner": "Obrotomierz", "@pickerSpinner": {}, "durationPickerSetting": "Wybór czasu trwania", "@durationPickerSetting": {}, - "swipActionCardAction": "Akcje kart", + "swipActionCardAction": "Akcje sekcji", "@swipActionCardAction": {}, - "swipeActionCardActionDescription": "Przesuń w lewo lub w prawo na karcie, aby wykonać czynności", + "swipeActionCardActionDescription": "Przesuń w lewo lub w prawo na sekcji, aby wykonać czynności", "@swipeActionCardActionDescription": {}, "swipActionSwitchTabs": "Przełącz karty", - "@swipActionSwitchTabs": {} + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Przesuwaj między kartami", + "@swipeActionSwitchTabsDescription": {}, + "showIstantAlarmButtonSetting": "Pokaż przycisk natychmiastowego alarmu", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Pokaż przycisk natychmiastowego czasomierza", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Logi", + "@logsSettingGroup": {}, + "maxLogsSetting": "Maksymalna liczba logów", + "@maxLogsSetting": {}, + "alarmLogSetting": "Logi alarmów", + "@alarmLogSetting": {}, + "resetButton": "Resetuj", + "@resetButton": {}, + "previewLabel": "Podgląd", + "@previewLabel": {}, + "cardLabel": "Sekcja", + "@cardLabel": {}, + "accentLabel": "Akcent", + "@accentLabel": {}, + "materialBrightnessLight": "Jasny", + "@materialBrightnessLight": {}, + "materialBrightnessSystem": "Systemowy", + "@materialBrightnessSystem": {}, + "materialBrightnessDark": "Ciemny", + "@materialBrightnessDark": {}, + "accentColorSetting": "Kolor akcentu", + "@accentColorSetting": {}, + "errorLabel": "Błąd", + "@errorLabel": {}, + "alarmWeekdaysSetting": "Dni tygodnia", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Daty", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Zakres dat", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Interwał", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Codziennie", + "@alarmIntervalDaily": {}, + "alarmDeleteAfterFinishingSetting": "Usuń po zakończeniu", + "@alarmDeleteAfterFinishingSetting": {}, + "alarmDeleteAfterRingingSetting": "Usuń po odrzuceniu", + "@alarmDeleteAfterRingingSetting": {}, + "alarmIntervalWeekly": "Co tydzień", + "@alarmIntervalWeekly": {}, + "mathVeryHardDifficulty": "Bardzo trudny (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "sequenceTask": "Sekwencja", + "@sequenceTask": {}, + "taskTryButton": "Wypróbuj", + "@taskTryButton": {}, + "retypeNumberChars": "Liczba znaków", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Uwzględnij liczby", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Uwzględnij małe litery", + "@retypeLowercaseSetting": {}, + "numberOfProblemsSetting": "Liczba działań", + "@numberOfProblemsSetting": {}, + "noAlarmMessage": "Nie utworzono żadnych alarmów", + "@noAlarmMessage": {}, + "noTimerMessage": "Nie utworzono żadnych czasomierzy", + "@noTimerMessage": {}, + "noTagsMessage": "Nie utworzono żadnych znaczników", + "@noTagsMessage": {}, + "noLogsMessage": "Brak logów alarmów", + "@noLogsMessage": {}, + "noStopwatchMessage": "Nie utworzono żadnych stoperów", + "@noStopwatchMessage": {}, + "noPresetsMessage": "Nie utworzono ustawień wstępnych", + "@noPresetsMessage": {}, + "cancelSkipAlarmButton": "Anuluj pominięcie", + "@cancelSkipAlarmButton": {}, + "dismissAlarmButton": "Odrzuć", + "@dismissAlarmButton": {}, + "scheduleDateFilterGroup": "Data zaplanowania", + "@scheduleDateFilterGroup": {}, + "nameAsc": "Etykieta A-Z", + "@nameAsc": {}, + "nameDesc": "Etykieta Z-A", + "@nameDesc": {}, + "timeOfDayAsc": "Najpierw wczesne godziny", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Najpierw późne godziny", + "@timeOfDayDesc": {}, + "filterActions": "Działania filtra", + "@filterActions": {}, + "clearFiltersAction": "Wyczyść wszystkie filtry", + "@clearFiltersAction": {}, + "emailLabel": "E-mail", + "@emailLabel": {}, + "viewOnGithubLabel": "Zobacz na GitHubie", + "@viewOnGithubLabel": {}, + "thursdayShort": "Czw", + "@thursdayShort": {}, + "fridayShort": "Pt", + "@fridayShort": {}, + "saturdayShort": "Sob", + "@saturdayShort": {}, + "sundayShort": "Ndz", + "@sundayShort": {}, + "mondayLetter": "P", + "@mondayLetter": {}, + "tuesdayLetter": "W", + "@tuesdayLetter": {}, + "wednesdayLetter": "Ś", + "@wednesdayLetter": {}, + "thursdayLetter": "C", + "@thursdayLetter": {}, + "fridayLetter": "P", + "@fridayLetter": {}, + "saturdayLetter": "S", + "@saturdayLetter": {}, + "donateDescription": "Przekaż darowiznę, aby wesprzeć rozwój aplikacji", + "@donateDescription": {}, + "donorsDescription": "Nasi hojni patroni", + "@donorsDescription": {}, + "contributorsDescription": "Ludzie, dzięki którym ta aplikacja jest tworzona", + "@contributorsDescription": {}, + "snoozePreventDeletionSetting": "Zapobiegaj usuwaniu", + "@snoozePreventDeletionSetting": {}, + "mathEasyDifficulty": "Łatwy (X + Y)", + "@mathEasyDifficulty": {}, + "mathHardDifficulty": "Trudny (X × Y + Z)", + "@mathHardDifficulty": {}, + "activeFilter": "Aktywny", + "@activeFilter": {}, + "inactiveFilter": "Nieaktywny", + "@inactiveFilter": {}, + "createdDateFilterGroup": "Data utworzenia", + "@createdDateFilterGroup": {}, + "runningTimerFilter": "Uruchomiony", + "@runningTimerFilter": {}, + "logTypeFilterGroup": "Typ", + "@logTypeFilterGroup": {}, + "pausedTimerFilter": "Wstrzymany", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Zatrzymany", + "@stoppedTimerFilter": {}, + "stateFilterGroup": "Stan", + "@stateFilterGroup": {}, + "sortGroup": "Sortowanie", + "@sortGroup": {}, + "defaultLabel": "Donyślne", + "@defaultLabel": {}, + "remainingTimeDesc": "Najkrótszy pozostały czas", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Najdłuższy pozostały czas", + "@remainingTimeAsc": {}, + "durationAsc": "Najkrótszy", + "@durationAsc": {}, + "durationDesc": "Najdłuższy", + "@durationDesc": {}, + "skipAllFilteredAlarmsAction": "Pomiń wszystkie przefiltrowane alarmy", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Anuluj pominięcie wszystkich przefiltrowanych alarmów", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Usuń wszystkie przefiltrowane elementy", + "@deleteAllFilteredAction": {}, + "alarmDescriptionWeekly": "Każdy {days}", + "@alarmDescriptionWeekly": {}, + "versionLabel": "Wersja", + "@versionLabel": {}, + "packageNameLabel": "Nazwa pakietu", + "@packageNameLabel": {}, + "licenseLabel": "Licencja", + "@licenseLabel": {}, + "importSettingsSetting": "Importuj", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Importuj ustawienia z pliku zapisanego lokalnie", + "@importSettingsSettingDescription": {}, + "noLapsMessage": "Brak okrążeń", + "@noLapsMessage": {}, + "elapsedTime": "Łączny czas, który upłynął", + "@elapsedTime": {}, + "tuesdayFull": "Wtorek", + "@tuesdayFull": {}, + "wednesdayFull": "Środa", + "@wednesdayFull": {}, + "sundayLetter": "N", + "@sundayLetter": {}, + "saturdayFull": "Sobota", + "@saturdayFull": {}, + "mondayShort": "Pon", + "@mondayShort": {}, + "styleThemeShapeSettingGroup": "Kształt", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Wysokość", + "@styleThemeElevationSetting": {}, + "maxSnoozesSetting": "Maksymalna ilość drzemek", + "@maxSnoozesSetting": {}, + "mathMediumDifficulty": "Średni (X × Y)", + "@mathMediumDifficulty": {}, + "dateFilterGroup": "Data", + "@dateFilterGroup": {}, + "enableAllFilteredAlarmsAction": "Włącz wszystkie przefiltrowane alarmy", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Wyłącz wszystkie przefiltrowane alarmy", + "@disableAllFilteredAlarmsAction": {}, + "openSourceLicensesSetting": "Licencje Open Source", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Współtwórcy", + "@contributorsSetting": {}, + "donorsSetting": "Darczyńcy", + "@donorsSetting": {}, + "donateButton": "Przekaż darowiznę", + "@donateButton": {}, + "searchSettingPlaceholder": "Wyszukaj ustawienie", + "@searchSettingPlaceholder": {}, + "cityAlreadyInFavorites": "To miasto jest już na liście ulubionych", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Wybierz czas trwania", + "@durationPickerTitle": {}, + "thursdayFull": "Czwartek", + "@thursdayFull": {}, + "tuesdayShort": "Wt", + "@tuesdayShort": {}, + "wednesdayShort": "Śr", + "@wednesdayShort": {}, + "searchCityPlaceholder": "Wyszukaj miasto", + "@searchCityPlaceholder": {}, + "alarmDescriptionEveryDay": "Codziennie", + "@alarmDescriptionEveryDay": {}, + "editButton": "Edytuj", + "@editButton": {}, + "mondayFull": "Poniedziałek", + "@mondayFull": {}, + "fridayFull": "Piątek", + "@fridayFull": {}, + "sundayFull": "Niedziela", + "@sundayFull": {}, + "reliabilitySettingGroup": "Niezawodność", + "@reliabilitySettingGroup": {}, + "displaySettingGroup": "Wyświetlanie", + "@displaySettingGroup": {}, + "systemDarkModeSetting": "Systemowy tryb ciemny", + "@systemDarkModeSetting": {}, + "stopwatchSettingGroup": "Stoper", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Eksportuj lub importuj swoje ustawienia lokalnie", + "@backupSettingGroupDescription": {}, + "saveButton": "Zapisz", + "@saveButton": {}, + "labelFieldPlaceholder": "Dodaj etykietę", + "@labelFieldPlaceholder": {}, + "scheduleTypeWeek": "W określone dni tygodnia", + "@scheduleTypeWeek": {}, + "scheduleTypeRange": "Zakres dni", + "@scheduleTypeRange": {}, + "snoozeSettingGroup": "Drzemka", + "@snoozeSettingGroup": {}, + "whileSnoozedSettingGroup": "Podczas drzemki", + "@whileSnoozedSettingGroup": {}, + "soundSettingGroup": "Dźwięk", + "@soundSettingGroup": {}, + "snoozePreventDisablingSetting": "Zapobiegaj wyłączaniu", + "@snoozePreventDisablingSetting": {}, + "mathTask": "Działania matematyczne", + "@mathTask": {}, + "retypeTask": "Przepisywanie tekstu", + "@retypeTask": {}, + "audioChannelSetting": "Kanał audio", + "@audioChannelSetting": {}, + "scheduleTypeDateDescription": "Będzie powtarzany w określonych dniach", + "@scheduleTypeDateDescription": {}, + "scheduleTypeRangeDescription": "Będzie powtarzany w określonym zakresie dni", + "@scheduleTypeRangeDescription": {}, + "risingVolumeSetting": "Rosnąca głośność", + "@risingVolumeSetting": {}, + "chooseTaskTitle": "Wybierz Zadanie do dodania", + "@chooseTaskTitle": {}, + "skipAlarmButton": "Pomiń następny alarm", + "@skipAlarmButton": {}, + "snoozedFilter": "Ustawiona drzemka", + "@snoozedFilter": {}, + "skippingDescriptionSuffix": "(pominięcie następnego zdarzenia)", + "@skippingDescriptionSuffix": {}, + "sequenceLengthSetting": "Długość sekwencji", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Rozmiar siatki", + "@sequenceGridSizeSetting": {}, + "saveReminderAlert": "Czy chcesz wyjść bez zapisywania?", + "@saveReminderAlert": {}, + "alarmDescriptionFinished": "Brak przyszłych terminów", + "@alarmDescriptionFinished": {}, + "alarmDescriptionSnooze": "Drzemka do {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionNotScheduled": "Nie zaplanowano", + "@alarmDescriptionNotScheduled": {}, + "alarmDescriptionTomorrow": "Tylko jutro", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionToday": "Tylko dzisiaj", + "@alarmDescriptionToday": {}, + "alarmDescriptionDays": "{days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Codziennie} weekly{Co tydzień} other{Inne}} od {startDate} do {endDate}", + "@alarmDescriptionRange": {}, + "alarmDescriptionWeekend": "W każdy weekend", + "@alarmDescriptionWeekend": {}, + "colorSchemeAccentSettingGroup": "Akcent", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Błąd", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Kontur", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Cień", + "@colorSchemeShadowSettingGroup": {}, + "colorSchemeUseAccentAsShadowSetting": "Używaj koloru akcentu w cieniu", + "@colorSchemeUseAccentAsShadowSetting": {}, + "colorSchemeUseAccentAsOutlineSetting": "Używaj koloru akcentu w konturze", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeCardSettingGroup": "Sekcja", + "@colorSchemeCardSettingGroup": {}, + "styleThemeNamePlaceholder": "Styl Motywu", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Cień", + "@styleThemeShadowSettingGroup": {}, + "styleThemeBlurSetting": "Rozmycie", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineSettingGroup": "Kontur", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Szerokość", + "@styleThemeOutlineWidthSetting": {}, + "styleThemeRadiusSetting": "Zaokrąglenie narożników", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Krycie", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "Rozpiętość", + "@styleThemeSpreadSetting": {}, + "layoutSettingGroup": "Układ", + "@layoutSettingGroup": {}, + "widgetsSettingGroup": "Widżety", + "@widgetsSettingGroup": {}, + "digitalClockSettingGroup": "Zegar cyfrowy", + "@digitalClockSettingGroup": {}, + "showDateSetting": "Pokaż datę", + "@showDateSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Nie można wyłączyć alarmu podczas drzemki", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "scheduleTypeOnceDescription": "Zadzwoni przy następnym nadejściu tej godziny", + "@scheduleTypeOnceDescription": {}, + "timeToFullVolumeSetting": "Czas do osiągnięcia pełnej głośności", + "@timeToFullVolumeSetting": {}, + "timePickerModeButton": "Tryb", + "@timePickerModeButton": {}, + "scheduleTypeDailyDescription": "Zadzwoni każdego dnia", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeekDescription": "Będzie powtarzany w określone dni tygodnia", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "W określonych dniach", + "@scheduleTypeDate": {}, + "soundAndVibrationSettingGroup": "Dźwięk i wibracje", + "@soundAndVibrationSettingGroup": {}, + "audioChannelAlarm": "Alarmy", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Powiadomienia", + "@audioChannelNotification": {}, + "audioChannelRingtone": "Dzwonek", + "@audioChannelRingtone": {}, + "audioChannelMedia": "Multimedia", + "@audioChannelMedia": {}, + "textSettingGroup": "Tekst", + "@textSettingGroup": {}, + "settingsTitle": "Ustawienia", + "@settingsTitle": {} } From 323c1e9603928f82a2b197141f8e8b6bd62c2aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Karpi=C5=84ski?= Date: Sat, 24 Aug 2024 18:02:54 +0000 Subject: [PATCH 134/177] Translated using Weblate (Polish) Currently translated at 88.9% (314 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/pl/ --- lib/l10n/app_pl.arb | 82 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 83955a0c..9240194f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -141,7 +141,7 @@ "@batteryOptimizationSetting": {}, "batteryOptimizationSettingDescription": "Wyłącz optymalizacje baterii dla tej aplikacji, aby alarmy nie były opóźnione", "@batteryOptimizationSettingDescription": {}, - "allowNotificationSettingDescription": "Zezwalaj na powiadomienia na ekranie blokady dla alarmów i minutników", + "allowNotificationSettingDescription": "Zezwalaj na powiadomienia na ekranie blokady dla alarmów i czasomierzy", "@allowNotificationSettingDescription": {}, "allowNotificationSetting": "Zezwól na wszystkie powiadomienia", "@allowNotificationSetting": {}, @@ -179,7 +179,7 @@ "@nameField": {}, "colorSetting": "Kolor", "@colorSetting": {}, - "appearanceSettingGroupDescription": "ustawienia schematu kolorów i", + "appearanceSettingGroupDescription": "Ustawiaj motywy, kolory i zmieniaj układ", "@appearanceSettingGroupDescription": {}, "textColorSetting": "tekst", "@textColorSetting": {}, @@ -555,7 +555,7 @@ "@soundAndVibrationSettingGroup": {}, "audioChannelAlarm": "Alarmy", "@audioChannelAlarm": {}, - "audioChannelNotification": "Powiadomienia", + "audioChannelNotification": "Powiadomienie", "@audioChannelNotification": {}, "audioChannelRingtone": "Dzwonek", "@audioChannelRingtone": {}, @@ -564,5 +564,79 @@ "textSettingGroup": "Tekst", "@textSettingGroup": {}, "settingsTitle": "Ustawienia", - "@settingsTitle": {} + "@settingsTitle": {}, + "showForegroundNotification": "Pokaż powiadomienie na pierwszym planie", + "@showForegroundNotification": {}, + "showForegroundNotificationDescription": "Wyświetlaj stałe powiadomienie, aby utrzymać działanie aplikacji", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Zezwalaj na wyświetlanie powiadomień", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Wyświetlanie animacji, które nie są dopracowane i mogą powodować spadki klatek na słabszych urządzeniach", + "@extraAnimationSettingDescription": {}, + "notificationsSettingGroup": "Powiadomienia", + "@notificationsSettingGroup": {}, + "showUpcomingAlarmNotificationSetting": "Pokazuj powiadomienia o nadchodzących alarmach", + "@showUpcomingAlarmNotificationSetting": {}, + "showSnoozeNotificationSetting": "Pokazuj powiadomienia o drzemce", + "@showSnoozeNotificationSetting": {}, + "showNotificationSetting": "Pokaż powiadomienie", + "@showNotificationSetting": {}, + "presetsSetting": "Ustawienia wstępne", + "@presetsSetting": {}, + "dismissActionSetting": "Odrzuć za pomocą", + "@dismissActionSetting": {}, + "dismissActionSlide": "Suwak", + "@dismissActionSlide": {}, + "dismissActionButtons": "Przyciski", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Duże przyciski", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Format czasu", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Pokaż milisekundy", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Porównanie okrążeń", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "Pokaż poprzednie okrążenie", + "@showPreviousLapSetting": {}, + "exportSettingsSetting": "Eksportuj", + "@exportSettingsSetting": {}, + "addLengthSetting": "Przycisk do dodawania czasu trwania", + "@addLengthSetting": {}, + "relativeTime": "{hours}h {relative, select, ahead{do przodu} behind{do tyłu} other{Inne}}", + "@relativeTime": {}, + "defaultSettingGroup": "Ustawienia domyślne", + "@defaultSettingGroup": {}, + "alarmsDefaultSettingGroupDescription": "Ustaw wartości domyślne dla nowych alarmów", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Ustaw wartości domyślne dla nowych czasomierzy", + "@timerDefaultSettingGroupDescription": {}, + "filtersSettingGroup": "Filtry", + "@filtersSettingGroup": {}, + "showFiltersSetting": "Pokaż filtry", + "@showFiltersSetting": {}, + "showSortSetting": "Pokaż sortowanie", + "@showSortSetting": {}, + "exportSettingsSettingDescription": "Eksportuj ustawienia do pliku lokalnego", + "@exportSettingsSettingDescription": {}, + "sameTime": "Ten sam czas", + "@sameTime": {}, + "newPresetPlaceholder": "Nowe ustawienie wstępne", + "@newPresetPlaceholder": {}, + "shortSecondsString": "{seconds}s", + "@shortSecondsString": {}, + "combinedTime": "{hours} i {minutes}", + "@combinedTime": {}, + "shortMinutesString": "{minutes}m", + "@shortMinutesString": {}, + "showNextAlarm": "Pokaż następny alarm", + "@showNextAlarm": {}, + "showFastestLapSetting": "Pokaż najszybsze okrążenie", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Pokaż średnią okrążeń", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Pokaż najwolniejsze okrążenie", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Tryb leworęczny", + "@leftHandedSetting": {} } From a06f18c949400c834f5398d07179589bf46ffaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A6=D1=80=D0=BD=D0=BE=D0=B1=D0=BE=D0=B3?= <68vuletic@gmail.com> Date: Mon, 26 Aug 2024 13:39:55 +0200 Subject: [PATCH 135/177] Added translation using Weblate (Serbian) --- lib/l10n/app_sr.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/app_sr.arb diff --git a/lib/l10n/app_sr.arb b/lib/l10n/app_sr.arb new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lib/l10n/app_sr.arb @@ -0,0 +1 @@ +{} From 30206532680ce809d888e458b12e30ef90012f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A6=D1=80=D0=BD=D0=BE=D0=B1=D0=BE=D0=B3?= <68vuletic@gmail.com> Date: Mon, 26 Aug 2024 11:41:37 +0000 Subject: [PATCH 136/177] Translated using Weblate (Serbian) Currently translated at 27.4% (97 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/sr/ --- lib/l10n/app_sr.arb | 205 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_sr.arb b/lib/l10n/app_sr.arb index 0967ef42..59eac2ac 100644 --- a/lib/l10n/app_sr.arb +++ b/lib/l10n/app_sr.arb @@ -1 +1,204 @@ -{} +{ + "timerTitle": "Тајмер", + "@timerTitle": { + "description": "Title of the timer screen" + }, + "stopwatchTitle": "Штоперица", + "@stopwatchTitle": { + "description": "Title of the stopwatch screen" + }, + "generalSettingGroupDescription": "Поставите општа подешавања апликације, као што је формат времена", + "@generalSettingGroupDescription": {}, + "timeFormat12": "12-часовни", + "@timeFormat12": {}, + "timeFormat24": "24-часовни", + "@timeFormat24": {}, + "timeFormatDevice": "Подешавања уређаја", + "@timeFormatDevice": {}, + "showSecondsSetting": "Прикажи секунде", + "@showSecondsSetting": {}, + "timePickerSetting": "Избор времена", + "@timePickerSetting": {}, + "pickerDial": "Бројчаник", + "@pickerDial": {}, + "pickerInput": "Унос", + "@pickerInput": {}, + "pickerSpinner": "Ротациони бирач", + "@pickerSpinner": {}, + "durationPickerSetting": "Избор трајања", + "@durationPickerSetting": {}, + "swipActionCardAction": "Акције на картицама", + "@swipActionCardAction": {}, + "swipActionSwitchTabs": "Промени картицу", + "@swipActionSwitchTabs": {}, + "swipeActionSwitchTabsDescription": "Прелистајте између картица", + "@swipeActionSwitchTabsDescription": {}, + "melodiesSetting": "Мелодије", + "@melodiesSetting": {}, + "tagsSetting": "Ознаке", + "@tagsSetting": {}, + "vendorSetting": "Подешавања произвођача", + "@vendorSetting": {}, + "vendorSettingDescription": "Ручно онемогући оптимизације специфичне за произвођача", + "@vendorSettingDescription": {}, + "batteryOptimizationSettingDescription": "Онемогућите оптимизацију батерије за ову апликацију како бисте спречили одлагање alarma", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Дозволите обавештења на закључаном екрану за аларме и тајмере", + "@allowNotificationSettingDescription": {}, + "autoStartSettingDescription": "Неки уређаји захтевају да буде омогућено \"Аутоматско покретање\" како би аларми звонили док је апликација затворена", + "@autoStartSettingDescription": {}, + "autoStartSetting": "Аутоматско покретање", + "@autoStartSetting": {}, + "permissionsSettingGroup": "Дозволе", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Игнориши оптимизацију батерије", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Дозвола за обавештења", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Дозвола за обавештења је већ дата", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Дозвола за игнорисање оптимизације батерије је већ дата", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "appearanceSettingGroupDescription": "Поставите теме, боје и промените изглед", + "@appearanceSettingGroupDescription": {}, + "nameField": "Назив", + "@nameField": {}, + "colorSetting": "Боја", + "@colorSetting": {}, + "textColorSetting": "Текст", + "@textColorSetting": {}, + "colorSchemeNamePlaceholder": "Шема боја", + "@colorSchemeNamePlaceholder": {}, + "colorSchemeBackgroundSettingGroup": "Позадина", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Обод", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Сенка", + "@colorSchemeShadowSettingGroup": {}, + "styleThemeNamePlaceholder": "Тема стила", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Сенка", + "@styleThemeShadowSettingGroup": {}, + "styleThemeShapeSettingGroup": "Облик", + "@styleThemeShapeSettingGroup": {}, + "styleThemeElevationSetting": "Уздигнутост", + "@styleThemeElevationSetting": {}, + "styleThemeBlurSetting": "Замућење", + "@styleThemeBlurSetting": {}, + "styleThemeOutlineSettingGroup": "Обод", + "@styleThemeOutlineSettingGroup": {}, + "styleThemeOutlineWidthSetting": "Ширина", + "@styleThemeOutlineWidthSetting": {}, + "accessibilitySettingGroup": "Приступачност", + "@accessibilitySettingGroup": {}, + "backupSettingGroup": "Резервна копија", + "@backupSettingGroup": {}, + "developerOptionsSettingGroup": "Опције за програмере", + "@developerOptionsSettingGroup": {}, + "showIstantAlarmButtonSetting": "Прикажи дугме за тренутни аларм", + "@showIstantAlarmButtonSetting": {}, + "showIstantTimerButtonSetting": "Прикажи дугме за тренутни тајмер", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Дневници", + "@logsSettingGroup": {}, + "alarmLogSetting": "Дневници аларма", + "@alarmLogSetting": {}, + "aboutSettingGroup": "О апликацији", + "@aboutSettingGroup": {}, + "restoreSettingGroup": "Врати подразумеване вредности", + "@restoreSettingGroup": {}, + "resetButton": "Врати", + "@resetButton": {}, + "previewLabel": "Преглед", + "@previewLabel": {}, + "cardLabel": "Картица", + "@cardLabel": {}, + "displaySettingGroup": "Екран", + "@displaySettingGroup": {}, + "reliabilitySettingGroup": "Поузданост", + "@reliabilitySettingGroup": {}, + "colorsSettingGroup": "Боје", + "@colorsSettingGroup": {}, + "styleSettingGroup": "Стил", + "@styleSettingGroup": {}, + "materialBrightnessLight": "Светлост", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Тамно", + "@materialBrightnessDark": {}, + "colorSchemeUseAccentAsOutlineSetting": "Боја нагласка као Обод", + "@colorSchemeUseAccentAsOutlineSetting": {}, + "colorSchemeUseAccentAsShadowSetting": "Боја нагласка као Сенка", + "@colorSchemeUseAccentAsShadowSetting": {}, + "accentLabel": "Боја нагласка", + "@accentLabel": {}, + "useMaterialStyleSetting": "Користи \"Meterial\" стил", + "@useMaterialStyleSetting": {}, + "alarmTitle": "Аларм", + "@alarmTitle": { + "description": "Title of the alarm screen" + }, + "clockTitle": "Сат", + "@clockTitle": { + "description": "Title of the clock screen" + }, + "system": "Систем", + "@system": {}, + "generalSettingGroup": "Опште", + "@generalSettingGroup": {}, + "timeFormatSetting": "Формат времена", + "@timeFormatSetting": {}, + "languageSetting": "Језик", + "@languageSetting": {}, + "longDateFormatSetting": "Дугачки формат датума", + "@longDateFormatSetting": {}, + "dateFormatSetting": "Формат датума", + "@dateFormatSetting": {}, + "pickerRings": "Прстенови", + "@pickerRings": {}, + "swipeActionSetting": "При превлачењу", + "@swipeActionSetting": {}, + "swipeActionCardActionDescription": "Превуците лево или десно на картици да бисте извршили акције", + "@swipeActionCardActionDescription": {}, + "batteryOptimizationSetting": "Ручно онемогући оптимизацију батерије", + "@batteryOptimizationSetting": {}, + "allowNotificationSetting": "Ручно дозволите сва обавештења", + "@allowNotificationSetting": {}, + "animationSettingGroup": "Анимације", + "@animationSettingGroup": {}, + "extraAnimationSetting": "Додатне анимације", + "@extraAnimationSetting": {}, + "animationSpeedSetting": "Брзина анимације", + "@animationSpeedSetting": {}, + "appearanceSettingGroup": "Изглед", + "@appearanceSettingGroup": {}, + "colorSchemeAccentSettingGroup": "Боја нагласка", + "@colorSchemeAccentSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Грешка", + "@colorSchemeErrorSettingGroup": {}, + "colorSchemeCardSettingGroup": "Картица", + "@colorSchemeCardSettingGroup": {}, + "styleThemeRadiusSetting": "Заобљеност углова", + "@styleThemeRadiusSetting": {}, + "errorLabel": "Грешка", + "@errorLabel": {}, + "styleThemeOpacitySetting": "Видљивост", + "@styleThemeOpacitySetting": {}, + "styleThemeSpreadSetting": "Распрострањеност", + "@styleThemeSpreadSetting": {}, + "maxLogsSetting": "Максималан број дневника", + "@maxLogsSetting": {}, + "materialBrightnessSystem": "Систем", + "@materialBrightnessSystem": {}, + "useMaterialYouColorSetting": "Користи \"Material You\"", + "@useMaterialYouColorSetting": {}, + "materialBrightnessSetting": "Осветљеност", + "@materialBrightnessSetting": {}, + "overrideAccentSetting": "Превазиђи боју нагласка", + "@overrideAccentSetting": {}, + "accentColorSetting": "Боја нагласка", + "@accentColorSetting": {}, + "styleThemeSetting": "Стил теме", + "@styleThemeSetting": {}, + "systemDarkModeSetting": "Системски тамни режим", + "@systemDarkModeSetting": {} +} From c1f8c770cffcea81c85bf0f08d5daeb8c4e00414 Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Wed, 28 Aug 2024 08:52:51 +0000 Subject: [PATCH 137/177] Translated using Weblate (Russian) Currently translated at 100.0% (353 of 353 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index c08ac51d..85a67bf3 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -35,7 +35,7 @@ "@overrideAccentSetting": {}, "materialBrightnessSetting": "Тема", "@materialBrightnessSetting": {}, - "styleThemeSetting": "Тема", + "styleThemeSetting": "Тема оформления", "@styleThemeSetting": {}, "useMaterialStyleSetting": "Использовать Material дизайн", "@useMaterialStyleSetting": {}, From 65c234da3dadeb40ddab4d303da8176a64b21629 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 07:20:45 +0500 Subject: [PATCH 138/177] Add option to start alarm at random position --- lib/alarm/data/alarm_settings_schema.dart | 22 +++++++++----- lib/alarm/logic/schedule_alarm.dart | 2 +- lib/alarm/types/alarm.dart | 1 + lib/audio/types/ringtone_player.dart | 37 +++++++++++++++++------ lib/l10n/app_en.arb | 4 +++ 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index d690d7d2..bd5da5ca 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -195,14 +195,14 @@ 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", (context) => AppLocalizations.of(context)!.volumeSetting, @@ -234,6 +234,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", diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index c2a41fc7..cd4012f6 100644 --- a/lib/alarm/logic/schedule_alarm.dart +++ b/lib/alarm/logic/schedule_alarm.dart @@ -90,7 +90,7 @@ Future scheduleAlarm( }, ); - logger.i('Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); + logger.i('Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); } } diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 6848cce5..2b4ddba9 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -79,6 +79,7 @@ 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; diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index 091c201e..41e0701e 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:audio_session/audio_session.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/audio/types/ringtone_manager.dart'; @@ -6,12 +8,15 @@ 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); @@ -73,13 +78,12 @@ class RingtonePlayer { // .uri; // } // } - await _play( - uri, - vibrate: alarm.vibrate, - loopMode: LoopMode.one, - volume: alarm.volume / 100, - secondsToMaxVolume: alarm.risingVolumeDuration.inSeconds, - ); + 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, @@ -100,6 +104,7 @@ class RingtonePlayer { static Future setVolume(double volume) async { logger.t("Setting volume to $volume"); + _stopRisingVolume = true; await activePlayer?.setVolume(volume); } @@ -109,8 +114,11 @@ class RingtonePlayer { LoopMode loopMode = LoopMode.one, double volume = 1.0, int secondsToMaxVolume = 0, + bool startAtRandomPos = false, // double duration = double.infinity, }) async { + _stopRisingVolume = false; + RingtoneManager.lastPlayedRingtoneUri = ringtoneUri; if (_vibratorIsAvailable && vibrate) { Vibration.vibrate(pattern: [500, 1000], repeat: 0); @@ -118,7 +126,15 @@ class RingtonePlayer { // activePlayer?. await activePlayer?.stop(); await activePlayer?.setLoopMode(loopMode); - await activePlayer?.setAudioSource(AudioSource.uri(Uri.parse(ringtoneUri))); + 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 @@ -127,7 +143,9 @@ class RingtonePlayer { Future.delayed( Duration(milliseconds: i * (secondsToMaxVolume * 100)), () { - setVolume((i / 10) * volume); + if (!_stopRisingVolume) { + setVolume((i / 10) * volume); + } }, ); } @@ -158,5 +176,6 @@ class RingtonePlayer { await Vibration.cancel(); } RingtoneManager.lastPlayedRingtoneUri = ""; + _stopRisingVolume = false; } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index be91dd2a..c48c08a9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -293,6 +293,10 @@ "@settingGroupMore": {}, "melodySetting": "Melody", "@melodySetting": {}, + "startMelodyAtRandomPos": "Random position", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "Melody will start at a random position", + "@startMelodyAtRandomPosDescription": {}, "vibrationSetting": "Vibration", "@vibrationSetting": {}, "audioChannelSetting": "Audio Channel", From 60545e037b39a42a5008ec867ca7a8b59b8f004e Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 20:05:11 +0500 Subject: [PATCH 139/177] Try adding ringtone directories (fail) --- android/app/src/main/AndroidManifest.xml | 8 +- .../vicolo/chrono/DocumentPickerActivity.kt | 31 ++++ .../kotlin/com/vicolo/chrono/MainActivity.kt | 140 ++++++++++++--- lib/audio/types/ringtone_player.dart | 166 ++++++++++-------- lib/common/utils/list_storage.dart | 28 +-- lib/common/utils/snackbar.dart | 18 +- lib/common/widgets/file_item_card.dart | 2 +- lib/debug/logic/logger.dart | 2 +- lib/debug/types/file_logger_output.dart | 10 +- lib/settings/screens/ringtones_screen.dart | 113 +++++------- lib/system/types/android_platform_file.dart | 97 ++++++++++ pubspec.lock | 28 ++- pubspec.yaml | 1 + 13 files changed, 449 insertions(+), 195 deletions(-) create mode 100644 android/app/src/main/kotlin/com/vicolo/chrono/DocumentPickerActivity.kt create mode 100644 lib/system/types/android_platform_file.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 673baac5..ae52c407 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -66,7 +66,13 @@ - + + + val returnIntent = Intent() + if (result.resultCode == RESULT_OK) { + val uri: Uri? = result.data?.data + if (uri != null) { + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + returnIntent.putExtra("getDirectoryUri", uri.toString()) + } + } + setResult(RESULT_OK, returnIntent) + finish() + } +} diff --git a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt index 6734d0cd..ec29168e 100644 --- a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt +++ b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt @@ -1,39 +1,123 @@ package com.vicolo.chrono -import android.content.Context +import android.app.Activity import android.content.Intent -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar -import java.util.ArrayList -import androidx.annotation.NonNull +import android.net.Uri +import androidx.documentfile.provider.DocumentFile import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.plugin.common.MethodChannel +class MainActivity : FlutterActivity() { + private val channel = "com.vicolo.chrono/documents" + private lateinit var result: MethodChannel.Result + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel) + .setMethodCallHandler { call, result -> + when (call.method) { + "getDirectoryPath" -> { + this.result = result + val intent = Intent(this, DirectoryPickerActivity::class.java) + startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE) + } + "listDirectories" -> { + val uriString = call.argument("uri")!! + val uri = Uri.parse(uriString) + val directories = listDirectories(uri) + result.success(directories) + } + "listFiles" -> { + val uriString = call.argument("uri")!! + val uri = Uri.parse(uriString) + val files = listFiles(uri) + result.success(files) + } + "getFileChunk" -> { + val uriString = call.argument("uri")!! + val offset = call.argument("offset")!! + val chunkSize = call.argument("chunkSize")!! + val uri = Uri.parse(uriString) + val chunk = readFileChunk(uri, offset, chunkSize) + if (chunk != null) { + result.success(chunk) + } else { + result.error("READ_ERROR", "Failed to read file chunk", null) + } + } + else -> result.notImplemented() + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + val pickedDirectoryUri = data?.getStringExtra("getDirectoryUri") + if (pickedDirectoryUri != null) { + result.success(pickedDirectoryUri) + } else { + result.error("NO_DIRECTORY_URI", "No directory URI found", null) + } + } + } -class MainActivity: FlutterActivity() { - private val CHANNEL = "com.flux.clock/alarm" - - // // create static method channel - // companion object { - // lateinit var channel: MethodChannel - // } + private fun listDirectories(uri: Uri): List { + val directory = DocumentFile.fromTreeUri(this, uri) + val directories = mutableListOf() - // override fun onCreate(savedInstanceState: Bundle?) { - // super.onCreate(savedInstanceState) - // // MethodChannelHolder.init(flutterView) - // // MethodChannelHolder.invokeMethod("onBoot") - // } + if (directory != null && directory.isDirectory) { + for (item in directory.listFiles()) { + if (item.isDirectory) { + directories.add(item.uri.toString()) + } + } + } - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - // GeneratedPluginRegistrant.registerWith(flutterEngine) - // flutterEngine.plugins.add(InitiateCallsToDartInBgPlugin()) - + return directories + } + + private fun listFiles(uri: Uri): List> { + val directory = DocumentFile.fromTreeUri(this, uri) + val files = mutableListOf>() + + if (directory != null && directory.isDirectory) { + for (item in directory.listFiles()) { + if (item.isFile) { + val fileInfo: Map = mapOf( + "uri" to item.uri.toString(), + "name" to (item.name ?: "Unknown"), + "size" to item.length(), + "modified" to item.lastModified() + ) + files.add(fileInfo) + } + } + } + + return files + } + + private fun readFileChunk(uri: Uri, offset: Int, chunkSize: Int): ByteArray? { + return try { + contentResolver.openInputStream(uri)?.use { inputStream -> + inputStream.skip(offset.toLong()) + val buffer = ByteArray(chunkSize) + val bytesRead = inputStream.read(buffer, 0, chunkSize) + if (bytesRead != -1) { + buffer.copyOf(bytesRead) + } else { + null + } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + companion object { + const val PICK_DIRECTORY_REQUEST_CODE = 1 } -} \ No newline at end of file +} diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index 41e0701e..e4f3c8ee 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -3,9 +3,12 @@ import 'dart:math'; import 'package:audio_session/audio_session.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/audio/types/ringtone_manager.dart'; +import 'package:clock_app/common/types/file_item.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:pick_or_save/pick_or_save.dart'; import 'package:vibration/vibration.dart'; Random random = Random(); @@ -39,6 +42,53 @@ class RingtonePlayer { await _play(ringtoneUri, vibrate: vibrate, loopMode: LoopMode.one); } + static Future getDefaultRingtoneUri() async { + return (await loadList("ringtones")) + .firstWhere((ringtone) => ringtone.type == FileItemType.audio) + .uri; + } + + static Future getRingtoneUri(Alarm alarm) async { + switch (alarm.ringtone.type) { + case FileItemType.directory: + try { + logger.t(alarm.ringtone.uri); + // logger.t( + // 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) { + logger.t("Audio files found in directory ${alarm.ringtone.uri}"); + Random random = Random(); + int index = random.nextInt(documentFiles.length); + DocumentFile documentFile = documentFiles[index]; + logger.t("${documentFile.name} ${documentFile.uri}"); + return documentFile.uri; + } else { + logger.t( + "No audio files found in directory ${alarm.ringtone.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 alarm.ringtone.uri; + + default: + return await getDefaultRingtoneUri(); + } + } + static Future playAlarm(Alarm alarm, {LoopMode loopMode = LoopMode.one}) async { await activePlayer?.stop(); @@ -48,36 +98,10 @@ 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; - // } - // } + String uri = await getRingtoneUri(alarm); + + logger.t("Playing alarm with uri: $uri"); + await _play(uri, vibrate: alarm.vibrate, loopMode: LoopMode.one, @@ -117,48 +141,52 @@ class RingtonePlayer { bool startAtRandomPos = false, // double duration = double.infinity, }) async { - _stopRisingVolume = false; + 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); - } - }, - ); + 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 { diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart index 0743ff40..a2d29329 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -1,14 +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:clock_app/debug/logic/logger.dart'; -import 'package:flutter/material.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'; @@ -48,9 +47,9 @@ void unwatchList(String key) { } List loadListSync(String key) { - try{ - return listFromString(loadTextFileSync(key)); - }catch(e){ + try { + return listFromString(loadTextFileSync(key)); + } catch (e) { logger.e("Error loading list ($key): $e"); return []; } @@ -62,19 +61,18 @@ Future> loadList(String key) async { Future saveList( String key, List list) async { - await saveTextFile(key, listToString(list)); + await saveTextFile(key, listToString(list)); } Future initList( 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)){ + if (!textFileExistsSync(key)) { logger.i("Initializing $key"); await saveTextFile(key, value); } @@ -92,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; } @@ -125,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 f99b2a6a..7a8e4708 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -44,15 +44,13 @@ SnackBar getSnackbar(String text, } return SnackBar( - content: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 28), - child: Container( - padding: const EdgeInsets.all(16), - alignment: Alignment.centerLeft, - color: color, - // 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, @@ -61,6 +59,6 @@ SnackBar getSnackbar(String text, ), padding: EdgeInsets.zero, elevation: 2, - dismissDirection: DismissDirection.none, + dismissDirection: DismissDirection.vertical, ); } 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/debug/logic/logger.dart b/lib/debug/logic/logger.dart index f0fad6f8..74d93d62 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -7,7 +7,7 @@ var logger = Logger( filter: FileLogFilter(), output: FileLoggerOutput(), printer: PrettyPrinter( - methodCount: 0, // Number of method calls to be displayed + methodCount: 5, // Number of method calls to be displayed errorMethodCount: 8, // Number of method calls if stacktrace is provided lineLength: 80, // Width of the output colors: true, // Colorful log messages diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart index dcb173ed..5213b650 100644 --- a/lib/debug/types/file_logger_output.dart +++ b/lib/debug/types/file_logger_output.dart @@ -14,13 +14,19 @@ class FileLoggerOutput extends LogOutput { print(line); } - _writeLog(event.origin.message as String, event.level); + String message = switch (event.origin.message.runtimeType) { + String => event.origin.message as String, + Exception => (event.origin.message as Exception).toString(), + _ => "Unknown error", + }; + + _writeLog(message, event.level); Future(() { if (event.level == Level.error && App.navigatorKey.currentContext != null) { showSnackBar( - App.navigatorKey.currentContext!, event.origin.message as String, + App.navigatorKey.currentContext!, message, error: true, navBar: false, fab: false); } }); diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index 591fa25c..86cd7fb3 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -5,9 +5,11 @@ import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/file_item_card.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; +import 'package:clock_app/debug/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/settings/widgets/settings_top_bar.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:pick_or_save/pick_or_save.dart'; @@ -39,8 +41,10 @@ class _RingtonesScreenState extends State { void _onDeleteItem(FileItem fileItem) { if (!fileItem.isDeletable) return; - final file = File(fileItem.uri); - file.deleteSync(); + if (fileItem.type != FileItemType.directory) { + final file = File(fileItem.uri); + file.deleteSync(); + } RingtonePlayer.stop(); } @@ -81,82 +85,53 @@ class _RingtonesScreenState extends State { ], ), FAB( - // icon: Icons.music_note_rounded, + icon: Icons.music_note_rounded, bottomPadding: 8, onPressed: () async { RingtonePlayer.stop(); - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - mimeTypesFilter: ['audio/*'], - getCachedFilePath: true, - enableMultipleSelection: true, - ), - ); - if (result != null && result.isNotEmpty) { - for (String uri in result) { - final metadata = await PickOrSave() - .fileMetaData(params: FileMetadataParams(filePath: uri)); - var name = metadata.displayName ?? "File"; - name = basenameWithoutExtension(name) - .replaceAll(RegExp(r"[0-9]+"), "") - .replaceAll(".", ""); - final fileItem = FileItem(name, uri, FileItemType.audio); + 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(), uri); + await saveRingtone(fileItem.id.toString(), bytes); _listController.addItem(fileItem); } } - - // Item? themeItem = widget.createThemeItem(); - // await _openCustomizeItemScreen( - // themeItem, - // onSave: (newThemeItem) { - // _listController.addItem(newThemeItem); - // }, - // isNewItem: true, - // ); }, ), - // FAB( - // index: 1, - // icon: Icons.create_new_folder_rounded, - // bottomPadding: 8, - // onPressed: () async { - // RingtonePlayer.stop(); - // String? result = await PickOrSave() - // .directoryPicker(params: const DirectoryPickerParams()); - // - // if (result != null && result.isNotEmpty) { - // List? documentFiles = - // await PickOrSave().directoryDocumentsPicker( - // params: DirectoryDocumentsPickerParams( - // directoryUri: result, - // recurseDirectories: true, - // // allowedExtensions: [".pdf"], - // mimeTypesFilter: ["audio/*"], - // ), - // ); - // if (documentFiles != null) { - // DocumentFile documentFile = documentFiles[0]; - // for (var document in documentFiles) { - // print("${document.name} ${document.uri}"); - // } - // } - // - // // final directory = Directory(result); - // // String name = result.split("/").last; - // String name = basename( - // result.replaceAll("%3A", ":").replaceAll("%2F", "/")); - // // final metadata = await PickOrSave() - // // .fileMetaData(params: FileMetadataParams(filePath: result)); - // print("================ ${name}"); - // final fileItem = FileItem(name, result, FileItemType.directory); - // // fileItem.uri = - // // await saveRingtone(fileItem.id.toString(), result); - // _listController.addItem(fileItem); - // } - // }, - // ) + FAB( + index: 1, + icon: Icons.create_new_folder_rounded, + bottomPadding: 8, + onPressed: () async { + RingtonePlayer.stop(); + 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); + } + }, + ) ], ), ); diff --git a/lib/system/types/android_platform_file.dart b/lib/system/types/android_platform_file.dart new file mode 100644 index 00000000..ccc78b25 --- /dev/null +++ b/lib/system/types/android_platform_file.dart @@ -0,0 +1,97 @@ +// import 'dart:async'; +// import 'dart:io'; +// +// import 'package:file_picker/file_picker.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/services.dart'; +// +// /// Android-specific. uses a platform channel which calls Scoped Storage APIs +// /// so that we can list directory contents and read files from a selected folder +// class AndroidPlatformFile { +// final File file; +// +// AndroidPlatformFile(this.file); +// +// static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); +// +// Stream> openRead() async* { +// const int chunkSize = 1024 * 1024; // 1MB +// int offset = 0; +// bool moreData = true; +// +// while (moreData) { +// try { +// final Map arguments = { +// 'uri': file.uri, +// 'offset': offset, +// 'chunkSize': chunkSize, +// }; +// final List? chunk = await methodChannel.invokeMethod('getFileChunk', arguments); +// if (chunk == null || chunk.isEmpty) { +// moreData = false; +// } else { +// yield Uint8List.fromList(chunk); +// offset += chunk.length; +// } +// } on PlatformException catch (e) { +// if (kDebugMode) print("Failed to get file chunk: ${e.message}"); +// moreData = false; +// } +// } +// } +// } +// +// +// class AndroidPlatformFolder { +// final String? path; +// +// AndroidPlatformFolder({this.path}); +// +// +// +// static const methodChannel = MethodChannel('com.yourCompany.app/documents'); +// +// dispose() { +// if (Platform.isIOS) { +// methodChannel.invokeMethod('stopAccessingSecurityScopedResource'); +// } +// } +// +// Future> files() async { +// if (path != null) { +// final directory = Directory(path!); +// return (await directory.list().toList()) +// .whereType() +// .map((file) => AndroidPlatformFile(file)) +// .toList(); +// } else { +// // Android +// return (await methodChannel.invokeMethod('listFiles', { 'uri': uri! })) +// .map((file) => AndroidPlatformFile(uri: file['uri'], name: file['name'], size: file['size'], modifiedDate: DateTime.fromMillisecondsSinceEpoch(file['modified']))) +// .cast() +// .toList(); +// } +// } +// +// +// Future> folders() async { +// if (path != null) { +// final directory = Directory(path!); +// return (await directory.list().toList()) +// .whereType() +// .map((directory) => PlatformFile(path: directory.path)) +// .toList(); +// } else { +// // Android +// return (await methodChannel.invokeMethod('listDirectories', { 'uri': uri! })) +// .map((folder) => AndroidPlatformFolder(uri: folder)) +// .cast() +// .toList(); +// } +// } +// +// String get name { +// if (path != null) return p.basename(path!); +// return Uri.decodeFull(uri!).split(RegExp(r'[/:]')).last; // Android +// } +// } diff --git a/pubspec.lock b/pubspec.lock index 83d91341..3a46af24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -154,6 +154,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -282,6 +290,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" + url: "https://pub.dev" + source: hosted + version: "8.0.7" flex_color_picker: dependency: "direct main" description: @@ -414,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + url: "https://pub.dev" + source: hosted + version: "2.0.22" flutter_shaders: dependency: transitive description: @@ -1219,10 +1243,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.5.4" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 61ae09a6..30f36605 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: logger: ^2.4.0 flutter_animate: ^4.5.0 quick_actions: ^1.0.7 + file_picker: ^8.0.7 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From 5cbfae79e6f47dfea59fc4818b746568c6412d02 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 22:39:19 +0500 Subject: [PATCH 140/177] Add option to shuffle emlodies --- lib/alarm/screens/alarm_screen.dart | 85 ++++++++++-------- lib/alarm/types/alarm.dart | 6 +- lib/audio/logic/ringtones.dart | 97 +++++++++++++++++++++ lib/audio/logic/system_ringtones.dart | 27 ------ lib/audio/types/ringtone_player.dart | 50 +---------- lib/l10n/app_en.arb | 10 +++ lib/settings/logic/initialize_settings.dart | 2 +- lib/timer/screens/timer_screen.dart | 52 +++++++---- 8 files changed, 196 insertions(+), 133 deletions(-) create mode 100644 lib/audio/logic/ringtones.dart delete mode 100644 lib/audio/logic/system_ringtones.dart diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 00168fe7..886c1dc2 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -6,7 +6,9 @@ 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'; @@ -194,10 +196,9 @@ class _AlarmScreenState extends State { _listController.changeItems((alarms) {}); } - void handleAddAlarmActon(){ + void handleAddAlarmActon() { ScaffoldMessenger.of(context).removeCurrentSnackBar(); - _selectTime(); - + _selectTime(); } List> _getListFilterItems() { @@ -241,6 +242,49 @@ class _AlarmScreenState extends State { } } + 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( @@ -273,43 +317,12 @@ class _AlarmScreenState extends State { isSelectable: true, // header: getNextAlarmWidget(), listFilters: _getListFilterItems(), - 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); - }), - ] - : [], + customActions: _getCustomActions(), sortOptions: _showSort.value ? alarmSortOptions : [], ), FAB( onPressed: handleAddAlarmActon, - ), + ), if (_showInstantAlarmButton.value) FAB( onPressed: () { diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 2b4ddba9..cda6e805 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -346,9 +346,9 @@ class Alarm extends CustomizableListItem { } } - // void _delete() { - // _markedForDeletion = true; - // } + void setRingtone(BuildContext context, int index) { + ; + } void setTime(Time time) { _time = time; diff --git a/lib/audio/logic/ringtones.dart b/lib/audio/logic/ringtones.dart new file mode 100644 index 00000000..76743f91 --- /dev/null +++ b/lib/audio/logic/ringtones.dart @@ -0,0 +1,97 @@ +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/debug/logic/logger.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_system_ringtones/flutter_system_ringtones.dart'; +import 'package:path/path.dart'; +import 'package:pick_or_save/pick_or_save.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; +} + +Future getRingtoneUri(FileItem fileItem) async { + switch (fileItem.type) { + case FileItemType.directory: + try { + logger.t(fileItem.uri); + // logger.t( + // await Directory(alarm.ringtone.uri).list(recursive: true).toList()); + List? documentFiles = + await PickOrSave().directoryDocumentsPicker( + params: DirectoryDocumentsPickerParams( + directoryUri: fileItem.uri, + // recurseDirectories: true, + mimeTypesFilter: ["audio/*"], + ), + ); + if (documentFiles != null && documentFiles.isNotEmpty) { + logger.t("Audio files found in directory ${fileItem.uri}"); + Random random = Random(); + int index = random.nextInt(documentFiles.length); + DocumentFile documentFile = documentFiles[index]; + logger.t("${documentFile.name} ${documentFile.uri}"); + return documentFile.uri; + } 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/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index e4f3c8ee..36c82b08 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -2,6 +2,7 @@ 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/common/types/file_item.dart'; import 'package:clock_app/common/utils/list_storage.dart'; @@ -42,53 +43,8 @@ class RingtonePlayer { await _play(ringtoneUri, vibrate: vibrate, loopMode: LoopMode.one); } - static Future getDefaultRingtoneUri() async { - return (await loadList("ringtones")) - .firstWhere((ringtone) => ringtone.type == FileItemType.audio) - .uri; - } - - static Future getRingtoneUri(Alarm alarm) async { - switch (alarm.ringtone.type) { - case FileItemType.directory: - try { - logger.t(alarm.ringtone.uri); - // logger.t( - // 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) { - logger.t("Audio files found in directory ${alarm.ringtone.uri}"); - Random random = Random(); - int index = random.nextInt(documentFiles.length); - DocumentFile documentFile = documentFiles[index]; - logger.t("${documentFile.name} ${documentFile.uri}"); - return documentFile.uri; - } else { - logger.t( - "No audio files found in directory ${alarm.ringtone.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 alarm.ringtone.uri; - - default: - return await getDefaultRingtoneUri(); - } - } + static Future playAlarm(Alarm alarm, {LoopMode loopMode = LoopMode.one}) async { await activePlayer?.stop(); @@ -98,7 +54,7 @@ class RingtonePlayer { contentType: AndroidAudioContentType.music, )); activePlayer = _alarmPlayer; - String uri = await getRingtoneUri(alarm); + String uri = await getRingtoneUri(alarm.ringtone); logger.t("Playing alarm with uri: $uri"); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c48c08a9..4214c373 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -467,10 +467,20 @@ "@disableAllFilteredAlarmsAction": {}, "skipAllFilteredAlarmsAction": "Skip all filtered alarms", "@skipAllFilteredAlarmsAction": {}, + "shuffleAlarmMelodiesAction": "Shuffle melodies for all filtered alarms", + "@shuffleAlarmMelodiesAction":{}, "cancelSkipAllFilteredAlarmsAction": "Cancel skip all filtered alarms", "@cancelSkipAllFilteredAlarmsAction": {}, "deleteAllFilteredAction": "Delete all filtered items", "@deleteAllFilteredAction": {}, + "resetAllFilteredTimersAction": "Reset all filtered timers", + "@resetAllFilteredTimersAction": {}, + "playAllFilteredTimersAction": "Play all filtered timers", + "@playAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Pause all filtered timers", + "@pauseAllFilteredTimersAction": {}, + "shuffleTimerMelodiesAction": "Shuffle melodies for all filtered timers", + "@shuffleTimerMelodiesAction": {}, "skippingDescriptionSuffix": "(skipping next occurrence)", "@skippingDescriptionSuffix": {}, "alarmDescriptionSnooze": "Snoozed until {date}", diff --git a/lib/settings/logic/initialize_settings.dart b/lib/settings/logic/initialize_settings.dart index 026d88f6..b621cd3c 100644 --- a/lib/settings/logic/initialize_settings.dart +++ b/lib/settings/logic/initialize_settings.dart @@ -4,7 +4,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/alarm_event.dart'; -import 'package:clock_app/audio/logic/system_ringtones.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/clock/data/default_favorite_cities.dart'; import 'package:clock_app/clock/logic/timezone_database.dart'; import 'package:clock_app/clock/types/city.dart'; diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 2c4292be..ebccd870 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:isolate'; import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:clock_app/audio/logic/ringtones.dart'; import 'package:clock_app/common/logic/customize_screen.dart'; +import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; @@ -343,6 +345,36 @@ class _TimerScreenState extends State { return timer; } + List> _getCustomActions() { + if (!_showFilters.value) return []; + return [ + ListFilterCustomAction( + name: "Reset all filtered timers", + icon: Icons.timer_off_rounded, + action: (timers) => _handleResetMultipleTimers(timers)), + ListFilterCustomAction( + name: "Play all filtered timers", + icon: Icons.play_arrow_rounded, + action: (timers) => _handleStartMultipleTimers(timers)), + ListFilterCustomAction( + name: "Pause all filtered timers", + icon: Icons.pause_rounded, + action: (timers) => _handlePauseMultipleTimers(timers)), + ListFilterCustomAction( + name: AppLocalizations.of(context)!.shuffleAlarmMelodiesAction, + icon: Icons.shuffle_rounded, + action: (timers) async { + List randomIndices = + await getNRandomRingtoneIndices(timers.length); + for (var timer in timers) { + final setting = timer.settings.getSetting("Melody") + as DynamicSelectSetting; + setting.setIndex(context, randomIndices.removeAt(0)); + } + }), + ]; + } + @override Widget build(BuildContext context) { return Stack(children: [ @@ -383,25 +415,7 @@ class _TimerScreenState extends State { reloadOnPop: true, listFilters: _showFilters.value ? timerListFilters : [], sortOptions: _showSort.value ? timerSortOptions : [], - customActions: _showFilters.value - ? [ - ListFilterCustomAction( - name: "Reset all filtered timers", - icon: Icons.timer_off_rounded, - action: (timers) => - _handleResetMultipleTimers(timers)), - ListFilterCustomAction( - name: "Play all filtered timers", - icon: Icons.play_arrow_rounded, - action: (timers) => - _handleStartMultipleTimers(timers)), - ListFilterCustomAction( - name: "Pause all filtered timers", - icon: Icons.pause_rounded, - action: (timers) => - _handlePauseMultipleTimers(timers)), - ] - : [], + customActions: _getCustomActions(), ), ), ], From 839c3a5d98af78f0a986831f7fd3ca86ffce083e Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 22:50:31 +0500 Subject: [PATCH 141/177] Update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4ba90a53..772f5a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ migrate_working_dir/ /build/ coverage/ +!/android/gradle/wrapper/gradle-wrapper.jar + # Symbolication related app.*.symbols From 266a50279d0fac19ed9b279df43885e78bdbd65c Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 22:56:16 +0500 Subject: [PATCH 142/177] Remove gradle-wrapper.jar from .gitignore --- .gitignore | 2 -- android/.gitignore | 2 +- android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes 3 files changed, 1 insertion(+), 3 deletions(-) create mode 100755 android/gradle/wrapper/gradle-wrapper.jar diff --git a/.gitignore b/.gitignore index 772f5a6f..4ba90a53 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,6 @@ migrate_working_dir/ /build/ coverage/ -!/android/gradle/wrapper/gradle-wrapper.jar - # Symbolication related app.*.symbols 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/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 zcmafaW0a=B^559DjdyHo$F^PVt zzd|cWgMz^T0YO0lQ8%TE1O06v|NZl~LH{LLQ58WtNjWhFP#}eWVO&eiP!jmdp!%24 z{&z-MK{-h=QDqf+S+Pgi=_wg$I{F28X*%lJ>A7Yl#$}fMhymMu?R9TEB?#6@|Q^e^AHhxcRL$z1gsc`-Q`3j+eYAd<4@z^{+?JM8bmu zSVlrVZ5-)SzLn&LU9GhXYG{{I+u(+6ES+tAtQUanYC0^6kWkks8cG;C&r1KGs)Cq}WZSd3k1c?lkzwLySimkP5z)T2Ox3pNs;PdQ=8JPDkT7#0L!cV? zzn${PZs;o7UjcCVd&DCDpFJvjI=h(KDmdByJuDYXQ|G@u4^Kf?7YkE67fWM97kj6F z973tGtv!k$k{<>jd~D&c(x5hVbJa`bILdy(00%lY5}HZ2N>)a|))3UZ&fUa5@uB`H z+LrYm@~t?g`9~@dFzW5l>=p0hG%rv0>(S}jEzqQg6-jImG%Pr%HPtqIV_Ym6yRydW z4L+)NhcyYp*g#vLH{1lK-hQQSScfvNiNx|?nSn-?cc8}-9~Z_0oxlr~(b^EiD`Mx< zlOLK)MH?nl4dD|hx!jBCIku-lI(&v~bCU#!L7d0{)h z;k4y^X+=#XarKzK*)lv0d6?kE1< zmCG^yDYrSwrKIn04tG)>>10%+ zEKzs$S*Zrl+GeE55f)QjY$ zD5hi~J17k;4VSF_`{lPFwf^Qroqg%kqM+Pdn%h#oOPIsOIwu?JR717atg~!)*CgXk zERAW?c}(66rnI+LqM^l7BW|9dH~5g1(_w$;+AAzSYlqop*=u5}=g^e0xjlWy0cUIT7{Fs2Xqx*8% zW71JB%hk%aV-wjNE0*$;E-S9hRx5|`L2JXxz4TX3nf8fMAn|523ssV;2&145zh{$V z#4lt)vL2%DCZUgDSq>)ei2I`*aeNXHXL1TB zC8I4!uq=YYVjAdcCjcf4XgK2_$y5mgsCdcn2U!VPljXHco>+%`)6W=gzJk0$e%m$xWUCs&Ju-nUJjyQ04QF_moED2(y6q4l+~fo845xm zE5Esx?~o#$;rzpCUk2^2$c3EBRNY?wO(F3Pb+<;qfq;JhMFuSYSxiMejBQ+l8(C-- zz?Xufw@7{qvh$;QM0*9tiO$nW(L>83egxc=1@=9Z3)G^+*JX-z92F((wYiK>f;6 zkc&L6k4Ua~FFp`x7EF;ef{hb*n8kx#LU|6{5n=A55R4Ik#sX{-nuQ}m7e<{pXq~8#$`~6| zi{+MIgsBRR-o{>)CE8t0Bq$|SF`M0$$7-{JqwFI1)M^!GMwq5RAWMP!o6G~%EG>$S zYDS?ux;VHhRSm*b^^JukYPVb?t0O%^&s(E7Rb#TnsWGS2#FdTRj_SR~YGjkaRFDI=d)+bw$rD;_!7&P2WEmn zIqdERAbL&7`iA^d?8thJ{(=)v>DgTF7rK-rck({PpYY$7uNY$9-Z< ze4=??I#p;$*+-Tm!q8z}k^%-gTm59^3$*ByyroqUe02Dne4?Fc%JlO>*f9Zj{++!^ zBz0FxuS&7X52o6-^CYq>jkXa?EEIfh?xdBPAkgpWpb9Tam^SXoFb3IRfLwanWfskJ zIbfU-rJ1zPmOV)|%;&NSWIEbbwj}5DIuN}!m7v4($I{Rh@<~-sK{fT|Wh?<|;)-Z; zwP{t@{uTsmnO@5ZY82lzwl4jeZ*zsZ7w%a+VtQXkigW$zN$QZnKw4F`RG`=@eWowO zFJ6RC4e>Y7Nu*J?E1*4*U0x^>GK$>O1S~gkA)`wU2isq^0nDb`);Q(FY<8V6^2R%= zDY}j+?mSj{bz2>F;^6S=OLqiHBy~7h4VVscgR#GILP!zkn68S^c04ZL3e$lnSU_(F zZm3e`1~?eu1>ys#R6>Gu$`rWZJG&#dsZ?^)4)v(?{NPt+_^Ak>Ap6828Cv^B84fa4 z_`l$0SSqkBU}`f*H#<14a)khT1Z5Z8;=ga^45{l8y*m|3Z60vgb^3TnuUKaa+zP;m zS`za@C#Y;-LOm&pW||G!wzr+}T~Q9v4U4ufu*fLJC=PajN?zN=?v^8TY}wrEeUygdgwr z7szml+(Bar;w*c^!5txLGKWZftqbZP`o;Kr1)zI}0Kb8yr?p6ZivtYL_KA<+9)XFE z=pLS5U&476PKY2aKEZh}%|Vb%!us(^qf)bKdF7x_v|Qz8lO7Ro>;#mxG0gqMaTudL zi2W!_#3@INslT}1DFJ`TsPvRBBGsODklX0`p-M6Mrgn~6&fF`kdj4K0I$<2Hp(YIA z)fFdgR&=qTl#sEFj6IHzEr1sYM6 zNfi!V!biByA&vAnZd;e_UfGg_={}Tj0MRt3SG%BQYnX$jndLG6>ssgIV{T3#=;RI% zE}b!9z#fek19#&nFgC->@!IJ*Fe8K$ZOLmg|6(g}ccsSBpc`)3;Ar8;3_k`FQ#N9&1tm>c|2mzG!!uWvelm zJj|oDZ6-m(^|dn3em(BF&3n12=hdtlb@%!vGuL*h`CXF?^=IHU%Q8;g8vABm=U!vX zT%Ma6gpKQC2c;@wH+A{)q+?dAuhetSxBDui+Z;S~6%oQq*IwSMu-UhMDy{pP z-#GB-a0`0+cJ%dZ7v0)3zfW$eV>w*mgU4Cma{P$DY3|w364n$B%cf()fZ;`VIiK_O zQ|q|(55+F$H(?opzr%r)BJLy6M&7Oq8KCsh`pA5^ohB@CDlMKoDVo5gO&{0k)R0b(UOfd>-(GZGeF}y?QI_T+GzdY$G{l!l% zHyToqa-x&X4;^(-56Lg$?(KYkgJn9W=w##)&CECqIxLe@+)2RhO*-Inpb7zd8txFG6mY8E?N8JP!kRt_7-&X{5P?$LAbafb$+hkA*_MfarZxf zXLpXmndnV3ubbXe*SYsx=eeuBKcDZI0bg&LL-a8f9>T(?VyrpC6;T{)Z{&|D5a`Aa zjP&lP)D)^YYWHbjYB6ArVs+4xvrUd1@f;;>*l zZH``*BxW+>Dd$be{`<&GN(w+m3B?~3Jjz}gB8^|!>pyZo;#0SOqWem%xeltYZ}KxOp&dS=bg|4 zY-^F~fv8v}u<7kvaZH`M$fBeltAglH@-SQres30fHC%9spF8Ld%4mjZJDeGNJR8+* zl&3Yo$|JYr2zi9deF2jzEC) zl+?io*GUGRp;^z+4?8gOFA>n;h%TJC#-st7#r&-JVeFM57P7rn{&k*z@+Y5 zc2sui8(gFATezp|Te|1-Q*e|Xi+__8bh$>%3|xNc2kAwTM!;;|KF6cS)X3SaO8^z8 zs5jV(s(4_NhWBSSJ}qUzjuYMKlkjbJS!7_)wwVsK^qDzHx1u*sC@C1ERqC#l%a zk>z>m@sZK{#GmsB_NkEM$$q@kBrgq%=NRBhL#hjDQHrI7(XPgFvP&~ZBJ@r58nLme zK4tD}Nz6xrbvbD6DaDC9E_82T{(WRQBpFc+Zb&W~jHf1MiBEqd57}Tpo8tOXj@LcF zwN8L-s}UO8%6piEtTrj@4bLH!mGpl5mH(UJR1r9bBOrSt0tSJDQ9oIjcW#elyMAxl7W^V(>8M~ss0^>OKvf{&oUG@uW{f^PtV#JDOx^APQKm& z{*Ysrz&ugt4PBUX@KERQbycxP%D+ApR%6jCx7%1RG2YpIa0~tqS6Xw6k#UN$b`^l6d$!I z*>%#Eg=n#VqWnW~MurJLK|hOQPTSy7G@29g@|g;mXC%MF1O7IAS8J^Q6D&Ra!h^+L&(IBYg2WWzZjT-rUsJMFh@E)g)YPW_)W9GF3 zMZz4RK;qcjpnat&J;|MShuPc4qAc)A| zVB?h~3TX+k#Cmry90=kdDoPYbhzs#z96}#M=Q0nC{`s{3ZLU)c(mqQQX;l~1$nf^c zFRQ~}0_!cM2;Pr6q_(>VqoW0;9=ZW)KSgV-c_-XdzEapeLySavTs5-PBsl-n3l;1jD z9^$^xR_QKDUYoeqva|O-+8@+e??(pRg@V|=WtkY!_IwTN~ z9Rd&##eWt_1w$7LL1$-ETciKFyHnNPjd9hHzgJh$J(D@3oYz}}jVNPjH!viX0g|Y9 zDD`Zjd6+o+dbAbUA( zEqA9mSoX5p|9sDVaRBFx_8)Ra4HD#xDB(fa4O8_J2`h#j17tSZOd3%}q8*176Y#ak zC?V8Ol<*X{Q?9j{Ys4Bc#sq!H;^HU$&F_`q2%`^=9DP9YV-A!ZeQ@#p=#ArloIgUH%Y-s>G!%V3aoXaY=f<UBrJTN+*8_lMX$yC=Vq+ zrjLn-pO%+VIvb~>k%`$^aJ1SevcPUo;V{CUqF>>+$c(MXxU12mxqyFAP>ki{5#;Q0 zx7Hh2zZdZzoxPY^YqI*Vgr)ip0xnpQJ+~R*UyFi9RbFd?<_l8GH@}gGmdB)~V7vHg z>Cjy78TQTDwh~+$u$|K3if-^4uY^|JQ+rLVX=u7~bLY29{lr>jWV7QCO5D0I>_1?; zx>*PxE4|wC?#;!#cK|6ivMzJ({k3bT_L3dHY#h7M!ChyTT`P#%3b=k}P(;QYTdrbe z+e{f@we?3$66%02q8p3;^th;9@y2vqt@LRz!DO(WMIk?#Pba85D!n=Ao$5NW0QVgS zoW)fa45>RkjU?H2SZ^#``zs6dG@QWj;MO4k6tIp8ZPminF`rY31dzv^e-3W`ZgN#7 z)N^%Rx?jX&?!5v`hb0-$22Fl&UBV?~cV*{hPG6%ml{k;m+a-D^XOF6DxPd$3;2VVY zT)E%m#ZrF=D=84$l}71DK3Vq^?N4``cdWn3 zqV=mX1(s`eCCj~#Nw4XMGW9tK>$?=cd$ule0Ir8UYzhi?%_u0S?c&j7)-~4LdolkgP^CUeE<2`3m)I^b ztV`K0k$OS^-GK0M0cNTLR22Y_eeT{<;G(+51Xx}b6f!kD&E4; z&Op8;?O<4D$t8PB4#=cWV9Q*i4U+8Bjlj!y4`j)^RNU#<5La6|fa4wLD!b6?RrBsF z@R8Nc^aO8ty7qzlOLRL|RUC-Bt-9>-g`2;@jfNhWAYciF{df9$n#a~28+x~@x0IWM zld=J%YjoKm%6Ea>iF){z#|~fo_w#=&&HRogJmXJDjCp&##oVvMn9iB~gyBlNO3B5f zXgp_1I~^`A0z_~oAa_YBbNZbDsnxLTy0@kkH!=(xt8|{$y<+|(wSZW7@)#|fs_?gU5-o%vpsQPRjIxq;AED^oG%4S%`WR}2(*!84Pe8Jw(snJ zq~#T7+m|w#acH1o%e<+f;!C|*&_!lL*^zRS`;E}AHh%cj1yR&3Grv&0I9k9v0*w8^ zXHEyRyCB`pDBRAxl;ockOh6$|7i$kzCBW$}wGUc|2bo3`x*7>B@eI=-7lKvI)P=gQ zf_GuA+36kQb$&{ZH)6o^x}wS}S^d&Xmftj%nIU=>&j@0?z8V3PLb1JXgHLq)^cTvB zFO6(yj1fl1Bap^}?hh<>j?Jv>RJdK{YpGjHxnY%d8x>A{k+(18J|R}%mAqq9Uzm8^Us#Ir_q^w9-S?W07YRD`w%D(n;|8N%_^RO`zp4 z@`zMAs>*x0keyE)$dJ8hR37_&MsSUMlGC*=7|wUehhKO)C85qoU}j>VVklO^TxK?! zO!RG~y4lv#W=Jr%B#sqc;HjhN={wx761vA3_$S>{j+r?{5=n3le|WLJ(2y_r>{)F_ z=v8Eo&xFR~wkw5v-{+9^JQukxf8*CXDWX*ZzjPVDc>S72uxAcY+(jtg3ns_5R zRYl2pz`B)h+e=|7SfiAAP;A zk0tR)3u1qy0{+?bQOa17SpBRZ5LRHz(TQ@L0%n5xJ21ri>^X420II1?5^FN3&bV?( zCeA)d9!3FAhep;p3?wLPs`>b5Cd}N!;}y`Hq3ppDs0+><{2ey0yq8o7m-4|oaMsWf zsLrG*aMh91drd-_QdX6t&I}t2!`-7$DCR`W2yoV%bcugue)@!SXM}fJOfG(bQQh++ zjAtF~zO#pFz})d8h)1=uhigDuFy`n*sbxZ$BA^Bt=Jdm}_KB6sCvY(T!MQnqO;TJs zVD{*F(FW=+v`6t^6{z<3-fx#|Ze~#h+ymBL^^GKS%Ve<)sP^<4*y_Y${06eD zH_n?Ani5Gs4&1z)UCL-uBvq(8)i!E@T_*0Sp5{Ddlpgke^_$gukJc_f9e=0Rfpta@ ze5~~aJBNK&OJSw!(rDRAHV0d+eW#1?PFbr==uG-$_fu8`!DWqQD~ef-Gx*ZmZx33_ zb0+I(0!hIK>r9_S5A*UwgRBKSd6!ieiYJHRigU@cogJ~FvJHY^DSysg)ac=7#wDBf zNLl!E$AiUMZC%%i5@g$WsN+sMSoUADKZ}-Pb`{7{S>3U%ry~?GVX!BDar2dJHLY|g zTJRo#Bs|u#8ke<3ohL2EFI*n6adobnYG?F3-#7eZZQO{#rmM8*PFycBR^UZKJWr(a z8cex$DPOx_PL^TO<%+f^L6#tdB8S^y#+fb|acQfD(9WgA+cb15L+LUdHKv)wE6={i zX^iY3N#U7QahohDP{g`IHS?D00eJC9DIx0V&nq!1T* z4$Bb?trvEG9JixrrNRKcjX)?KWR#Y(dh#re_<y*=5!J+-Wwb*D>jKXgr5L8_b6pvSAn3RIvI5oj!XF^m?otNA=t^dg z#V=L0@W)n?4Y@}49}YxQS=v5GsIF3%Cp#fFYm0Bm<}ey& zOfWB^vS8ye?n;%yD%NF8DvOpZqlB++#4KnUj>3%*S(c#yACIU>TyBG!GQl7{b8j#V z;lS})mrRtT!IRh2B-*T58%9;!X}W^mg;K&fb7?2#JH>JpCZV5jbDfOgOlc@wNLfHN z8O92GeBRjCP6Q9^Euw-*i&Wu=$>$;8Cktx52b{&Y^Ise-R1gTKRB9m0*Gze>$k?$N zua_0Hmbcj8qQy{ZyJ%`6v6F+yBGm>chZxCGpeL@os+v&5LON7;$tb~MQAbSZKG$k z8w`Mzn=cX4Hf~09q8_|3C7KnoM1^ZGU}#=vn1?1^Kc-eWv4x^T<|i9bCu;+lTQKr- zRwbRK!&XrWRoO7Kw!$zNQb#cJ1`iugR(f_vgmu!O)6tFH-0fOSBk6$^y+R07&&B!(V#ZV)CX42( zTC(jF&b@xu40fyb1=_2;Q|uPso&Gv9OSM1HR{iGPi@JUvmYM;rkv#JiJZ5-EFA%Lu zf;wAmbyclUM*D7>^nPatbGr%2aR5j55qSR$hR`c?d+z z`qko8Yn%vg)p=H`1o?=b9K0%Blx62gSy)q*8jWPyFmtA2a+E??&P~mT@cBdCsvFw4 zg{xaEyVZ|laq!sqN}mWq^*89$e6%sb6Thof;ml_G#Q6_0-zwf80?O}D0;La25A0C+ z3)w-xesp6?LlzF4V%yA9Ryl_Kq*wMk4eu&)Tqe#tmQJtwq`gI^7FXpToum5HP3@;N zpe4Y!wv5uMHUu`zbdtLys5)(l^C(hFKJ(T)z*PC>7f6ZRR1C#ao;R&_8&&a3)JLh* zOFKz5#F)hJqVAvcR#1)*AWPGmlEKw$sQd)YWdAs_W-ojA?Lm#wCd}uF0^X=?AA#ki zWG6oDQZJ5Tvifdz4xKWfK&_s`V*bM7SVc^=w7-m}jW6U1lQEv_JsW6W(| zkKf>qn^G!EWn~|7{G-&t0C6C%4)N{WRK_PM>4sW8^dDkFM|p&*aBuN%fg(I z^M-49vnMd%=04N95VO+?d#el>LEo^tvnQsMop70lNqq@%cTlht?e+B5L1L9R4R(_6 z!3dCLeGXb+_LiACNiqa^nOELJj%q&F^S+XbmdP}`KAep%TDop{Pz;UDc#P&LtMPgH zy+)P1jdgZQUuwLhV<89V{3*=Iu?u#v;v)LtxoOwV(}0UD@$NCzd=id{UuDdedeEp| z`%Q|Y<6T?kI)P|8c!K0Za&jxPhMSS!T`wlQNlkE(2B*>m{D#`hYYD>cgvsKrlcOcs7;SnVCeBiK6Wfho@*Ym9 zr0zNfrr}0%aOkHd)d%V^OFMI~MJp+Vg-^1HPru3Wvac@-QjLX9Dx}FL(l>Z;CkSvC zOR1MK%T1Edv2(b9$ttz!E7{x4{+uSVGz`uH&)gG`$)Vv0^E#b&JSZp#V)b6~$RWwe zzC3FzI`&`EDK@aKfeqQ4M(IEzDd~DS>GB$~ip2n!S%6sR&7QQ*=Mr(v*v-&07CO%# zMBTaD8-EgW#C6qFPPG1Ph^|0AFs;I+s|+A@WU}%@WbPI$S0+qFR^$gim+Fejs2f!$ z@Xdlb_K1BI;iiOUj`j+gOD%mjq^S~J0cZZwuqfzNH9}|(vvI6VO+9ZDA_(=EAo;( zKKzm`k!s!_sYCGOm)93Skaz+GF7eY@Ra8J$C)`X)`aPKym?7D^SI}Mnef4C@SgIEB z>nONSFl$qd;0gSZhNcRlq9VVHPkbakHlZ1gJ1y9W+@!V$TLpdsbKR-VwZrsSM^wLr zL9ob&JG)QDTaf&R^cnm5T5#*J3(pSpjM5~S1 z@V#E2syvK6wb?&h?{E)CoI~9uA(hST7hx4_6M(7!|BW3TR_9Q zLS{+uPoNgw(aK^?=1rFcDO?xPEk5Sm=|pW%-G2O>YWS^(RT)5EQ2GSl75`b}vRcD2 z|HX(x0#Qv+07*O|vMIV(0?KGjOny#Wa~C8Q(kF^IR8u|hyyfwD&>4lW=)Pa311caC zUk3aLCkAFkcidp@C%vNVLNUa#1ZnA~ZCLrLNp1b8(ndgB(0zy{Mw2M@QXXC{hTxr7 zbipeHI-U$#Kr>H4}+cu$#2fG6DgyWgq{O#8aa)4PoJ^;1z7b6t&zt zPei^>F1%8pcB#1`z`?f0EAe8A2C|}TRhzs*-vN^jf(XNoPN!tONWG=abD^=Lm9D?4 zbq4b(in{eZehKC0lF}`*7CTzAvu(K!eAwDNC#MlL2~&gyFKkhMIF=32gMFLvKsbLY z1d$)VSzc^K&!k#2Q?(f>pXn){C+g?vhQ0ijV^Z}p5#BGrGb%6n>IH-)SA$O)*z3lJ z1rtFlovL`cC*RaVG!p!4qMB+-f5j^1)ALf4Z;2X&ul&L!?`9Vdp@d(%(>O=7ZBV;l z?bbmyPen>!P{TJhSYPmLs759b1Ni1`d$0?&>OhxxqaU|}-?Z2c+}jgZ&vCSaCivx| z-&1gw2Lr<;U-_xzlg}Fa_3NE?o}R-ZRX->__}L$%2ySyiPegbnM{UuADqwDR{C2oS zPuo88%DNfl4xBogn((9j{;*YGE0>2YoL?LrH=o^SaAcgO39Ew|vZ0tyOXb509#6{7 z0<}CptRX5(Z4*}8CqCgpT@HY3Q)CvRz_YE;nf6ZFwEje^;Hkj0b1ESI*8Z@(RQrW4 z35D5;S73>-W$S@|+M~A(vYvX(yvLN(35THo!yT=vw@d(=q8m+sJyZMB7T&>QJ=jkwQVQ07*Am^T980rldC)j}}zf!gq7_z4dZ zHwHB94%D-EB<-^W@9;u|(=X33c(G>q;Tfq1F~-Lltp|+uwVzg?e$M96ndY{Lcou%w zWRkjeE`G*i)Bm*|_7bi+=MPm8by_};`=pG!DSGBP6y}zvV^+#BYx{<>p0DO{j@)(S zxcE`o+gZf8EPv1g3E1c3LIbw+`rO3N+Auz}vn~)cCm^DlEi#|Az$b z2}Pqf#=rxd!W*6HijC|u-4b~jtuQS>7uu{>wm)PY6^S5eo=?M>;tK`=DKXuArZvaU zHk(G??qjKYS9G6Du)#fn+ob=}C1Hj9d?V$_=J41ljM$CaA^xh^XrV-jzi7TR-{{9V zZZI0;aQ9YNEc`q=Xvz;@q$eqL<}+L(>HR$JA4mB6~g*YRSnpo zTofY;u7F~{1Pl=pdsDQx8Gg#|@BdoWo~J~j%DfVlT~JaC)he>he6`C`&@@#?;e(9( zgKcmoidHU$;pi{;VXyE~4>0{kJ>K3Uy6`s*1S--*mM&NY)*eOyy!7?9&osK*AQ~vi z{4qIQs)s#eN6j&0S()cD&aCtV;r>ykvAzd4O-fG^4Bmx2A2U7-kZR5{Qp-R^i4H2yfwC7?9(r3=?oH(~JR4=QMls>auMv*>^^!$}{}R z;#(gP+O;kn4G|totqZGdB~`9yzShMze{+$$?9%LJi>4YIsaPMwiJ{`gocu0U}$Q$vI5oeyKrgzz>!gI+XFt!#n z7vs9Pn`{{5w-@}FJZn?!%EQV!PdA3hw%Xa2#-;X4*B4?`WM;4@bj`R-yoAs_t4!!` zEaY5OrYi`3u3rXdY$2jZdZvufgFwVna?!>#t#DKAD2;U zqpqktqJ)8EPY*w~yj7r~#bNk|PDM>ZS?5F7T5aPFVZrqeX~5_1*zTQ%;xUHe#li?s zJ*5XZVERVfRjwX^s=0<%nXhULK+MdibMjzt%J7#fuh?NXyJ^pqpfG$PFmG!h*opyi zmMONjJY#%dkdRHm$l!DLeBm#_0YCq|x17c1fYJ#5YMpsjrFKyU=y>g5QcTgbDm28X zYL1RK)sn1@XtkGR;tNb}(kg#9L=jNSbJizqAgV-TtK2#?LZXrCIz({ zO^R|`ZDu(d@E7vE}df5`a zNIQRp&mDFbgyDKtyl@J|GcR9!h+_a$za$fnO5Ai9{)d7m@?@qk(RjHwXD}JbKRn|u z=Hy^z2vZ<1Mf{5ihhi9Y9GEG74Wvka;%G61WB*y7;&L>k99;IEH;d8-IR6KV{~(LZ zN7@V~f)+yg7&K~uLvG9MAY+{o+|JX?yf7h9FT%7ZrW7!RekjwgAA4jU$U#>_!ZC|c zA9%tc9nq|>2N1rg9uw-Qc89V}I5Y`vuJ(y`Ibc_?D>lPF0>d_mB@~pU`~)uWP48cT@fTxkWSw{aR!`K{v)v zpN?vQZZNPgs3ki9h{An4&Cap-c5sJ!LVLtRd=GOZ^bUpyDZHm6T|t#218}ZA zx*=~9PO>5IGaBD^XX-_2t7?7@WN7VfI^^#Csdz9&{1r z9y<9R?BT~-V8+W3kzWWQ^)ZSI+R zt^Lg`iN$Z~a27)sC_03jrD-%@{ArCPY#Pc*u|j7rE%}jF$LvO4vyvAw3bdL_mg&ei zXys_i=Q!UoF^Xp6^2h5o&%cQ@@)$J4l`AG09G6Uj<~A~!xG>KjKSyTX)zH*EdHMK0 zo;AV-D+bqWhtD-!^+`$*P0B`HokilLd1EuuwhJ?%3wJ~VXIjIE3tj653PExvIVhE& zFMYsI(OX-Q&W$}9gad^PUGuKElCvXxU_s*kx%dH)Bi&$*Q(+9j>(Q>7K1A#|8 zY!G!p0kW29rP*BNHe_wH49bF{K7tymi}Q!Vc_Ox2XjwtpM2SYo7n>?_sB=$c8O5^? z6as!fE9B48FcE`(ruNXP%rAZlDXrFTC7^aoXEX41k)tIq)6kJ*(sr$xVqsh_m3^?? zOR#{GJIr6E0Sz{-( z-R?4asj|!GVl0SEagNH-t|{s06Q3eG{kZOoPHL&Hs0gUkPc&SMY=&{C0&HDI)EHx9 zm#ySWluxwp+b~+K#VG%21%F65tyrt9RTPR$eG0afer6D`M zTW=y!@y6yi#I5V#!I|8IqU=@IfZo!@9*P+f{yLxGu$1MZ%xRY(gRQ2qH@9eMK0`Z> zgO`4DHfFEN8@m@dxYuljsmVv}c4SID+8{kr>d_dLzF$g>urGy9g+=`xAfTkVtz56G zrKNsP$yrDyP=kIqPN9~rVmC-wH672NF7xU>~j5M06Xr&>UJBmOV z%7Ie2d=K=u^D`~i3(U7x?n=h!SCSD1`aFe-sY<*oh+=;B>UVFBOHsF=(Xr(Cai{dL z4S7Y>PHdfG9Iav5FtKzx&UCgg)|DRLvq7!0*9VD`e6``Pgc z1O!qSaNeBBZnDXClh(Dq@XAk?Bd6+_rsFt`5(E+V2c)!Mx4X z47X+QCB4B7$B=Fw1Z1vnHg;x9oDV1YQJAR6Q3}_}BXTFg$A$E!oGG%`Rc()-Ysc%w za(yEn0fw~AaEFr}Rxi;if?Gv)&g~21UzXU9osI9{rNfH$gPTTk#^B|irEc<8W+|9$ zc~R${X2)N!npz1DFVa%nEW)cgPq`MSs)_I*Xwo<+ZK-2^hD(Mc8rF1+2v7&qV;5SET-ygMLNFsb~#u+LpD$uLR1o!ha67gPV5Q{v#PZK5X zUT4aZ{o}&*q7rs)v%*fDTl%}VFX?Oi{i+oKVUBqbi8w#FI%_5;6`?(yc&(Fed4Quy8xsswG+o&R zO1#lUiA%!}61s3jR7;+iO$;1YN;_*yUnJK=$PT_}Q%&0T@2i$ zwGC@ZE^A62YeOS9DU9me5#`(wv24fK=C)N$>!!6V#6rX3xiHehfdvwWJ>_fwz9l)o`Vw9yi z0p5BgvIM5o_ zgo-xaAkS_mya8FXo1Ke4;U*7TGSfm0!fb4{E5Ar8T3p!Z@4;FYT8m=d`C@4-LM121 z?6W@9d@52vxUT-6K_;1!SE%FZHcm0U$SsC%QB zxkTrfH;#Y7OYPy!nt|k^Lgz}uYudos9wI^8x>Y{fTzv9gfTVXN2xH`;Er=rTeAO1x znaaJOR-I)qwD4z%&dDjY)@s`LLSd#FoD!?NY~9#wQRTHpD7Vyyq?tKUHKv6^VE93U zt_&ePH+LM-+9w-_9rvc|>B!oT>_L59nipM-@ITy|x=P%Ezu@Y?N!?jpwP%lm;0V5p z?-$)m84(|7vxV<6f%rK3!(R7>^!EuvA&j@jdTI+5S1E{(a*wvsV}_)HDR&8iuc#>+ zMr^2z*@GTnfDW-QS38OJPR3h6U&mA;vA6Pr)MoT7%NvA`%a&JPi|K8NP$b1QY#WdMt8-CDA zyL0UXNpZ?x=tj~LeM0wk<0Dlvn$rtjd$36`+mlf6;Q}K2{%?%EQ+#FJy6v5cS+Q-~ ztk||Iwr$(CZQHi38QZF;lFFBNt+mg2*V_AhzkM<8#>E_S^xj8%T5tXTytD6f)vePG z^B0Ne-*6Pqg+rVW?%FGHLhl^ycQM-dhNCr)tGC|XyES*NK%*4AnZ!V+Zu?x zV2a82fs8?o?X} zjC1`&uo1Ti*gaP@E43NageV^$Xue3%es2pOrLdgznZ!_a{*`tfA+vnUv;^Ebi3cc$?-kh76PqA zMpL!y(V=4BGPQSU)78q~N}_@xY5S>BavY3Sez-+%b*m0v*tOz6zub9%*~%-B)lb}t zy1UgzupFgf?XyMa+j}Yu>102tP$^S9f7;b7N&8?_lYG$okIC`h2QCT_)HxG1V4Uv{xdA4k3-FVY)d}`cmkePsLScG&~@wE?ix2<(G7h zQ7&jBQ}Kx9mm<0frw#BDYR7_HvY7En#z?&*FurzdDNdfF znCL1U3#iO`BnfPyM@>;#m2Lw9cGn;(5*QN9$zd4P68ji$X?^=qHraP~Nk@JX6}S>2 zhJz4MVTib`OlEAqt!UYobU0-0r*`=03)&q7ubQXrt|t?^U^Z#MEZV?VEin3Nv1~?U zuwwSeR10BrNZ@*h7M)aTxG`D(By$(ZP#UmBGf}duX zhx;7y1x@j2t5sS#QjbEPIj95hV8*7uF6c}~NBl5|hgbB(}M3vnt zu_^>@s*Bd>w;{6v53iF5q7Em>8n&m&MXL#ilSzuC6HTzzi-V#lWoX zBOSBYm|ti@bXb9HZ~}=dlV+F?nYo3?YaV2=N@AI5T5LWWZzwvnFa%w%C<$wBkc@&3 zyUE^8xu<=k!KX<}XJYo8L5NLySP)cF392GK97(ylPS+&b}$M$Y+1VDrJa`GG7+%ToAsh z5NEB9oVv>as?i7f^o>0XCd%2wIaNRyejlFws`bXG$Mhmb6S&shdZKo;p&~b4wv$ z?2ZoM$la+_?cynm&~jEi6bnD;zSx<0BuCSDHGSssT7Qctf`0U!GDwG=+^|-a5%8Ty z&Q!%m%geLjBT*#}t zv1wDzuC)_WK1E|H?NZ&-xr5OX(ukXMYM~_2c;K}219agkgBte_#f+b9Al8XjL-p}1 z8deBZFjplH85+Fa5Q$MbL>AfKPxj?6Bib2pevGxIGAG=vr;IuuC%sq9x{g4L$?Bw+ zvoo`E)3#bpJ{Ij>Yn0I>R&&5B$&M|r&zxh+q>*QPaxi2{lp?omkCo~7ibow#@{0P> z&XBocU8KAP3hNPKEMksQ^90zB1&&b1Me>?maT}4xv7QHA@Nbvt-iWy7+yPFa9G0DP zP82ooqy_ku{UPv$YF0kFrrx3L=FI|AjG7*(paRLM0k1J>3oPxU0Zd+4&vIMW>h4O5G zej2N$(e|2Re z@8xQ|uUvbA8QVXGjZ{Uiolxb7c7C^nW`P(m*Jkqn)qdI0xTa#fcK7SLp)<86(c`A3 zFNB4y#NHe$wYc7V)|=uiW8gS{1WMaJhDj4xYhld;zJip&uJ{Jg3R`n+jywDc*=>bW zEqw(_+j%8LMRrH~+M*$V$xn9x9P&zt^evq$P`aSf-51`ZOKm(35OEUMlO^$>%@b?a z>qXny!8eV7cI)cb0lu+dwzGH(Drx1-g+uDX;Oy$cs+gz~?LWif;#!+IvPR6fa&@Gj zwz!Vw9@-Jm1QtYT?I@JQf%`=$^I%0NK9CJ75gA}ff@?I*xUD7!x*qcyTX5X+pS zAVy4{51-dHKs*OroaTy;U?zpFS;bKV7wb}8v+Q#z<^$%NXN(_hG}*9E_DhrRd7Jqp zr}2jKH{avzrpXj?cW{17{kgKql+R(Ew55YiKK7=8nkzp7Sx<956tRa(|yvHlW zNO7|;GvR(1q}GrTY@uC&ow0me|8wE(PzOd}Y=T+Ih8@c2&~6(nzQrK??I7DbOguA9GUoz3ASU%BFCc8LBsslu|nl>q8Ag(jA9vkQ`q2amJ5FfA7GoCdsLW znuok(diRhuN+)A&`rH{$(HXWyG2TLXhVDo4xu?}k2cH7QsoS>sPV)ylb45Zt&_+1& zT)Yzh#FHRZ-z_Q^8~IZ+G~+qSw-D<{0NZ5!J1%rAc`B23T98TMh9ylkzdk^O?W`@C??Z5U9#vi0d<(`?9fQvNN^ji;&r}geU zSbKR5Mv$&u8d|iB^qiLaZQ#@)%kx1N;Og8Js>HQD3W4~pI(l>KiHpAv&-Ev45z(vYK<>p6 z6#pU(@rUu{i9UngMhU&FI5yeRub4#u=9H+N>L@t}djC(Schr;gc90n%)qH{$l0L4T z;=R%r>CuxH!O@+eBR`rBLrT0vnP^sJ^+qE^C8ZY0-@te3SjnJ)d(~HcnQw@`|qAp|Trrs^E*n zY1!(LgVJfL?@N+u{*!Q97N{Uu)ZvaN>hsM~J?*Qvqv;sLnXHjKrtG&x)7tk?8%AHI zo5eI#`qV1{HmUf-Fucg1xn?Kw;(!%pdQ)ai43J3NP4{%x1D zI0#GZh8tjRy+2{m$HyI(iEwK30a4I36cSht3MM85UqccyUq6$j5K>|w$O3>`Ds;`0736+M@q(9$(`C6QZQ-vAKjIXKR(NAH88 zwfM6_nGWlhpy!_o56^BU``%TQ%tD4hs2^<2pLypjAZ;W9xAQRfF_;T9W-uidv{`B z{)0udL1~tMg}a!hzVM0a_$RbuQk|EG&(z*{nZXD3hf;BJe4YxX8pKX7VaIjjDP%sk zU5iOkhzZ&%?A@YfaJ8l&H;it@;u>AIB`TkglVuy>h;vjtq~o`5NfvR!ZfL8qS#LL` zD!nYHGzZ|}BcCf8s>b=5nZRYV{)KK#7$I06s<;RyYC3<~`mob_t2IfR*dkFJyL?FU zvuo-EE4U(-le)zdgtW#AVA~zjx*^80kd3A#?vI63pLnW2{j*=#UG}ISD>=ZGA$H&` z?Nd8&11*4`%MQlM64wfK`{O*ad5}vk4{Gy}F98xIAsmjp*9P=a^yBHBjF2*Iibo2H zGJAMFDjZcVd%6bZ`dz;I@F55VCn{~RKUqD#V_d{gc|Z|`RstPw$>Wu+;SY%yf1rI=>51Oolm>cnjOWHm?ydcgGs_kPUu=?ZKtQS> zKtLS-v$OMWXO>B%Z4LFUgw4MqA?60o{}-^6tf(c0{Y3|yF##+)RoXYVY-lyPhgn{1 z>}yF0Ab}D#1*746QAj5c%66>7CCWs8O7_d&=Ktu!SK(m}StvvBT1$8QP3O2a*^BNA z)HPhmIi*((2`?w}IE6Fo-SwzI_F~OC7OR}guyY!bOQfpNRg3iMvsFPYb9-;dT6T%R zhLwIjgiE^-9_4F3eMHZ3LI%bbOmWVe{SONpujQ;3C+58=Be4@yJK>3&@O>YaSdrevAdCLMe_tL zl8@F}{Oc!aXO5!t!|`I zdC`k$5z9Yf%RYJp2|k*DK1W@AN23W%SD0EdUV^6~6bPp_HZi0@dku_^N--oZv}wZA zH?Bf`knx%oKB36^L;P%|pf#}Tp(icw=0(2N4aL_Ea=9DMtF})2ay68V{*KfE{O=xL zf}tcfCL|D$6g&_R;r~1m{+)sutQPKzVv6Zw(%8w&4aeiy(qct1x38kiqgk!0^^X3IzI2ia zxI|Q)qJNEf{=I$RnS0`SGMVg~>kHQB@~&iT7+eR!Ilo1ZrDc3TVW)CvFFjHK4K}Kh z)dxbw7X%-9Ol&Y4NQE~bX6z+BGOEIIfJ~KfD}f4spk(m62#u%k<+iD^`AqIhWxtKGIm)l$7=L`=VU0Bz3-cLvy&xdHDe-_d3%*C|Q&&_-n;B`87X zDBt3O?Wo-Hg6*i?f`G}5zvM?OzQjkB8uJhzj3N;TM5dSM$C@~gGU7nt-XX_W(p0IA6$~^cP*IAnA<=@HVqNz=Dp#Rcj9_6*8o|*^YseK_4d&mBY*Y&q z8gtl;(5%~3Ehpz)bLX%)7|h4tAwx}1+8CBtu9f5%^SE<&4%~9EVn4*_!r}+{^2;} zwz}#@Iw?&|8F2LdXUIjh@kg3QH69tqxR_FzA;zVpY=E zcHnWh(3j3UXeD=4m_@)Ea4m#r?axC&X%#wC8FpJPDYR~@65T?pXuWdPzEqXP>|L`S zKYFF0I~%I>SFWF|&sDsRdXf$-TVGSoWTx7>7mtCVUrQNVjZ#;Krobgh76tiP*0(5A zs#<7EJ#J`Xhp*IXB+p5{b&X3GXi#b*u~peAD9vr0*Vd&mvMY^zxTD=e(`}ybDt=BC(4q)CIdp>aK z0c?i@vFWjcbK>oH&V_1m_EuZ;KjZSiW^i30U` zGLK{%1o9TGm8@gy+Rl=-5&z`~Un@l*2ne3e9B+>wKyxuoUa1qhf?-Pi= zZLCD-b7*(ybv6uh4b`s&Ol3hX2ZE<}N@iC+h&{J5U|U{u$XK0AJz)!TSX6lrkG?ris;y{s zv`B5Rq(~G58?KlDZ!o9q5t%^E4`+=ku_h@~w**@jHV-+cBW-`H9HS@o?YUUkKJ;AeCMz^f@FgrRi@?NvO3|J zBM^>4Z}}!vzNum!R~o0)rszHG(eeq!#C^wggTgne^2xc9nIanR$pH1*O;V>3&#PNa z7yoo?%T(?m-x_ow+M0Bk!@ow>A=skt&~xK=a(GEGIWo4AW09{U%(;CYLiQIY$bl3M zxC_FGKY%J`&oTS{R8MHVe{vghGEshWi!(EK*DWmoOv|(Ff#(bZ-<~{rc|a%}Q4-;w z{2gca97m~Nj@Nl{d)P`J__#Zgvc@)q_(yfrF2yHs6RU8UXxcU(T257}E#E_A}%2_IW?%O+7v((|iQ{H<|$S7w?;7J;iwD>xbZc$=l*(bzRXc~edIirlU0T&0E_EXfS5%yA zs0y|Sp&i`0zf;VLN=%hmo9!aoLGP<*Z7E8GT}%)cLFs(KHScNBco(uTubbxCOD_%P zD7XlHivrSWLth7jf4QR9`jFNk-7i%v4*4fC*A=;$Dm@Z^OK|rAw>*CI%E z3%14h-)|Q%_$wi9=p!;+cQ*N1(47<49TyB&B*bm_m$rs+*ztWStR~>b zE@V06;x19Y_A85N;R+?e?zMTIqdB1R8>(!4_S!Fh={DGqYvA0e-P~2DaRpCYf4$-Q z*&}6D!N_@s`$W(|!DOv%>R0n;?#(HgaI$KpHYpnbj~I5eeI(u4CS7OJajF%iKz)*V zt@8=9)tD1ML_CrdXQ81bETBeW!IEy7mu4*bnU--kK;KfgZ>oO>f)Sz~UK1AW#ZQ_ic&!ce~@(m2HT@xEh5u%{t}EOn8ET#*U~PfiIh2QgpT z%gJU6!sR2rA94u@xj3%Q`n@d}^iMH#X>&Bax+f4cG7E{g{vlJQ!f9T5wA6T`CgB%6 z-9aRjn$BmH=)}?xWm9bf`Yj-f;%XKRp@&7?L^k?OT_oZXASIqbQ#eztkW=tmRF$~% z6(&9wJuC-BlGrR*(LQKx8}jaE5t`aaz#Xb;(TBK98RJBjiqbZFyRNTOPA;fG$;~e` zsd6SBii3^(1Y`6^#>kJ77xF{PAfDkyevgox`qW`nz1F`&w*DH5Oh1idOTLES>DToi z8Qs4|?%#%>yuQO1#{R!-+2AOFznWo)e3~_D!nhoDgjovB%A8< zt%c^KlBL$cDPu!Cc`NLc_8>f?)!FGV7yudL$bKj!h;eOGkd;P~sr6>r6TlO{Wp1%xep8r1W{`<4am^(U} z+nCDP{Z*I?IGBE&*KjiaR}dpvM{ZFMW%P5Ft)u$FD373r2|cNsz%b0uk1T+mQI@4& zFF*~xDxDRew1Bol-*q>F{Xw8BUO;>|0KXf`lv7IUh%GgeLUzR|_r(TXZTbfXFE0oc zmGMwzNFgkdg><=+3MnncRD^O`m=SxJ6?}NZ8BR)=ag^b4Eiu<_bN&i0wUaCGi60W6 z%iMl&`h8G)y`gfrVw$={cZ)H4KSQO`UV#!@@cDx*hChXJB7zY18EsIo1)tw0k+8u; zg(6qLysbxVbLFbkYqKbEuc3KxTE+%j5&k>zHB8_FuDcOO3}FS|eTxoUh2~|Bh?pD| zsmg(EtMh`@s;`(r!%^xxDt(5wawK+*jLl>_Z3shaB~vdkJ!V3RnShluzmwn7>PHai z3avc`)jZSAvTVC6{2~^CaX49GXMtd|sbi*swkgoyLr=&yp!ASd^mIC^D;a|<=3pSt zM&0u%#%DGzlF4JpMDs~#kU;UCtyW+d3JwNiu`Uc7Yi6%2gfvP_pz8I{Q<#25DjM_D z(>8yI^s@_tG@c=cPoZImW1CO~`>l>rs=i4BFMZT`vq5bMOe!H@8q@sEZX<-kiY&@u3g1YFc zc@)@OF;K-JjI(eLs~hy8qOa9H1zb!3GslI!nH2DhP=p*NLHeh^9WF?4Iakt+b( z-4!;Q-8c|AX>t+5I64EKpDj4l2x*!_REy9L_9F~i{)1?o#Ws{YG#*}lg_zktt#ZlN zmoNsGm7$AXLink`GWtY*TZEH!J9Qv+A1y|@>?&(pb(6XW#ZF*}x*{60%wnt{n8Icp zq-Kb($kh6v_voqvA`8rq!cgyu;GaWZ>C2t6G5wk! zcKTlw=>KX3ldU}a1%XESW71))Z=HW%sMj2znJ;fdN${00DGGO}d+QsTQ=f;BeZ`eC~0-*|gn$9G#`#0YbT(>O(k&!?2jI z&oi9&3n6Vz<4RGR}h*1ggr#&0f%Op(6{h>EEVFNJ0C>I~~SmvqG+{RXDrexBz zw;bR@$Wi`HQ3e*eU@Cr-4Z7g`1R}>3-Qej(#Dmy|CuFc{Pg83Jv(pOMs$t(9vVJQJ zXqn2Ol^MW;DXq!qM$55vZ{JRqg!Q1^Qdn&FIug%O3=PUr~Q`UJuZ zc`_bE6i^Cp_(fka&A)MsPukiMyjG$((zE$!u>wyAe`gf-1Qf}WFfi1Y{^ zdCTTrxqpQE#2BYWEBnTr)u-qGSVRMV7HTC(x zb(0FjYH~nW07F|{@oy)rlK6CCCgyX?cB;19Z(bCP5>lwN0UBF}Ia|L0$oGHl-oSTZ zr;(u7nDjSA03v~XoF@ULya8|dzH<2G=n9A)AIkQKF0mn?!BU(ipengAE}6r`CE!jd z=EcX8exgDZZQ~~fgxR-2yF;l|kAfnjhz|i_o~cYRdhnE~1yZ{s zG!kZJ<-OVnO{s3bOJK<)`O;rk>=^Sj3M76Nqkj<_@Jjw~iOkWUCL+*Z?+_Jvdb!0cUBy=(5W9H-r4I zxAFts>~r)B>KXdQANyaeKvFheZMgoq4EVV0|^NR@>ea* zh%<78{}wsdL|9N1!jCN-)wH4SDhl$MN^f_3&qo?>Bz#?c{ne*P1+1 z!a`(2Bxy`S^(cw^dv{$cT^wEQ5;+MBctgPfM9kIQGFUKI#>ZfW9(8~Ey-8`OR_XoT zflW^mFO?AwFWx9mW2-@LrY~I1{dlX~jBMt!3?5goHeg#o0lKgQ+eZcIheq@A&dD}GY&1c%hsgo?z zH>-hNgF?Jk*F0UOZ*bs+MXO(dLZ|jzKu5xV1v#!RD+jRrHdQ z>>b){U(I@i6~4kZXn$rk?8j(eVKYJ2&k7Uc`u01>B&G@c`P#t#x@>Q$N$1aT514fK zA_H8j)UKen{k^ehe%nbTw}<JV6xN_|| z(bd-%aL}b z3VITE`N~@WlS+cV>C9TU;YfsU3;`+@hJSbG6aGvis{Gs%2K|($)(_VfpHB|DG8Nje+0tCNW%_cu3hk0F)~{-% zW{2xSu@)Xnc`Dc%AOH)+LT97ImFR*WekSnJ3OYIs#ijP4TD`K&7NZKsfZ;76k@VD3py?pSw~~r^VV$Z zuUl9lF4H2(Qga0EP_==vQ@f!FLC+Y74*s`Ogq|^!?RRt&9e9A&?Tdu=8SOva$dqgYU$zkKD3m>I=`nhx-+M;-leZgt z8TeyQFy`jtUg4Ih^JCUcq+g_qs?LXSxF#t+?1Jsr8c1PB#V+f6aOx@;ThTIR4AyF5 z3m$Rq(6R}U2S}~Bn^M0P&Aaux%D@ijl0kCCF48t)+Y`u>g?|ibOAJoQGML@;tn{%3IEMaD(@`{7ByXQ`PmDeK*;W?| zI8%%P8%9)9{9DL-zKbDQ*%@Cl>Q)_M6vCs~5rb(oTD%vH@o?Gk?UoRD=C-M|w~&vb z{n-B9>t0EORXd-VfYC>sNv5vOF_Wo5V)(Oa%<~f|EU7=npanpVX^SxPW;C!hMf#kq z*vGNI-!9&y!|>Zj0V<~)zDu=JqlQu+ii387D-_U>WI_`3pDuHg{%N5yzU zEulPN)%3&{PX|hv*rc&NKe(bJLhH=GPuLk5pSo9J(M9J3v)FxCo65T%9x<)x+&4Rr2#nu2?~Glz|{28OV6 z)H^`XkUL|MG-$XE=M4*fIPmeR2wFWd>5o*)(gG^Y>!P4(f z68RkX0cRBOFc@`W-IA(q@p@m>*2q-`LfujOJ8-h$OgHte;KY4vZKTxO95;wh#2ZDL zKi8aHkz2l54lZd81t`yY$Tq_Q2_JZ1d(65apMg}vqwx=ceNOWjFB)6m3Q!edw2<{O z4J6+Un(E8jxs-L-K_XM_VWahy zE+9fm_ZaxjNi{fI_AqLKqhc4IkqQ4`Ut$=0L)nzlQw^%i?bP~znsbMY3f}*nPWqQZ zz_CQDpZ?Npn_pEr`~SX1`OoSkS;bmzQ69y|W_4bH3&U3F7EBlx+t%2R02VRJ01cfX zo$$^ObDHK%bHQaOcMpCq@@Jp8!OLYVQO+itW1ZxlkmoG#3FmD4b61mZjn4H|pSmYi2YE;I#@jtq8Mhjdgl!6({gUsQA>IRXb#AyWVt7b=(HWGUj;wd!S+q z4S+H|y<$yPrrrTqQHsa}H`#eJFV2H5Dd2FqFMA%mwd`4hMK4722|78d(XV}rz^-GV(k zqsQ>JWy~cg_hbp0=~V3&TnniMQ}t#INg!o2lN#H4_gx8Tn~Gu&*ZF8#kkM*5gvPu^ zw?!M^05{7q&uthxOn?%#%RA_%y~1IWly7&_-sV!D=Kw3DP+W)>YYRiAqw^d7vG_Q%v;tRbE1pOBHc)c&_5=@wo4CJTJ1DeZErEvP5J(kc^GnGYX z|LqQjTkM{^gO2cO#-(g!7^di@$J0ibC(vsnVkHt3osnWL8?-;R1BW40q5Tmu_9L-s z7fNF5fiuS-%B%F$;D97N-I@!~c+J>nv%mzQ5vs?1MgR@XD*Gv`A{s8 z5Cr>z5j?|sb>n=c*xSKHpdy667QZT?$j^Doa%#m4ggM@4t5Oe%iW z@w~j_B>GJJkO+6dVHD#CkbC(=VMN8nDkz%44SK62N(ZM#AsNz1KW~3(i=)O;q5JrK z?vAVuL}Rme)OGQuLn8{3+V352UvEBV^>|-TAAa1l-T)oiYYD&}Kyxw73shz?Bn})7 z_a_CIPYK(zMp(i+tRLjy4dV#CBf3s@bdmwXo`Y)dRq9r9-c@^2S*YoNOmAX%@OYJOXs zT*->in!8Ca_$W8zMBb04@|Y)|>WZ)-QGO&S7Zga1(1#VR&)X+MD{LEPc%EJCXIMtr z1X@}oNU;_(dfQ_|kI-iUSTKiVzcy+zr72kq)TIp(GkgVyd%{8@^)$%G)pA@^Mfj71FG%d?sf(2Vm>k%X^RS`}v0LmwIQ7!_7cy$Q8pT?X1VWecA_W68u==HbrU& z@&L6pM0@8ZHL?k{6+&ewAj%grb6y@0$3oamTvXsjGmPL_$~OpIyIq%b$(uI1VKo zk_@{r>1p84UK3}B>@d?xUZ}dJk>uEd+-QhwFQ`U?rA=jj+$w8sD#{492P}~R#%z%0 z5dlltiAaiPKv9fhjmuy{*m!C22$;>#85EduvdSrFES{QO$bHpa7E@&{bWb@<7VhTF zXCFS_wB>7*MjJ3$_i4^A2XfF2t7`LOr3B@??OOUk=4fKkaHne4RhI~Lm$JrHfUU*h zgD9G66;_F?3>0W{pW2A^DR7Bq`ZUiSc${S8EM>%gFIqAw0du4~kU#vuCb=$I_PQv? zZfEY7X6c{jJZ@nF&T>4oyy(Zr_XqnMq)ZtGPASbr?IhZOnL|JKY()`eo=P5UK9(P-@ zOJKFogtk|pscVD+#$7KZs^K5l4gC}*CTd0neZ8L(^&1*bPrCp23%{VNp`4Ld*)Fly z)b|zb*bCzp?&X3_=qLT&0J+=p01&}9*xbk~^hd^@mV!Ha`1H+M&60QH2c|!Ty`RepK|H|Moc5MquD z=&$Ne3%WX+|7?iiR8=7*LW9O3{O%Z6U6`VekeF8lGr5vd)rsZu@X#5!^G1;nV60cz zW?9%HgD}1G{E(YvcLcIMQR65BP50)a;WI*tjRzL7diqRqh$3>OK{06VyC=pj6OiardshTnYfve5U>Tln@y{DC99f!B4> zCrZa$B;IjDrg}*D5l=CrW|wdzENw{q?oIj!Px^7DnqAsU7_=AzXxoA;4(YvN5^9ag zwEd4-HOlO~R0~zk>!4|_Z&&q}agLD`Nx!%9RLC#7fK=w06e zOK<>|#@|e2zjwZ5aB>DJ%#P>k4s0+xHJs@jROvoDQfSoE84l8{9y%5^POiP+?yq0> z7+Ymbld(s-4p5vykK@g<{X*!DZt1QWXKGmj${`@_R~=a!qPzB357nWW^KmhV!^G3i zsYN{2_@gtzsZH*FY!}}vNDnqq>kc(+7wK}M4V*O!M&GQ|uj>+8!Q8Ja+j3f*MzwcI z^s4FXGC=LZ?il4D+Y^f89wh!d7EU-5dZ}}>_PO}jXRQ@q^CjK-{KVnmFd_f&IDKmx zZ5;PDLF%_O);<4t`WSMN;Ec^;I#wU?Z?_R|Jg`#wbq;UM#50f@7F?b7ySi-$C-N;% zqXowTcT@=|@~*a)dkZ836R=H+m6|fynm#0Y{KVyYU=_*NHO1{=Eo{^L@wWr7 zjz9GOu8Fd&v}a4d+}@J^9=!dJRsCO@=>K6UCM)Xv6};tb)M#{(k!i}_0Rjq z2kb7wPcNgov%%q#(1cLykjrxAg)By+3QueBR>Wsep&rWQHq1wE!JP+L;q+mXts{j@ zOY@t9BFmofApO0k@iBFPeKsV3X=|=_t65QyohXMSfMRr7Jyf8~ogPVmJwbr@`nmml zov*NCf;*mT(5s4K=~xtYy8SzE66W#tW4X#RnN%<8FGCT{z#jRKy@Cy|!yR`7dsJ}R z!eZzPCF+^b0qwg(mE=M#V;Ud9)2QL~ z-r-2%0dbya)%ui_>e6>O3-}4+Q!D+MU-9HL2tH)O`cMC1^=rA=q$Pcc;Zel@@ss|K zH*WMdS^O`5Uv1qNTMhM(=;qjhaJ|ZC41i2!kt4;JGlXQ$tvvF8Oa^C@(q6(&6B^l) zNG{GaX?`qROHwL-F1WZDEF;C6Inuv~1&ZuP3j53547P38tr|iPH#3&hN*g0R^H;#) znft`cw0+^Lwe{!^kQat+xjf_$SZ05OD6~U`6njelvd+4pLZU(0ykS5&S$)u?gm!;} z+gJ8g12b1D4^2HH!?AHFAjDAP^q)Juw|hZfIv{3Ryn%4B^-rqIF2 zeWk^za4fq#@;re{z4_O|Zj&Zn{2WsyI^1%NW=2qA^iMH>u>@;GAYI>Bk~u0wWQrz* zdEf)7_pSYMg;_9^qrCzvv{FZYwgXK}6e6ceOH+i&+O=x&{7aRI(oz3NHc;UAxMJE2 zDb0QeNpm$TDcshGWs!Zy!shR$lC_Yh-PkQ`{V~z!AvUoRr&BAGS#_*ZygwI2-)6+a zq|?A;+-7f0Dk4uuht z6sWPGl&Q$bev1b6%aheld88yMmBp2j=z*egn1aAWd?zN=yEtRDGRW&nmv#%OQwuJ; zqKZ`L4DsqJwU{&2V9f>2`1QP7U}`6)$qxTNEi`4xn!HzIY?hDnnJZw+mFnVSry=bLH7ar+M(e9h?GiwnOM?9ZJcTJ08)T1-+J#cr&uHhXkiJ~}&(}wvzCo33 zLd_<%rRFQ3d5fzKYQy41<`HKk#$yn$Q+Fx-?{3h72XZrr*uN!5QjRon-qZh9-uZ$rWEKZ z!dJMP`hprNS{pzqO`Qhx`oXGd{4Uy0&RDwJ`hqLw4v5k#MOjvyt}IkLW{nNau8~XM z&XKeoVYreO=$E%z^WMd>J%tCdJx5-h+8tiawu2;s& zD7l`HV!v@vcX*qM(}KvZ#%0VBIbd)NClLBu-m2Scx1H`jyLYce;2z;;eo;ckYlU53 z9JcQS+CvCwj*yxM+e*1Vk6}+qIik2VzvUuJyWyO}piM1rEk%IvS;dsXOIR!#9S;G@ zPcz^%QTf9D<2~VA5L@Z@FGQqwyx~Mc-QFzT4Em?7u`OU!PB=MD8jx%J{<`tH$Kcxz zjIvb$x|`s!-^^Zw{hGV>rg&zb;=m?XYAU0LFw+uyp8v@Y)zmjj&Ib7Y1@r4`cfrS%cVxJiw`;*BwIU*6QVsBBL;~nw4`ZFqs z1YSgLVy=rvA&GQB4MDG+j^)X1N=T;Ty2lE-`zrg(dNq?=Q`nCM*o8~A2V~UPArX<| zF;e$5B0hPSo56=ePVy{nah#?e-Yi3g*z6iYJ#BFJ-5f0KlQ-PRiuGwe29fyk1T6>& zeo2lvb%h9Vzi&^QcVNp}J!x&ubtw5fKa|n2XSMlg#=G*6F|;p)%SpN~l8BaMREDQN z-c9O}?%U1p-ej%hzIDB!W_{`9lS}_U==fdYpAil1E3MQOFW^u#B)Cs zTE3|YB0bKpXuDKR9z&{4gNO3VHDLB!xxPES+)yaJxo<|}&bl`F21};xsQnc!*FPZA zSct2IU3gEu@WQKmY-vA5>MV?7W|{$rAEj4<8`*i)<%fj*gDz2=ApqZ&MP&0UmO1?q!GN=di+n(#bB_mHa z(H-rIOJqamMfwB%?di!TrN=x~0jOJtvb0e9uu$ZCVj(gJyK}Fa5F2S?VE30P{#n3eMy!-v7e8viCooW9cfQx%xyPNL*eDKL zB=X@jxulpkLfnar7D2EeP*0L7c9urDz{XdV;@tO;u`7DlN7#~ zAKA~uM2u8_<5FLkd}OzD9K zO5&hbK8yakUXn8r*H9RE zO9Gsipa2()=&x=1mnQtNP#4m%GXThu8Ccqx*qb;S{5}>bU*V5{SY~(Hb={cyTeaTM zMEaKedtJf^NnJrwQ^Bd57vSlJ3l@$^0QpX@_1>h^+js8QVpwOiIMOiSC_>3@dt*&| zV?0jRdlgn|FIYam0s)a@5?0kf7A|GD|dRnP1=B!{ldr;N5s)}MJ=i4XEqlC}w)LEJ}7f9~c!?It(s zu>b=YBlFRi(H-%8A!@Vr{mndRJ z_jx*?BQpK>qh`2+3cBJhx;>yXPjv>dQ0m+nd4nl(L;GmF-?XzlMK zP(Xeyh7mFlP#=J%i~L{o)*sG7H5g~bnL2Hn3y!!r5YiYRzgNTvgL<(*g5IB*gcajK z86X3LoW*5heFmkIQ-I_@I_7b!Xq#O;IzOv(TK#(4gd)rmCbv5YfA4koRfLydaIXUU z8(q?)EWy!sjsn-oyUC&uwJqEXdlM}#tmD~*Ztav=mTQyrw0^F=1I5lj*}GSQTQOW{ z=O12;?fJfXxy`)ItiDB@0sk43AZo_sRn*jc#S|(2*%tH84d|UTYN!O4R(G6-CM}84 zpiyYJ^wl|w@!*t)dwn0XJv2kuHgbfNL$U6)O-k*~7pQ?y=sQJdKk5x`1>PEAxjIWn z{H$)fZH4S}%?xzAy1om0^`Q$^?QEL}*ZVQK)NLgmnJ`(we z21c23X1&=^>k;UF-}7}@nzUf5HSLUcOYW&gsqUrj7%d$)+d8ZWwTZq)tOgc%fz95+ zl%sdl)|l|jXfqIcjKTFrX74Rbq1}osA~fXPSPE?XO=__@`7k4Taa!sHE8v-zfx(AM zXT_(7u;&_?4ZIh%45x>p!(I&xV|IE**qbqCRGD5aqLpCRvrNy@uT?iYo-FPpu`t}J zSTZ}MDrud+`#^14r`A%UoMvN;raizytxMBV$~~y3i0#m}0F}Dj_fBIz+)1RWdnctP z>^O^vd0E+jS+$V~*`mZWER~L^q?i-6RPxxufWdrW=%prbCYT{5>Vgu%vPB)~NN*2L zB?xQg2K@+Xy=sPh$%10LH!39p&SJG+3^i*lFLn=uY8Io6AXRZf;p~v@1(hWsFzeKzx99_{w>r;cypkPVJCKtLGK>?-K0GE zGH>$g?u`)U_%0|f#!;+E>?v>qghuBwYZxZ*Q*EE|P|__G+OzC-Z+}CS(XK^t!TMoT zc+QU|1C_PGiVp&_^wMxfmMAuJDQ%1p4O|x5DljN6+MJiO%8s{^ts8$uh5`N~qK46c`3WY#hRH$QI@*i1OB7qBIN*S2gK#uVd{ zik+wwQ{D)g{XTGjKV1m#kYhmK#?uy)g@idi&^8mX)Ms`^=hQGY)j|LuFr8SJGZjr| zzZf{hxYg)-I^G|*#dT9Jj)+wMfz-l7ixjmwHK9L4aPdXyD-QCW!2|Jn(<3$pq-BM; zs(6}egHAL?8l?f}2FJSkP`N%hdAeBiD{3qVlghzJe5s9ZUMd`;KURm_eFaK?d&+TyC88v zCv2R(Qg~0VS?+p+l1e(aVq`($>|0b{{tPNbi} zaZDffTZ7N|t2D5DBv~aX#X+yGagWs1JRsqbr4L8a`B`m) z1p9?T`|*8ZXHS7YD8{P1Dk`EGM`2Yjsy0=7M&U6^VO30`Gx!ZkUoqmc3oUbd&)V*iD08>dk=#G!*cs~^tOw^s8YQqYJ z!5=-4ZB7rW4mQF&YZw>T_in-c9`0NqQ_5Q}fq|)%HECgBd5KIo`miEcJ>~a1e2B@) zL_rqoQ;1MowD34e6#_U+>D`WcnG5<2Q6cnt4Iv@NC$*M+i3!c?6hqPJLsB|SJ~xo! zm>!N;b0E{RX{d*in3&0w!cmB&TBNEjhxdg!fo+}iGE*BWV%x*46rT@+cXU;leofWy zxst{S8m!_#hIhbV7wfWN#th8OI5EUr3IR_GOIzBgGW1u4J*TQxtT7PXp#U#EagTV* zehVkBFF06`@5bh!t%L)-)`p|d7D|^kED7fsht#SN7*3`MKZX};Jh0~nCREL_BGqNR zxpJ4`V{%>CAqEE#Dt95u=;Un8wLhrac$fao`XlNsOH%&Ey2tK&vAcriS1kXnntDuttcN{%YJz@!$T zD&v6ZQ>zS1`o!qT=JK-Y+^i~bZkVJpN8%<4>HbuG($h9LP;{3DJF_Jcl8CA5M~<3s^!$Sg62zLEnJtZ z0`)jwK75Il6)9XLf(64~`778D6-#Ie1IR2Ffu+_Oty%$8u+bP$?803V5W6%(+iZzp zp5<&sBV&%CJcXUIATUakP1czt$&0x$lyoLH!ueNaIpvtO z*eCijxOv^-D?JaLzH<3yhOfDENi@q#4w(#tl-19(&Yc2K%S8Y&r{3~-)P17sC1{rQ zOy>IZ6%814_UoEi+w9a4XyGXF66{rgE~UT)oT4x zg9oIx@|{KL#VpTyE=6WK@Sbd9RKEEY)5W{-%0F^6(QMuT$RQRZ&yqfyF*Z$f8>{iT zq(;UzB-Ltv;VHvh4y%YvG^UEkvpe9ugiT97ErbY0ErCEOWs4J=kflA!*Q}gMbEP`N zY#L`x9a?E)*~B~t+7c8eR}VY`t}J;EWuJ-6&}SHnNZ8i0PZT^ahA@@HXk?c0{)6rC zP}I}_KK7MjXqn1E19gOwWvJ3i9>FNxN67o?lZy4H?n}%j|Dq$p%TFLUPJBD;R|*0O z3pLw^?*$9Ax!xy<&fO@;E2w$9nMez{5JdFO^q)B0OmGwkxxaDsEU+5C#g+?Ln-Vg@ z-=z4O*#*VJa*nujGnGfK#?`a|xfZsuiO+R}7y(d60@!WUIEUt>K+KTI&I z9YQ6#hVCo}0^*>yr-#Lisq6R?uI=Ms!J7}qm@B}Zu zp%f-~1Cf!-5S0xXl`oqq&fS=tt0`%dDWI&6pW(s zJXtYiY&~t>k5I0RK3sN;#8?#xO+*FeK#=C^%{Y>{k{~bXz%(H;)V5)DZRk~(_d0b6 zV!x54fwkl`1y;%U;n|E#^Vx(RGnuN|T$oJ^R%ZmI{8(9>U-K^QpDcT?Bb@|J0NAfvHtL#wP ziYupr2E5=_KS{U@;kyW7oy*+UTOiF*e+EhYqVcV^wx~5}49tBNSUHLH1=x}6L2Fl^4X4633$k!ZHZTL50Vq+a5+ z<}uglXQ<{x&6ey)-lq6;4KLHbR)_;Oo^FodsYSw3M-)FbLaBcPI=-ao+|))T2ksKb z{c%Fu`HR1dqNw8%>e0>HI2E_zNH1$+4RWfk}p-h(W@)7LC zwVnUO17y+~kw35CxVtokT44iF$l8XxYuetp)1Br${@lb(Q^e|q*5%7JNxp5B{r<09 z-~8o#rI1(Qb9FhW-igcsC6npf5j`-v!nCrAcVx5+S&_V2D>MOWp6cV$~Olhp2`F^Td{WV`2k4J`djb#M>5D#k&5XkMu*FiO(uP{SNX@(=)|Wm`@b> z_D<~{ip6@uyd7e3Rn+qM80@}Cl35~^)7XN?D{=B-4@gO4mY%`z!kMIZizhGtCH-*7 z{a%uB4usaUoJwbkVVj%8o!K^>W=(ZzRDA&kISY?`^0YHKe!()(*w@{w7o5lHd3(Us zUm-K=z&rEbOe$ackQ3XH=An;Qyug2g&vqf;zsRBldxA+=vNGoM$Zo9yT?Bn?`Hkiq z&h@Ss--~+=YOe@~JlC`CdSHy zcO`;bgMASYi6`WSw#Z|A;wQgH@>+I3OT6(*JgZZ_XQ!LrBJfVW2RK%#02|@V|H4&8DqslU6Zj(x!tM{h zRawG+Vy63_8gP#G!Eq>qKf(C&!^G$01~baLLk#)ov-Pqx~Du>%LHMv?=WBx2p2eV zbj5fjTBhwo&zeD=l1*o}Zs%SMxEi9yokhbHhY4N!XV?t8}?!?42E-B^Rh&ABFxovs*HeQ5{{*)SrnJ%e{){Z_#JH+jvwF7>Jo zE+qzWrugBwVOZou~oFa(wc7?`wNde>~HcC@>fA^o>ll?~aj-e|Ju z+iJzZg0y1@eQ4}rm`+@hH(|=gW^;>n>ydn!8%B4t7WL)R-D>mMw<7Wz6>ulFnM7QA ze2HEqaE4O6jpVq&ol3O$46r+DW@%glD8Kp*tFY#8oiSyMi#yEpVIw3#t?pXG?+H>v z$pUwT@0ri)_Bt+H(^uzp6qx!P(AdAI_Q?b`>0J?aAKTPt>73uL2(WXws9+T|%U)Jq zP?Oy;y6?{%J>}?ZmfcnyIQHh_jL;oD$`U#!v@Bf{5%^F`UiOX%)<0DqQ^nqA5Ac!< z1DPO5C>W0%m?MN*x(k>lDT4W3;tPi=&yM#Wjwc5IFNiLkQf`7GN+J*MbB4q~HVePM zeDj8YyA*btY&n!M9$tuOxG0)2um))hsVsY+(p~JnDaT7x(s2If0H_iRSju7!z7p|8 zzI`NV!1hHWX3m)?t68k6yNKvop{Z>kl)f5GV(~1InT4%9IxqhDX-rgj)Y|NYq_NTlZgz-)=Y$=x9L7|k0=m@6WQ<4&r=BX@pW25NtCI+N{e&`RGSpR zeb^`@FHm5?pWseZ6V08{R(ki}--13S2op~9Kzz;#cPgL}Tmrqd+gs(fJLTCM8#&|S z^L+7PbAhltJDyyxAVxqf(2h!RGC3$;hX@YNz@&JRw!m5?Q)|-tZ8u0D$4we+QytG^ zj0U_@+N|OJlBHdWPN!K={a$R1Zi{2%5QD}s&s-Xn1tY1cwh)8VW z$pjq>8sj4)?76EJs6bA0E&pfr^Vq`&Xc;Tl2T!fm+MV%!H|i0o;7A=zE?dl)-Iz#P zSY7QRV`qRc6b&rON`BValC01zSLQpVemH5y%FxK8m^PeNN(Hf1(%C}KPfC*L?Nm!nMW0@J3(J=mYq3DPk;TMs%h`-amWbc%7{1Lg3$ z^e=btuqch-lydbtLvazh+fx?87Q7!YRT(=-Vx;hO)?o@f1($e5B?JB9jcRd;zM;iE zu?3EqyK`@_5Smr#^a`C#M>sRwq2^|ym)X*r;0v6AM`Zz1aK94@9Ti)Lixun2N!e-A z>w#}xPxVd9AfaF$XTTff?+#D(xwOpjZj9-&SU%7Z-E2-VF-n#xnPeQH*67J=j>TL# z<v}>AiTXrQ(fYa%82%qlH=L z6Fg8@r4p+BeTZ!5cZlu$iR?EJpYuTx>cJ~{{B7KODY#o*2seq=p2U0Rh;3mX^9sza zk^R_l7jzL5BXWlrVkhh!+LQ-Nc0I`6l1mWkp~inn)HQWqMTWl4G-TBLglR~n&6J?4 z7J)IO{wkrtT!Csntw3H$Mnj>@;QbrxC&Shqn^VVu$Ls*_c~TTY~fri6fO-=eJsC*8(3(H zSyO>=B;G`qA398OvCHRvf3mabrPZaaLhn*+jeA`qI!gP&i8Zs!*bBqMXDJpSZG$N) zx0rDLvcO>EoqCTR)|n7eOp-jmd>`#w`6`;+9+hihW2WnKVPQ20LR94h+(p)R$Y!Q zj_3ZEY+e@NH0f6VjLND)sh+Cvfo3CpcXw?`$@a^@CyLrAKIpjL8G z`;cDLqvK=ER)$q)+6vMKlxn!!SzWl>Ib9Ys9L)L0IWr*Ox;Rk#(Dpqf;wapY_EYL8 zKFrV)Q8BBKO4$r2hON%g=r@lPE;kBUVYVG`uxx~QI>9>MCXw_5vnmDsm|^KRny929 zeKx>F(LDs#K4FGU*k3~GX`A!)l8&|tyan-rBHBm6XaB5hc5sGKWwibAD7&3M-gh1n z2?eI7E2u{(^z#W~wU~dHSfy|m)%PY454NBxED)y-T3AO`CLQxklcC1I@Y`v4~SEI#Cm> z-cjqK6I?mypZapi$ZK;y&G+|#D=woItrajg69VRD+Fu8*UxG6KdfFmFLE}HvBJ~Y) zC&c-hr~;H2Idnsz7_F~MKpBZldh)>itc1AL0>4knbVy#%pUB&9vqL1Kg*^aU`k#(p z=A%lur(|$GWSqILaWZ#2xj(&lheSiA|N6DOG?A|$!aYM)?oME6ngnfLw0CA79WA+y zhUeLbMw*VB?drVE_D~3DWVaD>8x?_q>f!6;)i3@W<=kBZBSE=uIU60SW)qct?AdM zXgti8&O=}QNd|u%Fpxr172Kc`sX^@fm>Fxl8fbFalJYci_GGoIzU*~U*I!QLz? z4NYk^=JXBS*Uph@51da-v;%?))cB^(ps}y8yChu7CzyC9SX{jAq13zdnqRHRvc{ha zcPmgCUqAJ^1RChMCCz;ZN*ap{JPoE<1#8nNObDbAt6Jr}Crq#xGkK@w2mLhIUecvy z#?s~?J()H*?w9K`_;S+8TNVkHSk}#yvn+|~jcB|he}OY(zH|7%EK%-Tq=)18730)v zM3f|=oFugXq3Lqn={L!wx|u(ycZf(Te11c3?^8~aF; zNMC)gi?nQ#S$s{46yImv_7@4_qu|XXEza~);h&cr*~dO@#$LtKZa@@r$8PD^jz{D6 zk~5;IJBuQjsKk+8i0wzLJ2=toMw4@rw7(|6`7*e|V(5-#ZzRirtkXBO1oshQ&0>z&HAtSF8+871e|ni4gLs#`3v7gnG#^F zDv!w100_HwtU}B2T!+v_YDR@-9VmoGW+a76oo4yy)o`MY(a^GcIvXW+4)t{lK}I-& zl-C=(w_1Z}tsSFjFd z3iZjkO6xnjLV3!EE?ex9rb1Zxm)O-CnWPat4vw08!GtcQ3lHD+ySRB*3zQu-at$rj zzBn`S?5h=JlLXX8)~Jp%1~YS6>M8c-Mv~E%s7_RcvIYjc-ia`3r>dvjxZ6=?6=#OM zfsv}?hGnMMdi9C`J9+g)5`M9+S79ug=!xE_XcHdWnIRr&hq$!X7aX5kJV8Q(6Lq?|AE8N2H z37j{DPDY^Jw!J>~>Mwaja$g%q1sYfH4bUJFOR`x=pZQ@O(-4b#5=_Vm(0xe!LW>YF zO4w`2C|Cu%^C9q9B>NjFD{+qt)cY3~(09ma%mp3%cjFsj0_93oVHC3)AsbBPuQNBO z`+zffU~AgGrE0K{NVR}@oxB4&XWt&pJ-mq!JLhFWbnXf~H%uU?6N zWJ7oa@``Vi$pMWM#7N9=sX1%Y+1qTGnr_G&h3YfnkHPKG}p>i{fAG+(klE z(g~u_rJXF48l1D?;;>e}Ra{P$>{o`jR_!s{hV1Wk`vURz`W2c$-#r9GM7jgs2>um~ zouGlCm92rOiLITzf`jgl`v2qYw^!Lh0YwFHO1|3Krp8ztE}?#2+>c)yQlNw%5e6w5 zIm9BKZN5Q9b!tX`Zo$0RD~B)VscWp(FR|!a!{|Q$={;ZWl%10vBzfgWn}WBe!%cug z^G%;J-L4<6&aCKx@@(Grsf}dh8fuGT+TmhhA)_16uB!t{HIAK!B-7fJLe9fsF)4G- zf>(~ⅅ8zCNKueM5c!$)^mKpZNR!eIlFST57ePGQcqCqedAQ3UaUEzpjM--5V4YO zY22VxQm%$2NDnwfK+jkz=i2>NjAM6&P1DdcO<*Xs1-lzdXWn#LGSxwhPH7N%D8-zCgpFWt@`LgNYI+Fh^~nSiQmwH0^>E>*O$47MqfQza@Ce z1wBw;igLc#V2@y-*~Hp?jA1)+MYYyAt|DV_8RQCrRY@sAviO}wv;3gFdO>TE(=9o? z=S(r=0oT`w24=ihA=~iFV5z$ZG74?rmYn#eanx(!Hkxcr$*^KRFJKYYB&l6$WVsJ^ z-Iz#HYmE)Da@&seqG1fXsTER#adA&OrD2-T(z}Cwby|mQf{0v*v3hq~pzF`U`jenT z=XHXeB|fa?Ws$+9ADO0rco{#~+`VM?IXg7N>M0w1fyW1iiKTA@p$y zSiAJ%-Mg{m>&S4r#Tw@?@7ck}#oFo-iZJCWc`hw_J$=rw?omE{^tc59ftd`xq?jzf zo0bFUI=$>O!45{!c4?0KsJmZ#$vuYpZLo_O^oHTmmLMm0J_a{Nn`q5tG1m=0ecv$T z5H7r0DZGl6be@aJ+;26EGw9JENj0oJ5K0=^f-yBW2I0jqVIU};NBp*gF7_KlQnhB6 z##d$H({^HXj@il`*4^kC42&3)(A|tuhs;LygA-EWFSqpe+%#?6HG6}mE215Z4mjO2 zY2^?5$<8&k`O~#~sSc5Fy`5hg5#e{kG>SAbTxCh{y32fHkNryU_c0_6h&$zbWc63T z7|r?X7_H!9XK!HfZ+r?FvBQ$x{HTGS=1VN<>Ss-7M3z|vQG|N}Frv{h-q623@Jz*@ ziXlZIpAuY^RPlu&=nO)pFhML5=ut~&zWDSsn%>mv)!P1|^M!d5AwmSPIckoY|0u9I zTDAzG*U&5SPf+@c_tE_I!~Npfi$?gX(kn=zZd|tUZ_ez(xP+)xS!8=k(<{9@<+EUx zYQgZhjn(0qA#?~Q+EA9oh_Jx5PMfE3#KIh#*cFIFQGi)-40NHbJO&%ZvL|LAqU=Rw zf?Vr4qkUcKtLr^g-6*N-tfk+v8@#Lpl~SgKyH!+m9?T8B>WDWK22;!i5&_N=%f{__ z-LHb`v-LvKqTJZCx~z|Yg;U_f)VZu~q7trb%C6fOKs#eJosw&b$nmwGwP;Bz`=zK4 z>U3;}T_ptP)w=vJaL8EhW;J#SHA;fr13f=r#{o)`dRMOs-T;lp&Toi@u^oB_^pw=P zp#8Geo2?@!h2EYHY?L;ayT}-Df0?TeUCe8Cto{W0_a>!7Gxmi5G-nIIS;X{flm2De z{SjFG%knZoVa;mtHR_`*6)KEf=dvOT3OgT7C7&-4P#4X^B%VI&_57cBbli()(%zZC?Y0b;?5!f22UleQ=9h4_LkcA!Xsqx@q{ko&tvP_V@7epFs}AIpM{g??PA>U(sk$Gum>2Eu zD{Oy{$OF%~?B6>ixQeK9I}!$O0!T3#Ir8MW)j2V*qyJ z8Bg17L`rg^B_#rkny-=<3fr}Y42+x0@q6POk$H^*p3~Dc@5uYTQ$pfaRnIT}Wxb;- zl!@kkZkS=l)&=y|21veY8yz$t-&7ecA)TR|=51BKh(@n|d$EN>18)9kSQ|GqP?aeM ztXd9C&Md$PPF*FVs*GhoHM2L@D$(Qf%%x zwQBUt!jM~GgwluBcwkgwQ!249uPkNz3u@LSYZgmpHgX|P#8!iKk^vSKZ;?)KE$92d z2U>y}VWJ0&zjrIqddM3dz-nU%>bL&KU%SA|LiiUU7Ka|c=jF|vQ1V)Jz`JZe*j<5U6~RVuBEVJoY~ z&GE+F$f>4lN=X4-|9v*5O*Os>>r87u z!_1NSV?_X&HeFR1fOFb8_P)4lybJ6?1BWK`Tv2;4t|x1<#@17UO|hLGnrB%nu)fDk zfstJ4{X4^Y<8Lj<}g2^kksSefQTMuTo?tJLCh zC~>CR#a0hADw!_Vg*5fJwV{~S(j8)~sn>Oyt(ud2$1YfGck77}xN@3U_#T`q)f9!2 zf>Ia;Gwp2_C>WokU%(z2ec8z94pZyhaK+e>3a9sj^-&*V494;p9-xk+u1Jn#N_&xs z59OI2w=PuTErv|aNcK*>3l^W*p3}fjXJjJAXtBA#%B(-0--s;1U#f8gFYW!JL+iVG zV0SSx5w8eVgE?3Sg@eQv)=x<+-JgpVixZQNaZr}3b8sVyVs$@ndkF5FYKka@b+YAh z#nq_gzlIDKEs_i}H4f)(VQ!FSB}j>5znkVD&W0bOA{UZ7h!(FXrBbtdGA|PE1db>s z$!X)WY)u#7P8>^7Pjjj-kXNBuJX3(pJVetTZRNOnR5|RT5D>xmwxhAn)9KF3J05J; z-Mfb~dc?LUGqozC2p!1VjRqUwwDBnJhOua3vCCB-%ykW_ohSe?$R#dz%@Gym-8-RA zjMa_SJSzIl8{9dV+&63e9$4;{=1}w2=l+_j_Dtt@<(SYMbV-18&%F@Zl7F_5! z@xwJ0wiDdO%{}j9PW1(t+8P7Ud79yjY>x>aZYWJL_NI?bI6Y02`;@?qPz_PRqz(7v``20`- z033Dy|4;y6di|>cz|P-z|6c&3f&g^OAt8aN0Zd&0yZ>dq2aFCsE<~Ucf$v{sL=*++ zBxFSa2lfA+Y%U@B&3D=&CBO&u`#*nNc|PCY7XO<}MnG0VR764XrHtrb5zwC*2F!Lp zE<~Vj0;z!S-|3M4DFxuQ=`ShTf28<9p!81(0hFbGNqF%0gg*orez9!qt8e%o@Yfl@ zhvY}{@3&f??}7<`p>FyU;7?VkKbh8_=csozU=|fH&szgZ{=NDCylQ>EH^x5!K3~-V z)_2Y>0uJ`Z0Pb58y`RL+&n@m9tJ)O<%q#&u#DAIt+-rRt0eSe1MTtMl@W)H$b3D)@ z*A-1bUgZI)>HdcI4&W>P4W5{-j=s5p5`cbQ+{(g0+RDnz!TR^mxSLu_y#SDVKrj8i zA^hi6>jMGM;`$9Vfb-Yf!47b)Ow`2OKtNB=z|Kxa$5O}WPo;(Dc^`q(7X8kkeFyO8 z{XOq^07=u|7*P2`m;>PIFf=i80MKUxsN{d2cX0M+REsE*20+WQ79T9&cqT>=I_U% z{=8~^Isg(Nzo~`4iQfIb_#CVCD>#5h>=-Z#5dH}WxYzn%0)GAm6L2WdUdP=0_h>7f z(jh&7%1i(ZOn+}D8$iGK4Vs{pmHl_w4Qm-46H9>4^{3dz^DZDh+dw)6Xd@CpQNK$j z{CU;-cmpK=egplZ3y3%y=sEnCJ^eYVKXzV8H2_r*fJ*%*B;a1_lOpt6)IT1IAK2eB z{rie|uDJUrbgfUE>~C>@RO|m5ex55F{=~Bb4Cucp{ok7Yf9V}QuZ`#Gc|WaqsQlK- zKaV)iMRR__&Ak2Z=IM9R9g5$WM4u{a^C-7uX*!myEym z#_#p^T!P~#Dx$%^K>Y_nj_3J*E_LwJ60-5Xu=LkJAwcP@|0;a&+|+ZX`Jbj9P5;T% z|KOc}4*#4o{U?09`9Hz`Xo-I!P=9XfIrr*MQ}y=$!qgv?_J38^bNb4kM&_OVg^_=Eu-qG5U(fw0KMgH){C8pazq~51rN97hf#20-7=aK0)N|UM H-+%o-(+5aQ literal 0 HcmV?d00001 From e9dc31b6a3a2619fba3d08d2d032b5a997c55401 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 23:04:17 +0500 Subject: [PATCH 143/177] Fix system navigation bar color --- lib/navigation/widgets/app_top_bar.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index 95852f10..530fb879 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -20,17 +20,22 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; + Brightness iconBrightness = + colorScheme.background.computeLuminance() > 0.179 + ? Brightness.dark + : Brightness.light; + return PreferredSize( preferredSize: preferredSize, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: AppBar( systemOverlayStyle: SystemUiOverlayStyle( + systemNavigationBarColor: colorScheme.background, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: iconBrightness, statusBarColor: colorScheme.background, - statusBarIconBrightness: - colorScheme.background.computeLuminance() > 0.179 - ? Brightness.dark - : Brightness.light, // For Android (dark icons) + statusBarIconBrightness: iconBrightness, // For Android (dark icons) ), scrolledUnderElevation: 0, toolbarHeight: preferredSize.height, From 482102e7ebe7cca0db5dbebcc9fcc9c86dfbe2fb Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 23:19:49 +0500 Subject: [PATCH 144/177] Fix database --- .../kotlin/com/vicolo/chrono/MainActivity.kt | 138 ++++-------------- assets/timezones.db | Bin 397312 -> 397312 bytes 2 files changed, 27 insertions(+), 111 deletions(-) diff --git a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt index ec29168e..b1a86021 100644 --- a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt +++ b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt @@ -1,123 +1,39 @@ package com.vicolo.chrono -import android.app.Activity +import android.content.Context import android.content.Intent -import android.net.Uri -import androidx.documentfile.provider.DocumentFile +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar +import java.util.ArrayList +import androidx.annotation.NonNull import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel - -class MainActivity : FlutterActivity() { - private val channel = "com.vicolo.chrono/documents" - private lateinit var result: MethodChannel.Result - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel) - .setMethodCallHandler { call, result -> - when (call.method) { - "getDirectoryPath" -> { - this.result = result - val intent = Intent(this, DirectoryPickerActivity::class.java) - startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE) - } - "listDirectories" -> { - val uriString = call.argument("uri")!! - val uri = Uri.parse(uriString) - val directories = listDirectories(uri) - result.success(directories) - } - "listFiles" -> { - val uriString = call.argument("uri")!! - val uri = Uri.parse(uriString) - val files = listFiles(uri) - result.success(files) - } - "getFileChunk" -> { - val uriString = call.argument("uri")!! - val offset = call.argument("offset")!! - val chunkSize = call.argument("chunkSize")!! - val uri = Uri.parse(uriString) - val chunk = readFileChunk(uri, offset, chunkSize) - if (chunk != null) { - result.success(chunk) - } else { - result.error("READ_ERROR", "Failed to read file chunk", null) - } - } - else -> result.notImplemented() - } - } - } +import io.flutter.plugins.GeneratedPluginRegistrant; - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { - val pickedDirectoryUri = data?.getStringExtra("getDirectoryUri") - if (pickedDirectoryUri != null) { - result.success(pickedDirectoryUri) - } else { - result.error("NO_DIRECTORY_URI", "No directory URI found", null) - } - } - } - private fun listDirectories(uri: Uri): List { - val directory = DocumentFile.fromTreeUri(this, uri) - val directories = mutableListOf() - if (directory != null && directory.isDirectory) { - for (item in directory.listFiles()) { - if (item.isDirectory) { - directories.add(item.uri.toString()) - } - } - } +class MainActivity: FlutterActivity() { + private val CHANNEL = "com.vicolo.chrono/alarm" + + // // create static method channel + // companion object { + // lateinit var channel: MethodChannel + // } - return directories - } + // override fun onCreate(savedInstanceState: Bundle?) { + // super.onCreate(savedInstanceState) + // // MethodChannelHolder.init(flutterView) + // // MethodChannelHolder.invokeMethod("onBoot") + // } - private fun listFiles(uri: Uri): List> { - val directory = DocumentFile.fromTreeUri(this, uri) - val files = mutableListOf>() - - if (directory != null && directory.isDirectory) { - for (item in directory.listFiles()) { - if (item.isFile) { - val fileInfo: Map = mapOf( - "uri" to item.uri.toString(), - "name" to (item.name ?: "Unknown"), - "size" to item.length(), - "modified" to item.lastModified() - ) - files.add(fileInfo) - } - } - } - - return files - } - - private fun readFileChunk(uri: Uri, offset: Int, chunkSize: Int): ByteArray? { - return try { - contentResolver.openInputStream(uri)?.use { inputStream -> - inputStream.skip(offset.toLong()) - val buffer = ByteArray(chunkSize) - val bytesRead = inputStream.read(buffer, 0, chunkSize) - if (bytesRead != -1) { - buffer.copyOf(bytesRead) - } else { - null - } - } - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - companion object { - const val PICK_DIRECTORY_REQUEST_CODE = 1 + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + // GeneratedPluginRegistrant.registerWith(flutterEngine) + // flutterEngine.plugins.add(InitiateCallsToDartInBgPlugin()) + } } diff --git a/assets/timezones.db b/assets/timezones.db index cdb2ee337f2d7860b996de5c60d75202a881e6ef..1ae1ea1590e34c2349ab8d5afda50f94b99e9380 100644 GIT binary patch delta 654 zcmZozAknZuVuCay^F$eEer5)}vTj}m1_nm{i46P`Hx?@JZ|>tSKET9NHktW?31ib{ ziwkx+sUh3xgcak16aW+k7KERm3 z*tb390HYkIFi;;{yTUX!7Daja>76GTr5RJVZ#>Buz$VSt$B@bJl!5;m|3>~c{tSM1 zenoyJzT14e_-1XFxxiS&r-N*S>@;r{X?c0C#H7TWN-U1pzUv;NJ0lavj_J$~7_%6U zY|neZ=*5BJ3c2aAERxFd)Azq%lmxos=?g}ga3+=m)0rKZRxzI0zRrP3h(#CKPPsYx zAPs(rNjas(V1Ik0CKcu9!F|5{tqYTgG!u`&bmjymD@K{^b_q35EK*3!QE*Dk!RCPNd`zsL zZJC&l0z-i{kI{U3kprtW<3(U#u!=Jab7&*tQ0{OgIP}gqvw}kJlQXL&i$1c23WuXX Wk?fRMS)$;Rn1MBtqC8la9R~oDk*(YS delta 605 zcmZozAknZuVuCay(?l6(ekKMz|5{!K1_nm{i46P`Hx?@JZ|>tSKETAlFq!#+31iu2 ziwkxVaY`XEL?VoqvtNoHPVVxB@sYEe;UNq$k~=J|J$9s@N` zXFkA~z}U1sZx(5J^XUukF-q{FxMTaSdyMXkV232W%KE$Uoc9dX?XgAQ6?O$!GUQN;f*Z0BQQ{cH>IAZs3@_VgkLR%^ziK!37|GxKU4t_1nhDY2v^ hvk03%KRL5nvastMjs`{KbUP1LX*A=bJXn_<2LR0q#nb=* From 2c83416ea30b8efd14d4cc6b87858b6076ec9f24 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 8 Sep 2024 23:48:31 +0500 Subject: [PATCH 145/177] Add alarm labels to different uis --- lib/alarm/logic/alarm_reminder_notifications.dart | 6 +++--- lib/alarm/screens/alarm_notification_screen.dart | 6 ++++++ lib/alarm/types/alarm.dart | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) 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/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 3e3e3030..0d616f7e 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -121,6 +121,12 @@ class _AlarmNotificationScreenState extends State { child: Column( children: [ const Spacer(), + if (alarm.label.isNotEmpty) + Text( + alarm.label, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), const Clock( // dateTime: Date, horizontalAlignment: ElementAlignment.center, diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index cda6e805..66067aa7 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -262,7 +262,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); From 569e8726d6b06a45625f92592f312273d9c69ad5 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 9 Sep 2024 12:30:10 +0500 Subject: [PATCH 146/177] Make skipped alarm invisible to the system --- android/app/src/main/AndroidManifest.xml | 4 +++- lib/alarm/types/alarm.dart | 13 ++++++++++--- lib/alarm/types/alarm_runner.dart | 7 ++++--- lib/alarm/types/schedules/alarm_schedule.dart | 6 +++++- lib/alarm/types/schedules/daily_alarm_schedule.dart | 5 +++-- lib/alarm/types/schedules/dates_alarm_schedule.dart | 4 ++-- lib/alarm/types/schedules/once_alarm_schedule.dart | 4 ++-- lib/alarm/types/schedules/range_alarm_schedule.dart | 4 ++-- .../types/schedules/weekly_alarm_schedule.dart | 4 ++-- 9 files changed, 33 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ae52c407..d4255cfa 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,9 @@ - + diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 66067aa7..66797ac2 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -79,7 +79,8 @@ 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 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; @@ -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(); } diff --git a/lib/alarm/types/alarm_runner.dart b/lib/alarm/types/alarm_runner.dart index 5043ba2d..f40aa2f5 100644 --- a/lib/alarm/types/alarm_runner.dart +++ b/lib/alarm/types/alarm_runner.dart @@ -7,16 +7,17 @@ class AlarmRunner extends JsonSerializable { late int _id; DateTime? _currentScheduleDateTime; - get id => _id; + get id => _id; DateTime? get currentScheduleDateTime => _currentScheduleDateTime; AlarmRunner() { _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 { 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..a18f1e1d 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 { + Future schedule(Time time, String description, + [bool alarmClock = false]) async { DateTime alarmDate = getDailyAlarmDate(time); - await _alarmRunner.schedule(alarmDate,description); + 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..794c172a 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); + 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 b17dcb62..bd8480ad 100644 --- a/lib/alarm/types/schedules/range_alarm_schedule.dart +++ b/lib/alarm/types/schedules/range_alarm_schedule.dart @@ -42,7 +42,7 @@ 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 @@ -51,7 +51,7 @@ class RangeAlarmSchedule extends AlarmSchedule { if (alarmDate.isAfter(endDate)) { _isFinished = true; } else { - await _alarmRunner.schedule(alarmDate, description); + await _alarmRunner.schedule(alarmDate, description, alarmClock); _isFinished = false; } } diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart index 222d11e1..7a36bc5c 100644 --- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart +++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart @@ -84,7 +84,7 @@ class WeeklyAlarmSchedule extends AlarmSchedule { super(); @override - Future schedule(Time time,String description) async { + Future schedule(Time time,String description, [bool alarmClock = false]) async { for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { weekdaySchedule.alarmRunner.cancel(); } @@ -103,7 +103,7 @@ class WeeklyAlarmSchedule extends AlarmSchedule { for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { DateTime alarmDate = getWeeklyAlarmDate(time, weekdaySchedule.weekday); - await weekdaySchedule.alarmRunner.schedule(alarmDate,description); + await weekdaySchedule.alarmRunner.schedule(alarmDate,description, alarmClock); } } From 93190c55df4794d3841982ef29ae7d91f59e9ea0 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 9 Sep 2024 20:27:43 +0500 Subject: [PATCH 147/177] Change foreground type --- android/app/src/main/AndroidManifest.xml | 4 +- android/build.gradle | 1 + lib/alarm/logic/alarm_isolate.dart | 11 +- .../screens/alarm_notification_screen.dart | 10 +- lib/app.dart | 6 +- lib/navigation/data/visisbility.dart | 0 lib/notifications/data/action_keys.dart | 2 + .../data/fullscreen_notification_data.dart | 15 ++ .../logic/alarm_notifications.dart | 226 ++++++++++++++++ lib/notifications/logic/foreground_task.dart | 2 +- .../logic/notification_callbacks.dart | 13 +- .../notifications_listeners.dart} | 6 +- .../types/alarm_notification_arguments.dart | 17 ++ .../types/fullscreen_notification_data.dart | 14 +- .../fullscreen_notification_manager.dart | 254 ------------------ lib/system/logic/handle_intents.dart | 12 +- lib/timer/logic/timer_notification.dart | 2 - .../screens/timer_notification_screen.dart | 8 +- 18 files changed, 296 insertions(+), 307 deletions(-) create mode 100644 lib/navigation/data/visisbility.dart create mode 100644 lib/notifications/data/action_keys.dart create mode 100644 lib/notifications/data/fullscreen_notification_data.dart create mode 100644 lib/notifications/logic/alarm_notifications.dart rename lib/notifications/{types/notifications_controller.dart => logic/notifications_listeners.dart} (89%) create mode 100644 lib/notifications/types/alarm_notification_arguments.dart delete mode 100644 lib/notifications/types/fullscreen_notification_manager.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d4255cfa..4b8e35b2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ - + diff --git a/android/build.gradle b/android/build.gradle index abc3b441..7f756f55 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,6 +2,7 @@ allprojects { repositories { google() mavenCentral() + maven { url "${project(':background_fetch').projectDir}/libs" } } } diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index c9e1699a..a7a4606d 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -5,6 +5,7 @@ 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/debug/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/timer.dart'; import 'package:flutter/foundation.dart'; @@ -13,7 +14,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'; @@ -116,7 +116,7 @@ void triggerAlarm(int scheduleId, Json params) async { // Remove any existing alarm notifications if (RingingManager.isAlarmRinging) { - await AlarmNotificationManager.removeNotification( + await removeAlarmNotification( ScheduledNotificationType.alarm); } @@ -134,9 +134,8 @@ void triggerAlarm(int scheduleId, Json params) async { 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, @@ -190,14 +189,14 @@ void triggerTimer(int scheduleId, Json params) async { // Remove any existing timer notifications if (RingingManager.isTimerRinging) { - await AlarmNotificationManager.removeNotification( + await removeAlarmNotification( ScheduledNotificationType.timer); } RingtonePlayer.playTimer(timer); RingingManager.ringTimer(scheduleId); - AlarmNotificationManager.showFullScreenNotification( + showAlarmNotification( type: ScheduledNotificationType.timer, scheduleIds: RingingManager.ringingTimerIds, snoozeActionLabel: '+${timer.addLength.floor()}:00', diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 0d616f7e..16409e40 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -1,14 +1,14 @@ import 'dart:ui'; import 'package:clock_app/alarm/logic/alarm_isolate.dart'; -import 'package:clock_app/alarm/logic/update_alarms.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/debug/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'; @@ -49,7 +49,7 @@ class _AlarmNotificationScreenState extends State { widget.onPop!(); Navigator.of(context).pop(true); } else { - AlarmNotificationManager.dismissNotification(widget.scheduleId, + dismissAlarmNotification(widget.scheduleId, widget.dismissType, ScheduledNotificationType.alarm); } } else { @@ -68,7 +68,7 @@ class _AlarmNotificationScreenState extends State { Alarm? currentAlarm = getAlarmById(widget.scheduleId); if (currentAlarm == null) { - AlarmNotificationManager.dismissNotification(widget.scheduleId, + dismissAlarmNotification(widget.scheduleId, widget.dismissType, ScheduledNotificationType.alarm); return; } @@ -96,7 +96,7 @@ class _AlarmNotificationScreenState extends State { } void _snoozeAlarm() { - AlarmNotificationManager.snoozeAlarm( + snoozeAlarm( widget.scheduleId, ScheduledNotificationType.alarm); } diff --git a/lib/app.dart b/lib/app.dart index 3c6da52f..9182ab7f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,8 +5,8 @@ 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'; @@ -62,7 +62,7 @@ class _AppState extends State { setDigitalClockWidgetData(context); - NotificationController.setListeners(); + setNotificationListeners(); _appearanceSettings = appSettings.getGroup("Appearance"); _colorSettings = _appearanceSettings.getGroup("Colors"); diff --git a/lib/navigation/data/visisbility.dart b/lib/navigation/data/visisbility.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/notifications/data/action_keys.dart b/lib/notifications/data/action_keys.dart new file mode 100644 index 00000000..8b660b65 --- /dev/null +++ b/lib/notifications/data/action_keys.dart @@ -0,0 +1,2 @@ +const String snoozeActionKey = "snooze"; +const String dismissActionKey = "dismiss"; diff --git a/lib/notifications/data/fullscreen_notification_data.dart b/lib/notifications/data/fullscreen_notification_data.dart new file mode 100644 index 00000000..72cda121 --- /dev/null +++ b/lib/notifications/data/fullscreen_notification_data.dart @@ -0,0 +1,15 @@ +import 'package:clock_app/common/types/notification_type.dart'; +import 'package:clock_app/navigation/types/routes.dart'; +import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; + +Map + alarmNotificationData = { + ScheduledNotificationType.alarm: FullScreenNotificationData( + id: 0, + route: Routes.alarmNotificationRoute, + ), + ScheduledNotificationType.timer: FullScreenNotificationData( + id: 1, + route: Routes.timerNotificationRoute, + ), +}; diff --git a/lib/notifications/logic/alarm_notifications.dart b/lib/notifications/logic/alarm_notifications.dart new file mode 100644 index 00000000..7adab2ec --- /dev/null +++ b/lib/notifications/logic/alarm_notifications.dart @@ -0,0 +1,226 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:awesome_notifications/android_foreground_service.dart'; +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/app.dart'; +import 'package:clock_app/common/types/notification_type.dart'; +import 'package:clock_app/notifications/data/action_keys.dart'; +import 'package:clock_app/notifications/data/fullscreen_notification_data.dart'; +import 'package:clock_app/notifications/data/notification_channel.dart'; +import 'package:clock_app/alarm/logic/schedule_alarm.dart'; +import 'package:clock_app/navigation/types/routes.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; +import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; +import 'package:flutter_fgbg/flutter_fgbg.dart'; +import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; +import 'package:move_to_background/move_to_background.dart'; +import 'package:receive_intent/receive_intent.dart'; + +FGBGType appVisibilityWhenAlarmNotificationCreated = FGBGType.foreground; + +void showAlarmNotification({ + required ScheduledNotificationType type, + required List scheduleIds, + bool showSnoozeButton = true, + bool tasksRequired = false, + required String title, + required String body, + required String dismissActionLabel, + required String snoozeActionLabel, +}) { + FullScreenNotificationData data = alarmNotificationData[type]!; + + List actionButtons = []; + + if (scheduleIds.length > 1) { + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: dismissActionKey, + label: '$dismissActionLabel All', + actionType: ActionType.SilentAction, + autoDismissible: true, + )); + } else { + if (showSnoozeButton) { + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: snoozeActionKey, + label: snoozeActionLabel, + actionType: ActionType.SilentAction, + autoDismissible: true, + )); + } + + actionButtons.add(NotificationActionButton( + showInCompactView: true, + key: dismissActionKey, + label: "${tasksRequired ? "Solve tasks to " : ""}$dismissActionLabel", + actionType: tasksRequired ? ActionType.Default : ActionType.SilentAction, + autoDismissible: tasksRequired ? false : true, + )); + } + + AwesomeNotifications().createNotification( + content: NotificationContent( + id: data.id, + channelKey: alarmNotificationChannelKey, + title: title, + body: body, + payload: { + "scheduleIds": json.encode(scheduleIds), + "type": type.name, + "tasksRequired": tasksRequired.toString(), + }, + category: NotificationCategory.Alarm, + fullScreenIntent: true, + autoDismissible: false, + wakeUpScreen: true, + locked: true, + ), + actionButtons: actionButtons); +} + +Future removeAlarmNotification(ScheduledNotificationType type) async { + FullScreenNotificationData data = alarmNotificationData[type]!; + + await AwesomeNotifications() + .cancelNotificationsByChannelKey(alarmNotificationChannelKey); + await AndroidForegroundService.stopForeground(data.id); +} + +Future closeAlarmNotification(ScheduledNotificationType type) async { + final intent = await ReceiveIntent.getInitialIntent(); + + await removeAlarmNotification(type); + + await FlutterShowWhenLocked().hide(); + + // If app was launched from a notification, close the app when the notification + // is closed + if (intent?.action == "SELECT_NOTIFICATION") { + await MoveToBackground.moveTaskToBack(); + // SystemNavigator.pop(); + } else { + // If notification was created while app was in background, move app back + // to background when we close the notification + if (appVisibilityWhenAlarmNotificationCreated == FGBGType.background) { + appVisibilityWhenAlarmNotificationCreated = FGBGType.foreground; + await MoveToBackground.moveTaskToBack(); + } + } + // If we were on the alarm screen, pop it off the stack. Sometimes the system + // decides to show a heads up notification instead of a full screen one, so + // we can't always pop the top screen. + Routes.popIf(alarmNotificationData[type]?.route); +} + +Future snoozeAlarm(int scheduleId, ScheduledNotificationType type) async { + await stopAlarm(scheduleId, type, AlarmStopAction.snooze); +} + +Future dismissAlarm( + int scheduleId, ScheduledNotificationType type) async { + await stopAlarm(scheduleId, type, AlarmStopAction.dismiss); +} + +Future stopAlarm(int scheduleId, ScheduledNotificationType type, + AlarmStopAction action) async { + // Send a message to tell the alarm isolate to run the code to stop alarm + // See stopScheduledNotification in lib/alarm/logic/alarm_isolate.dart + IsolateNameServer.lookupPortByName(stopAlarmPortName) + ?.send([scheduleId, type.name, action.name]); +} + +Future dismissAlarmNotification(int scheduleId, AlarmDismissType dismissType, + ScheduledNotificationType type) async { + switch (dismissType) { + case AlarmDismissType.dismiss: + await dismissAlarm(scheduleId, type); + break; + case AlarmDismissType.skip: + await updateAlarmById(scheduleId, (alarm) async { + alarm.setShouldSkip(true); + }); + break; + case AlarmDismissType.snooze: + await snoozeAlarm(scheduleId, type); + break; + + case AlarmDismissType.unsnooze: + await updateAlarmById(scheduleId, (alarm) async { + await alarm.cancelSnooze(); + await alarm.update("Skipped snooze"); + }); + break; + } + await closeAlarmNotification(type); +} + + + +Future openAlarmNotificationScreen( + FullScreenNotificationData data, + List scheduleIds, { + bool tasksOnly = false, + AlarmDismissType dismissType = AlarmDismissType.dismiss, +}) async { + await FlutterShowWhenLocked().show(); + // If we're already on the same notification screen, pop it off the + // stack so we don't have two of them on the stack. + if (Routes.currentRoute == data.route) { + Routes.pop(); + } + App.navigatorKey.currentState?.pushNamedAndRemoveUntil( + data.route, + (route) => (route.settings.name != data.route) || route.isFirst, + arguments: AlarmNotificationArguments( + scheduleIds: scheduleIds, + tasksOnly: tasksOnly, + dismissType: dismissType), + ); +} + +Future handleAlarmNotificationDismiss( + ReceivedAction action, AlarmDismissType dismissType) async { + Payload payload = action.payload!; + final type = ScheduledNotificationType.values.byName((payload['type'])!); + FullScreenNotificationData data = alarmNotificationData[type]!; + bool tasksRequired = payload['tasksRequired'] == 'true'; + List scheduleIds = + (json.decode((payload['scheduleIds'])!) as List).cast(); + if (scheduleIds.isEmpty) return; + + if (tasksRequired && dismissType != AlarmDismissType.snooze) { + await openAlarmNotificationScreen(data, scheduleIds, + tasksOnly: true, dismissType: dismissType); + } else { + await dismissAlarmNotification(scheduleIds.first, dismissType, type); + } +} + +Future handleAlarmNotificationAction(ReceivedAction action) async { + Payload payload = action.payload!; + final type = ScheduledNotificationType.values.byName((payload['type'])!); + FullScreenNotificationData data = alarmNotificationData[type]!; + + List scheduleIds = + (json.decode((payload['scheduleIds'])!) as List).cast(); + + switch (action.buttonKeyPressed) { + case snoozeActionKey: + await handleAlarmNotificationDismiss(action, AlarmDismissType.snooze); + break; + + case dismissActionKey: + await handleAlarmNotificationDismiss(action, AlarmDismissType.dismiss); + break; + + // When notification is created or notification is clicked + default: + await openAlarmNotificationScreen(data, scheduleIds); + break; + } +} diff --git a/lib/notifications/logic/foreground_task.dart b/lib/notifications/logic/foreground_task.dart index b1de2b75..7cffd859 100644 --- a/lib/notifications/logic/foreground_task.dart +++ b/lib/notifications/logic/foreground_task.dart @@ -24,7 +24,7 @@ void initForegroundTask() { playSound: false, ), foregroundTaskOptions: const ForegroundTaskOptions( - interval: 5000, + interval: 1000 * 60, isOnceEvent: false, autoRunOnBoot: true, allowWakeLock: true, diff --git a/lib/notifications/logic/notification_callbacks.dart b/lib/notifications/logic/notification_callbacks.dart index 4aba2833..2901434c 100644 --- a/lib/notifications/logic/notification_callbacks.dart +++ b/lib/notifications/logic/notification_callbacks.dart @@ -1,7 +1,8 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; import 'package:clock_app/stopwatch/logic/update_stopwatch.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; @@ -17,7 +18,7 @@ Future onNotificationCreatedMethod( Payload payload = receivedNotification.payload!; int? scheduleId = int.tryParse(payload['scheduleId']); if (scheduleId == null) return; - AlarmNotificationManager.handleNotificationCreated(receivedNotification); + // AlarmNotificationManager.handleNotificationCreated(receivedNotification); break; } } @@ -35,7 +36,7 @@ Future onDismissActionReceivedMethod( switch (receivedAction.channelKey) { case alarmNotificationChannelKey: - AlarmNotificationManager.handleNotificationDismiss( + handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.dismiss); break; } @@ -48,16 +49,16 @@ Future onActionReceivedMethod(ReceivedAction receivedAction) async { switch (receivedAction.channelKey) { case alarmNotificationChannelKey: - AlarmNotificationManager.handleNotificationAction(receivedAction); + handleAlarmNotificationAction(receivedAction); break; case reminderNotificationChannelKey: switch (receivedAction.buttonKeyPressed) { case 'alarm_skip': - await AlarmNotificationManager.handleNotificationDismiss( + await handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.skip); break; case 'alarm_skip_snooze': - await AlarmNotificationManager.handleNotificationDismiss( + await handleAlarmNotificationDismiss( receivedAction, AlarmDismissType.unsnooze); break; } diff --git a/lib/notifications/types/notifications_controller.dart b/lib/notifications/logic/notifications_listeners.dart similarity index 89% rename from lib/notifications/types/notifications_controller.dart rename to lib/notifications/logic/notifications_listeners.dart index 3fe1870b..1cec1d33 100644 --- a/lib/notifications/types/notifications_controller.dart +++ b/lib/notifications/logic/notifications_listeners.dart @@ -1,8 +1,7 @@ import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:clock_app/notifications/logic/notification_callbacks.dart'; -class NotificationController { - static void setListeners() { + void setNotificationListeners() { // Only after at least the action method is set, the notification events are delivered AwesomeNotifications().setListeners( onActionReceivedMethod: onActionReceivedMethod, @@ -10,4 +9,5 @@ class NotificationController { onNotificationDisplayedMethod: onNotificationDisplayedMethod, onDismissActionReceivedMethod: onDismissActionReceivedMethod); } -} + + diff --git a/lib/notifications/types/alarm_notification_arguments.dart b/lib/notifications/types/alarm_notification_arguments.dart new file mode 100644 index 00000000..0fb0b6a7 --- /dev/null +++ b/lib/notifications/types/alarm_notification_arguments.dart @@ -0,0 +1,17 @@ +class AlarmNotificationArguments { + final List scheduleIds; + final bool tasksOnly; + final AlarmDismissType dismissType; + + AlarmNotificationArguments( + {required this.scheduleIds, + required this.tasksOnly, + required this.dismissType}); +} + +enum AlarmDismissType { + dismiss, + skip, + snooze, + unsnooze, +} diff --git a/lib/notifications/types/fullscreen_notification_data.dart b/lib/notifications/types/fullscreen_notification_data.dart index 2e104834..78779836 100644 --- a/lib/notifications/types/fullscreen_notification_data.dart +++ b/lib/notifications/types/fullscreen_notification_data.dart @@ -1,6 +1,4 @@ import 'package:clock_app/common/types/json.dart'; -import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/navigation/types/routes.dart'; class FullScreenNotificationData { int id; @@ -14,14 +12,4 @@ class FullScreenNotificationData { typedef Payload = Json; -Map - alarmNotificationData = { - ScheduledNotificationType.alarm: FullScreenNotificationData( - id: 0, - route: Routes.alarmNotificationRoute, - ), - ScheduledNotificationType.timer: FullScreenNotificationData( - id: 1, - route: Routes.timerNotificationRoute, - ), -}; + diff --git a/lib/notifications/types/fullscreen_notification_manager.dart b/lib/notifications/types/fullscreen_notification_manager.dart deleted file mode 100644 index 4dcb8499..00000000 --- a/lib/notifications/types/fullscreen_notification_manager.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'dart:convert'; -import 'dart:isolate'; -import 'dart:ui'; - -import 'package:awesome_notifications/android_foreground_service.dart'; -import 'package:awesome_notifications/awesome_notifications.dart'; -import 'package:clock_app/alarm/logic/alarm_isolate.dart'; -import 'package:clock_app/alarm/logic/update_alarms.dart'; -import 'package:clock_app/app.dart'; -import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/notifications/data/notification_channel.dart'; -import 'package:clock_app/alarm/logic/schedule_alarm.dart'; -import 'package:clock_app/navigation/types/routes.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_data.dart'; -import 'package:flutter_fgbg/flutter_fgbg.dart'; -import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; -import 'package:move_to_background/move_to_background.dart'; -import 'package:receive_intent/receive_intent.dart'; - -class AlarmNotificationManager { - static const String _snoozeActionKey = "snooze"; - static const String _dismissActionKey = "dismiss"; - - static FGBGType appVisibilityWhenCreated = FGBGType.foreground; - - static void showFullScreenNotification({ - required ScheduledNotificationType type, - required List scheduleIds, - bool showSnoozeButton = true, - bool tasksRequired = false, - required String title, - required String body, - required String dismissActionLabel, - required String snoozeActionLabel, - }) { - FullScreenNotificationData data = alarmNotificationData[type]!; - - List actionButtons = []; - - if (scheduleIds.length > 1) { - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _dismissActionKey, - label: '$dismissActionLabel All', - actionType: ActionType.SilentAction, - autoDismissible: true, - )); - } else { - if (showSnoozeButton) { - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _snoozeActionKey, - label: snoozeActionLabel, - actionType: ActionType.SilentAction, - autoDismissible: true, - )); - } - - actionButtons.add(NotificationActionButton( - showInCompactView: true, - key: _dismissActionKey, - label: "${tasksRequired ? "Solve tasks to " : ""}$dismissActionLabel", - actionType: - tasksRequired ? ActionType.Default : ActionType.SilentAction, - autoDismissible: tasksRequired ? false : true, - )); - } - - AwesomeNotifications().createNotification( - content: NotificationContent( - id: data.id, - channelKey: alarmNotificationChannelKey, - title: title, - body: body, - payload: { - "scheduleIds": json.encode(scheduleIds), - "type": type.name, - "tasksRequired": tasksRequired.toString(), - }, - category: NotificationCategory.Alarm, - fullScreenIntent: true, - autoDismissible: false, - wakeUpScreen: true, - locked: true, - ), - actionButtons: actionButtons); - } - - static Future removeNotification(ScheduledNotificationType type) async { - FullScreenNotificationData data = alarmNotificationData[type]!; - - await AwesomeNotifications() - .cancelNotificationsByChannelKey(alarmNotificationChannelKey); - await AndroidForegroundService.stopForeground(data.id); - } - - static Future closeNotification(ScheduledNotificationType type) async { - final intent = await ReceiveIntent.getInitialIntent(); - - print(intent?.action); - - await removeNotification(type); - - await FlutterShowWhenLocked().hide(); - - // If app was launched from a notification, close the app when the notification - // is closed - if (intent?.action == "SELECT_NOTIFICATION") { - await MoveToBackground.moveTaskToBack(); - // SystemNavigator.pop(); - } else { - // If notification was created while app was in background, move app back - // to background when we close the notification - if (appVisibilityWhenCreated == FGBGType.background) { - appVisibilityWhenCreated = FGBGType.foreground; - await MoveToBackground.moveTaskToBack(); - } - } - // If we were on the alarm screen, pop it off the stack. Sometimes the system - // decides to show a heads up notification instead of a full screen one, so - // we can't always pop the top screen. - Routes.popIf(alarmNotificationData[type]?.route); - } - - static Future snoozeAlarm( - int scheduleId, ScheduledNotificationType type) async { - await stopAlarm(scheduleId, type, AlarmStopAction.snooze); - } - - static Future dismissAlarm( - int scheduleId, ScheduledNotificationType type) async { - await stopAlarm(scheduleId, type, AlarmStopAction.dismiss); - } - - static Future dismissNotification(int scheduleId, - AlarmDismissType dismissType, ScheduledNotificationType type) async { - switch (dismissType) { - case AlarmDismissType.dismiss: - await dismissAlarm(scheduleId, type); - break; - case AlarmDismissType.skip: - await updateAlarmById(scheduleId, (alarm) async { - alarm.setShouldSkip(true); - }); - break; - case AlarmDismissType.snooze: - await snoozeAlarm(scheduleId, type); - break; - - case AlarmDismissType.unsnooze: - await updateAlarmById(scheduleId, (alarm) async { - await alarm.cancelSnooze(); - await alarm.update("Skipped snooze"); - }); - break; - } - await closeNotification(type); - } - - static Future stopAlarm(int scheduleId, ScheduledNotificationType type, - AlarmStopAction action) async { - // Send a message to tell the alarm isolate to run the code to stop alarm - // See stopScheduledNotification in lib/alarm/logic/alarm_isolate.dart - IsolateNameServer.lookupPortByName(stopAlarmPortName) - ?.send([scheduleId, type.name, action.name]); - - // await closeNotification(type); - } - - static void handleNotificationCreated(ReceivedNotification notification) { - // _appVisibilityWhenCreated = AppVisibility.state; - } - - static Future openNotificationScreen( - FullScreenNotificationData data, - List scheduleIds, { - bool tasksOnly = false, - AlarmDismissType dismissType = AlarmDismissType.dismiss, - }) async { - await FlutterShowWhenLocked().show(); - // If we're already on the same notification screen, pop it off the - // stack so we don't have two of them on the stack. - if (Routes.currentRoute == data.route) { - Routes.pop(); - } - App.navigatorKey.currentState?.pushNamedAndRemoveUntil( - data.route, - (route) => (route.settings.name != data.route) || route.isFirst, - arguments: AlarmNotificationArguments( - scheduleIds: scheduleIds, - tasksOnly: tasksOnly, - dismissType: dismissType), - ); - } - - static Future handleNotificationDismiss( - ReceivedAction action, AlarmDismissType dismissType) async { - Payload payload = action.payload!; - final type = ScheduledNotificationType.values.byName((payload['type'])!); - FullScreenNotificationData data = alarmNotificationData[type]!; - bool tasksRequired = payload['tasksRequired'] == 'true'; - List scheduleIds = - (json.decode((payload['scheduleIds'])!) as List).cast(); - if (scheduleIds.isEmpty) return; - - if (tasksRequired && dismissType != AlarmDismissType.snooze) { - await openNotificationScreen(data, scheduleIds, - tasksOnly: true, dismissType: dismissType); - } else { - await dismissNotification(scheduleIds.first, dismissType, type); - } - } - - static Future handleNotificationAction(ReceivedAction action) async { - Payload payload = action.payload!; - final type = ScheduledNotificationType.values.byName((payload['type'])!); - FullScreenNotificationData data = alarmNotificationData[type]!; - - List scheduleIds = - (json.decode((payload['scheduleIds'])!) as List).cast(); - - switch (action.buttonKeyPressed) { - case _snoozeActionKey: - await handleNotificationDismiss(action, AlarmDismissType.snooze); - break; - - case _dismissActionKey: - await handleNotificationDismiss(action, AlarmDismissType.dismiss); - break; - - default: - await openNotificationScreen(data, scheduleIds); - break; - } - } -} - -class AlarmNotificationArguments { - final List scheduleIds; - final bool tasksOnly; - final AlarmDismissType dismissType; - - AlarmNotificationArguments( - {required this.scheduleIds, - required this.tasksOnly, - required this.dismissType}); -} - -enum AlarmDismissType { - dismiss, - skip, - snooze, - unsnooze, -} diff --git a/lib/system/logic/handle_intents.dart b/lib/system/logic/handle_intents.dart index 718a0a68..593badaf 100644 --- a/lib/system/logic/handle_intents.dart +++ b/lib/system/logic/handle_intents.dart @@ -4,20 +4,17 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/schedules/weekly_alarm_schedule.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:flutter/material.dart' hide Intent; import 'package:receive_intent/receive_intent.dart'; -// void navigateToTab(BuildContext context, int tab) { -// Navigator.of(context) -// .pushNamedAndRemoveUntil(Routes.rootRoute, (Route route) => false, arguments: {'tabIndex': tab});} - void handleIntent(Intent? receivedIntent, BuildContext context, Function(Alarm) onSetAlarm, Function(int) setTab) async { if (receivedIntent != null) { - print( + logger.i( "Intent received ${receivedIntent.action} ${receivedIntent.data} ${receivedIntent.extra}"); switch (receivedIntent.action) { case "android.intent.action.MAIN": @@ -96,8 +93,7 @@ void handleIntent(Intent? receivedIntent, BuildContext context, case "android.intent.action.VIEW_TIMERS": break; case "SELECT_NOTIFICATION": - AlarmNotificationManager.appVisibilityWhenCreated = AppVisibility.state; - print("************Select************************** ${AppVisibility.state}"); + appVisibilityWhenAlarmNotificationCreated = AppVisibility.state; break; default: break; diff --git a/lib/timer/logic/timer_notification.dart b/lib/timer/logic/timer_notification.dart index 175cc22f..3cecb357 100644 --- a/lib/timer/logic/timer_notification.dart +++ b/lib/timer/logic/timer_notification.dart @@ -4,8 +4,6 @@ import 'package:clock_app/timer/types/time_duration.dart'; import 'package:clock_app/timer/types/timer.dart'; Future updateTimerNotification(ClockTimer timer, int count) async { - // print("------------ ${timer.remainingSeconds.toDouble() / - // timer.currentDuration.inSeconds.toDouble()}"); List actionButtons = []; if (count == 1) { diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 07501141..86605da8 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -1,9 +1,9 @@ -import 'package:clock_app/alarm/logic/schedule_alarm.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/navigation/types/routes.dart'; -import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart'; +import 'package:clock_app/notifications/logic/alarm_notifications.dart'; +import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; import 'package:clock_app/notifications/widgets/notification_actions/slide_notification_action.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/timer/types/time_duration.dart'; @@ -37,12 +37,12 @@ class _TimerNotificationScreenState extends State { ); void _addTime() { - AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + dismissAlarmNotification(widget.scheduleIds[0], AlarmDismissType.snooze, ScheduledNotificationType.timer); } void _stop() { - AlarmNotificationManager.dismissNotification(widget.scheduleIds[0], + dismissAlarmNotification(widget.scheduleIds[0], AlarmDismissType.dismiss, ScheduledNotificationType.timer); } From c37c6d35a88a6321f225534ca1f350b1724a7ac7 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 00:09:33 +0500 Subject: [PATCH 148/177] Fix range alarm not working and date picker being stuck in past --- android/app/src/main/AndroidManifest.xml | 3 +- lib/alarm/data/alarm_settings_schema.dart | 9 +-- lib/alarm/logic/alarm_isolate.dart | 66 ++++++++++++------- lib/alarm/logic/alarm_time.dart | 41 ++++++------ lib/alarm/logic/schedule_alarm.dart | 12 ++-- lib/alarm/types/ringing_manager.dart | 2 + .../types/schedules/daily_alarm_schedule.dart | 2 +- .../types/schedules/once_alarm_schedule.dart | 2 +- .../types/schedules/range_alarm_schedule.dart | 2 +- .../schedules/weekly_alarm_schedule.dart | 8 +-- .../fields/date_picker_bottom_sheet.dart | 21 +++--- lib/debug/logic/logger.dart | 2 +- pubspec.lock | 8 +++ pubspec.yaml | 1 + 14 files changed, 108 insertions(+), 71 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4b8e35b2..0b7fe27d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,8 +13,7 @@ android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> + android:name="android.permission.SCHEDULE_EXACT_ALARM"/> diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index bd5da5ca..ed64315e 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -122,7 +122,7 @@ SettingGroup alarmSettingsSchema = SettingGroup( DateTimeSetting( "Date Range", (context) => AppLocalizations.of(context)!.alarmRangeSetting, - [DateTime.now(), DateTime.now().add(const Duration(days: 2))], + [], rangeOnly: true, enableConditions: [ ValueCondition(["Type"], (value) => value == RangeAlarmSchedule) @@ -312,9 +312,10 @@ SettingGroup alarmSettingsSchema = SettingGroup( ListSetting( "Tasks", (context) => AppLocalizations.of(context)!.tasksSetting, - kDebugMode - ? [AlarmTask(AlarmTaskType.math), AlarmTask(AlarmTaskType.sequence)] - : [], + [], + // 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( diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index a7a4606d..b5c0045c 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -80,31 +80,45 @@ void stopScheduledNotification(List message) { void triggerAlarm(int scheduleId, Json params) async { logger.i("Alarm triggered $scheduleId"); if (params == null) { - logger.e("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) { - logger.i("Skipping alarm $scheduleId"); + // 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; } @@ -116,13 +130,21 @@ void triggerAlarm(int scheduleId, Json params) async { // Remove any existing alarm notifications if (RingingManager.isAlarmRinging) { - await removeAlarmNotification( - 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( @@ -134,7 +156,6 @@ void triggerAlarm(int scheduleId, Json params) async { String timeFormatString = await loadTextFile("time_format_string"); String title = alarm.label.isEmpty ? "Alarm Ringing..." : alarm.label; - showAlarmNotification( type: ScheduledNotificationType.alarm, scheduleIds: [scheduleId], @@ -189,8 +210,7 @@ void triggerTimer(int scheduleId, Json params) async { // Remove any existing timer notifications if (RingingManager.isTimerRinging) { - await removeAlarmNotification( - ScheduledNotificationType.timer); + await removeAlarmNotification(ScheduledNotificationType.timer); } RingtonePlayer.playTimer(timer); diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart index 94061c5b..3e37ab0b 100644 --- a/lib/alarm/logic/alarm_time.dart +++ b/lib/alarm/logic/alarm_time.dart @@ -1,46 +1,43 @@ import 'package:clock_app/common/types/time.dart'; import 'package:clock_app/common/utils/date_time.dart'; +import 'package:clock_app/debug/logic/logger.dart'; // Calculates the DateTime when the provided `time` will next occur -DateTime getDailyAlarmDate( +DateTime getScheduleDateForTime( Time time, { DateTime? scheduleStartDate, int interval = 1, }) { - if (scheduleStartDate != null && scheduleStartDate.isAfter(DateTime.now())) { - return DateTime(scheduleStartDate.year, scheduleStartDate.month, - scheduleStartDate.day, time.hour, time.minute, time.second); - } - + // logger.d('getDailyAlarmDate: $time, $scheduleStartDate, $interval'); + // if (scheduleStartDate != null && scheduleStartDate.isAfter(DateTime.now())) { + // return DateTime(scheduleStartDate.year, scheduleStartDate.month, + // scheduleStartDate.day, time.hour, time.minute, time.second); + // } + // // If a date has not been provided, assume it to be today - DateTime scheduleDate = DateTime.now(); + DateTime scheduleDate = scheduleStartDate ?? DateTime.now(); DateTime alarmTime; - if (time.toHours() > scheduleDate.toHours()) { - // If the time is in the future, set the alarm for today + // if (time.toHours() > scheduleDate.toHours()) { + // // If the time is in the future, set the alarm for today alarmTime = DateTime(scheduleDate.year, scheduleDate.month, scheduleDate.day, time.hour, time.minute, time.second); - } else { - // If the time has already passed, set the alarm for next occurence - if (scheduleStartDate != null) { - scheduleDate = scheduleStartDate; - } - - while (scheduleDate.isBefore(DateTime.now())) { - scheduleDate = scheduleDate.add(Duration(days: interval)); + // } else { + while (alarmTime.isBefore(DateTime.now())) { + alarmTime = alarmTime.add(Duration(days: interval)); } - alarmTime = DateTime(scheduleDate.year, scheduleDate.month, - scheduleDate.day, time.hour, time.minute, time.second); - } + // alarmTime = DateTime(scheduleDate.year, scheduleDate.month, + // scheduleDate.day, time.hour, time.minute, time.second); + // } return alarmTime; } // 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/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index cd4012f6..f0ac0bc1 100644 --- a/lib/alarm/logic/schedule_alarm.dart +++ b/lib/alarm/logic/schedule_alarm.dart @@ -19,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')) { @@ -90,7 +92,8 @@ Future scheduleAlarm( }, ); - logger.i('Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); + logger.i( + 'Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); } } @@ -134,5 +137,6 @@ Future scheduleSnoozeAlarm(int scheduleId, Duration delay, await createSnoozeNotification(scheduleId, DateTime.now().add(delay)); } - logger.i('Scheduled snooze alarm $scheduleId for ${DateTime.now().add(delay)} with type ${type.name}: $description'); + logger.i( + 'Scheduled snooze alarm $scheduleId for ${DateTime.now().add(delay)} with type ${type.name}: $description'); } 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/daily_alarm_schedule.dart b/lib/alarm/types/schedules/daily_alarm_schedule.dart index a18f1e1d..4d2ae0a3 100644 --- a/lib/alarm/types/schedules/daily_alarm_schedule.dart +++ b/lib/alarm/types/schedules/daily_alarm_schedule.dart @@ -26,7 +26,7 @@ class DailyAlarmSchedule extends AlarmSchedule { @override Future schedule(Time time, String description, [bool alarmClock = false]) async { - DateTime alarmDate = getDailyAlarmDate(time); + DateTime alarmDate = getScheduleDateForTime(time); await _alarmRunner.schedule(alarmDate, description, alarmClock); } diff --git a/lib/alarm/types/schedules/once_alarm_schedule.dart b/lib/alarm/types/schedules/once_alarm_schedule.dart index 794c172a..9a7b8988 100644 --- a/lib/alarm/types/schedules/once_alarm_schedule.dart +++ b/lib/alarm/types/schedules/once_alarm_schedule.dart @@ -30,7 +30,7 @@ class OnceAlarmSchedule extends AlarmSchedule { if (currentScheduleDateTime?.isBefore(DateTime.now()) ?? false) { _isDisabled = true; } else { - DateTime alarmDate = getDailyAlarmDate(time); + 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 bd8480ad..b86514a3 100644 --- a/lib/alarm/types/schedules/range_alarm_schedule.dart +++ b/lib/alarm/types/schedules/range_alarm_schedule.dart @@ -46,7 +46,7 @@ class RangeAlarmSchedule extends AlarmSchedule { 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, + DateTime alarmDate = getScheduleDateForTime(time, scheduleStartDate: startDate, interval: intervalDays); if (alarmDate.isAfter(endDate)) { _isFinished = true; diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart index 7a36bc5c..b2d1e341 100644 --- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart +++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart @@ -85,9 +85,9 @@ class WeeklyAlarmSchedule extends AlarmSchedule { @override Future schedule(Time time,String description, [bool alarmClock = false]) async { - for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { - weekdaySchedule.alarmRunner.cancel(); - } + // 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. @@ -102,7 +102,7 @@ class WeeklyAlarmSchedule extends AlarmSchedule { } for (WeekdaySchedule weekdaySchedule in _weekdaySchedules) { - DateTime alarmDate = getWeeklyAlarmDate(time, weekdaySchedule.weekday); + DateTime alarmDate = getWeeklyScheduleDateForTIme(time, weekdaySchedule.weekday); await weekdaySchedule.alarmRunner.schedule(alarmDate,description, alarmClock); } } 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/debug/logic/logger.dart b/lib/debug/logic/logger.dart index 74d93d62..f0fad6f8 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -7,7 +7,7 @@ var logger = Logger( filter: FileLogFilter(), output: FileLoggerOutput(), printer: PrettyPrinter( - methodCount: 5, // Number of method calls to be displayed + methodCount: 0, // Number of method calls to be displayed errorMethodCount: 8, // Number of method calls if stacktrace is provided lineLength: 80, // Width of the output colors: true, // Colorful log messages diff --git a/pubspec.lock b/pubspec.lock index 3a46af24..e2e37e61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,6 +90,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + background_fetch: + dependency: "direct main" + description: + name: background_fetch + sha256: e9f26ae54d88310b7ac2a68f2f9fcee0081a4d5f11100f233a70702021e7ac4f + url: "https://pub.dev" + source: hosted + version: "1.3.7" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 30f36605..2bd40dc6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: flutter_animate: ^4.5.0 quick_actions: ^1.0.7 file_picker: ^8.0.7 + background_fetch: ^1.3.7 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From e23878935657d406ed0f3ad8ddebe634ebb360f3 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 12:05:09 +0500 Subject: [PATCH 149/177] Add tests for getScheduleDateForTime --- lib/alarm/logic/alarm_time.dart | 30 +- lib/debug/types/file_logger_output.dart | 13 +- pubspec.lock | 2 +- pubspec.yaml | 2 + test/alarm/logic/alarm_time.dart | 305 ++++++++++++++++++ .../logic/schedule_description_test.dart | 21 +- 6 files changed, 336 insertions(+), 37 deletions(-) create mode 100644 test/alarm/logic/alarm_time.dart diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart index 3e37ab0b..cac48ec0 100644 --- a/lib/alarm/logic/alarm_time.dart +++ b/lib/alarm/logic/alarm_time.dart @@ -1,5 +1,5 @@ +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/debug/logic/logger.dart'; // Calculates the DateTime when the provided `time` will next occur @@ -8,28 +8,16 @@ DateTime getScheduleDateForTime( DateTime? scheduleStartDate, int interval = 1, }) { - // logger.d('getDailyAlarmDate: $time, $scheduleStartDate, $interval'); - // if (scheduleStartDate != null && scheduleStartDate.isAfter(DateTime.now())) { - // return DateTime(scheduleStartDate.year, scheduleStartDate.month, - // scheduleStartDate.day, time.hour, time.minute, time.second); - // } - // - // If a date has not been provided, assume it to be today - DateTime scheduleDate = scheduleStartDate ?? DateTime.now(); - DateTime alarmTime; + DateTime now = clock.now(); - // if (time.toHours() > scheduleDate.toHours()) { - // // If the time is in the future, set the alarm for today - alarmTime = DateTime(scheduleDate.year, scheduleDate.month, - scheduleDate.day, time.hour, time.minute, time.second); - // } else { - while (alarmTime.isBefore(DateTime.now())) { - alarmTime = alarmTime.add(Duration(days: interval)); - } + // If a date has not been provided, assume it to be today + DateTime scheduleDate = scheduleStartDate ?? now; + DateTime alarmTime = DateTime(scheduleDate.year, scheduleDate.month, + scheduleDate.day, time.hour, time.minute, time.second); - // alarmTime = DateTime(scheduleDate.year, scheduleDate.month, - // scheduleDate.day, time.hour, time.minute, time.second); - // } + while (!alarmTime.isAfter(now)) { + alarmTime = alarmTime.add(Duration(days: interval)); + } return alarmTime; } diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart index 5213b650..4b3e363b 100644 --- a/lib/debug/types/file_logger_output.dart +++ b/lib/debug/types/file_logger_output.dart @@ -20,16 +20,17 @@ class FileLoggerOutput extends LogOutput { _ => "Unknown error", }; - _writeLog(message, event.level); + if (!Platform.environment.containsKey('FLUTTER_TEST')) { + _writeLog(message, event.level); - Future(() { if (event.level == Level.error && App.navigatorKey.currentContext != null) { - showSnackBar( - App.navigatorKey.currentContext!, message, - error: true, navBar: false, fab: false); + Future(() { + showSnackBar(App.navigatorKey.currentContext!, message, + error: true, navBar: false, fab: false); + }); } - }); + } } Future _writeLog(String message, Level level) async { diff --git a/pubspec.lock b/pubspec.lock index e2e37e61..990ea0e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -139,7 +139,7 @@ packages: source: hosted version: "2.0.3" clock: - dependency: transitive + dependency: "direct main" description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf diff --git a/pubspec.yaml b/pubspec.yaml index 2bd40dc6..62f34a66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,8 @@ dependencies: quick_actions: ^1.0.7 file_picker: ^8.0.7 background_fetch: ^1.3.7 + clock: ^1.1.1 + # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" diff --git a/test/alarm/logic/alarm_time.dart b/test/alarm/logic/alarm_time.dart new file mode 100644 index 00000000..5d365a94 --- /dev/null +++ b/test/alarm/logic/alarm_time.dart @@ -0,0 +1,305 @@ +import 'package:clock/clock.dart'; +import 'package:clock_app/alarm/logic/alarm_time.dart'; +import 'package:clock_app/common/types/time.dart'; +import 'package:flutter_test/flutter_test.dart'; + +DateTime currentDate = DateTime(2000, 1, 10, 10, 0); +DateTime futureScheduleStartDate = DateTime(2000, 1, 20, 10, 0); +DateTime pastScheduleStartDate = DateTime(2000, 1, 1, 10, 0); + +void testGetScheduleDateForTime(Time scheduleTime, DateTime expectedDate, + {DateTime? scheduleStartDate, int interval = 1}) { + withClock( + Clock.fixed(currentDate), + () { + DateTime scheduledDateTime = getScheduleDateForTime( + scheduleTime, + interval: interval, + scheduleStartDate: scheduleStartDate, + ); + expect( + scheduledDateTime, + DateTime( + expectedDate.year, + expectedDate.month, + expectedDate.day, + scheduleTime.hour, + scheduleTime.minute, + scheduleTime.second, + )); + }, + ); +} + +void main() async { + group('getScheduleDateForTime()', () { + group('with interval = 1', () { + group('without scheduleStartDate', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + )); + }, + ); + test( + "returns tomorrow's date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + )); + }, + ); + test( + "returns tomorrow's date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + )); + }, + ); + }); + group('with scheduleStartDate in the future', () { + test( + "returns scheduleStartDate when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + test( + "returns day after scheduleStartDate when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + test( + "returns day after scheduleStartDate when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + ); + }, + ); + }); + group('with scheduleStartDate in the past', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + test( + "returns tomorrow's date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + test( + "returns tomorrow's date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 1, + ), + scheduleStartDate: pastScheduleStartDate, + ); + }, + ); + }); + }); + group('with interval = 7', () { + group('without scheduleStartDate', () { + test( + "returns today's date when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day, + ), + interval: 7, + ); + }, + ); + test( + "returns next week date when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 7, + ), + interval: 7, + ); + }, + ); + test( + "returns next week date when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + currentDate.day + 7, + ), + interval: 7, + ); + }, + ); + }); + group('with scheduleStartDate in the future', () { + test( + "returns scheduleStartDate when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns scheduleStartDate when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns scheduleStartDate when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + futureScheduleStartDate.year, + futureScheduleStartDate.month, + futureScheduleStartDate.day, + ), + scheduleStartDate: futureScheduleStartDate, + interval: 7, + ); + }, + ); + }); + group('with scheduleStartDate in the past', () { + test( + "returns correctly when scheduled time is after current time", + () { + testGetScheduleDateForTime( + const Time(hour: 11, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns correctly when scheduled time is before current time", + () { + testGetScheduleDateForTime( + const Time(hour: 9, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + test( + "returns returns correctly when scheduled time is same as current time", + () { + testGetScheduleDateForTime( + const Time(hour: 10, minute: 0), + DateTime( + currentDate.year, + currentDate.month, + 15, + ), + scheduleStartDate: pastScheduleStartDate, + interval: 7, + ); + }, + ); + }); + }); + }); +} diff --git a/test/alarm/logic/schedule_description_test.dart b/test/alarm/logic/schedule_description_test.dart index 5c717db5..70dcc197 100644 --- a/test/alarm/logic/schedule_description_test.dart +++ b/test/alarm/logic/schedule_description_test.dart @@ -26,8 +26,8 @@ void testDescription(String name, Function(BuildContext) callback) { } void main() async { - group('getAlarmScheduleDescription', () { - testDescription('when alarm is snoozed', (context) async { + group('getAlarmScheduleDescription()', () { + testDescription('returns correctly when alarm is snoozed', (context) async { final alarm = Alarm(const Time(hour: 8, minute: 30)); await alarm.snooze(); @@ -52,7 +52,8 @@ void main() async { // expect(result, 'No future dates'); // }); - testDescription('when alarm is not enabled', (context) async { + testDescription('returns correctly when alarm is not enabled', + (context) async { final alarm = Alarm(const Time(hour: 8, minute: 30)); await alarm.disable(); @@ -63,7 +64,8 @@ void main() async { expect(result, 'Not scheduled'); }); - testDescription('when alarm has once schedule', (context) { + testDescription('returns correctly when alarm has once schedule', + (context) { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 0); @@ -76,7 +78,8 @@ void main() async { ); }); - testDescription('when alarm has daily schedule', (context) { + testDescription('returns correctly when alarm has daily schedule', + (context) { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 1); @@ -86,7 +89,7 @@ void main() async { expect(result, 'Every day'); }); - group('when alarm has weekly schedule', () { + group('returns correctly when alarm has weekly schedule', () { final alarm = Alarm(const Time(hour: 8, minute: 30)); alarm.setSettingWithoutNotify("Type", 2); testDescription("with all week days", (context) { @@ -99,7 +102,7 @@ void main() async { expect(result, 'Every day'); }); - testDescription("with only weekends", (context) { + testDescription("returns correctly with only weekends", (context) { alarm.setSettingWithoutNotify( "Week Days", [false, false, false, false, false, true, true]); @@ -108,7 +111,7 @@ void main() async { expect(result, 'Every weekend'); }); - testDescription("with only weekdays", (context) { + testDescription("returns correctly with only weekdays", (context) { alarm.setSettingWithoutNotify( "Week Days", [true, true, true, true, true, false, false]); @@ -117,7 +120,7 @@ void main() async { expect(result, 'Every weekday'); }); - testDescription("with other week days", (context) { + testDescription("returns correctly with other week days", (context) { alarm.setSettingWithoutNotify( "Week Days", [true, false, false, false, false, false, true]); From 4f24909560ec70d4e45bfd62180066c7fc0e4e62 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 15:59:13 +0500 Subject: [PATCH 150/177] Add background fetch --- lib/alarm/logic/alarm_isolate.dart | 16 ++++-- lib/audio/logic/ringtones.dart | 4 +- lib/debug/logic/logger.dart | 2 +- lib/debug/types/file_logger_output.dart | 8 +-- lib/main.dart | 24 +++----- lib/settings/screens/ringtones_screen.dart | 4 +- lib/system/logic/background_service.dart | 57 +++++++++++++++++++ .../logic/initialize_isolate_ports.dart | 22 +++++++ 8 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 lib/system/logic/background_service.dart create mode 100644 lib/system/logic/initialize_isolate_ports.dart diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index b5c0045c..6239e5f6 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:isolate'; import 'dart:ui'; @@ -29,7 +30,8 @@ void triggerScheduledNotification(int scheduleId, Json params) async { logger.f(details.exception.toString()); }; - logger.i("Alarm isolate triggered $scheduleId"); + logger.i( + "Alarm isolate triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); // print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}"); if (params == null) { logger.e("Params was null when triggering alarm"); @@ -52,9 +54,12 @@ void triggerScheduledNotification(int scheduleId, Json params) async { IsolateNameServer.registerPortWithName( receivePort.sendPort, stopAlarmPortName); receivePort.listen((message) { + logger.d("Received message: $message"); stopScheduledNotification(message); }); + // Isolate.current.addOnExitListener(receivePort.sendPort); + if (notificationType == ScheduledNotificationType.alarm) { triggerAlarm(scheduleId, params); } else if (notificationType == ScheduledNotificationType.timer) { @@ -75,6 +80,9 @@ void stopScheduledNotification(List message) { } else if (notificationType == ScheduledNotificationType.timer) { stopTimer(scheduleId, action); } + + logger.i( + "Alarm stop triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); } void triggerAlarm(int scheduleId, Json params) async { @@ -86,7 +94,7 @@ void triggerAlarm(int scheduleId, Json params) async { 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"); @@ -105,8 +113,7 @@ void triggerAlarm(int scheduleId, Json params) async { return; } if (alarm.currentScheduleDateTime == null) { - logger.i( - "Skipping alarm $scheduleId because it has no scheduled date"); + logger.i("Skipping alarm $scheduleId because it has no scheduled date"); return; } if (now.millisecondsSinceEpoch < @@ -136,7 +143,6 @@ void triggerAlarm(int scheduleId, Json params) async { 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 diff --git a/lib/audio/logic/ringtones.dart b/lib/audio/logic/ringtones.dart index 76743f91..a994014b 100644 --- a/lib/audio/logic/ringtones.dart +++ b/lib/audio/logic/ringtones.dart @@ -59,7 +59,7 @@ Future getRingtoneUri(FileItem fileItem) async { switch (fileItem.type) { case FileItemType.directory: try { - logger.t(fileItem.uri); + // logger.t(fileItem.uri); // logger.t( // await Directory(alarm.ringtone.uri).list(recursive: true).toList()); List? documentFiles = @@ -75,7 +75,7 @@ Future getRingtoneUri(FileItem fileItem) async { Random random = Random(); int index = random.nextInt(documentFiles.length); DocumentFile documentFile = documentFiles[index]; - logger.t("${documentFile.name} ${documentFile.uri}"); + // logger.t("${documentFile.name} ${documentFile.uri}"); return documentFile.uri; } else { logger.t( diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart index f0fad6f8..6c80b915 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -18,6 +18,6 @@ var logger = Logger( ); void printIsolateInfo() { - logger.i( + logger.t( "Isolate: ${Isolate.current.debugName}, id: ${Isolate.current.hashCode}"); } diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart index 4b3e363b..6da8f1b9 100644 --- a/lib/debug/types/file_logger_output.dart +++ b/lib/debug/types/file_logger_output.dart @@ -11,20 +11,20 @@ class FileLoggerOutput extends LogOutput { @override void output(OutputEvent event) { for (var line in event.lines) { + // ignore: avoid_print print(line); } String message = switch (event.origin.message.runtimeType) { - String => event.origin.message as String, - Exception => (event.origin.message as Exception).toString(), + const (String) => event.origin.message as String, + const (Exception) => (event.origin.message as Exception).toString(), _ => "Unknown error", }; if (!Platform.environment.containsKey('FLUTTER_TEST')) { _writeLog(message, event.level); - if (event.level == Level.error && - App.navigatorKey.currentContext != null) { + if (App.navigatorKey.currentContext != null) { Future(() { showSnackBar(App.navigatorKey.currentContext!, message, error: true, navBar: false, fab: false); diff --git a/lib/main.dart b/lib/main.dart index 1d9b101c..2b2337a9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,9 @@ import 'package:clock_app/settings/logic/initialize_settings.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/system/data/device_info.dart'; +import 'package:clock_app/system/logic/background_service.dart'; import 'package:clock_app/system/logic/handle_boot.dart'; +import 'package:clock_app/system/logic/initialize_isolate_ports.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_boot_receiver/flutter_boot_receiver.dart'; @@ -49,24 +51,14 @@ void main() async { await initializeStorage(); await initializeSettings(); - await updateAlarms("Update Alarms on Start"); - await updateTimers("Update Timers on Start"); + updateAlarms("Update Alarms on Start"); + updateTimers("Update Timers on Start"); AppVisibility.initialize(); initForegroundTask(); - - ReceivePort receivePort = ReceivePort(); - IsolateNameServer.removePortNameMapping(updatePortName); - IsolateNameServer.registerPortWithName(receivePort.sendPort, updatePortName); - printIsolateInfo(); - receivePort.listen((message) { - if (message == "updateAlarms") { - ListenerManager.notifyListeners("alarms"); - } else if (message == "updateTimers") { - ListenerManager.notifyListeners("timers"); - } else if (message == "updateStopwatches") { - ListenerManager.notifyListeners("stopwatch"); - } - }); + initBackgroundService(); + initializeIsolatePorts(); runApp(const App()); + + registerHeadlessBackgroundService(); } diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index 86cd7fb3..e3615d03 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -115,13 +115,13 @@ class _RingtonesScreenState extends State { await FilePicker.platform.getDirectoryPath(); if (selectedDirectory != null && selectedDirectory.isNotEmpty) { - logger.t("selectedDirectory: $selectedDirectory"); + // logger.t("selectedDirectory: $selectedDirectory"); final directory = Directory(selectedDirectory); final List entities = await directory.list().toList(); - logger.t(entities); + // logger.t(entities); String name = basename(selectedDirectory .replaceAll("%3A", "/") diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart new file mode 100644 index 00000000..2e543172 --- /dev/null +++ b/lib/system/logic/background_service.dart @@ -0,0 +1,57 @@ +import 'package:background_fetch/background_fetch.dart'; +import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/timer/logic/update_timers.dart'; + +Future initBackgroundService() async { + await BackgroundFetch.configure( + BackgroundFetchConfig( + minimumFetchInterval: 30, + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.NONE), (String taskId) async { + // <-- Event handler + // This is the fetch-event callback. + logger.i("[BackgroundFetch] Event received $taskId"); + + // await initializeIsolate(); + + await updateAlarms("initBackgroundService(): Update alarms in background service"); + await updateTimers("initBackgroundService(): Update timers in background service"); + // IMPORTANT: You must signal completion of your task or the OS can punish your app + // for taking too long in the background. + BackgroundFetch.finish(taskId); + }, (String taskId) async { + // <-- Task timeout handler. + // This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId) + logger.i("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + BackgroundFetch.finish(taskId); + }); +} + +// [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` +@pragma('vm:entry-point') +void handleBackgroundServiceTask(HeadlessTask task) async { + String taskId = task.taskId; + bool isTimeout = task.timeout; + if (isTimeout) { + // This task has exceeded its allowed running-time. + // You must stop what you're doing and immediately .finish(taskId) + logger.i("[BackgroundFetch] Headless task timed-out: $taskId"); + BackgroundFetch.finish(taskId); + return; + } + logger.i('[BackgroundFetch] Headless event received.'); + await updateAlarms("handleBackgroundServiceTask(): Update alarms in background service"); + await updateTimers("handleBackgroundServiceTask(): Update timers in background service"); + + BackgroundFetch.finish(taskId); +} + +void registerHeadlessBackgroundService() { + BackgroundFetch.registerHeadlessTask(handleBackgroundServiceTask); +} diff --git a/lib/system/logic/initialize_isolate_ports.dart b/lib/system/logic/initialize_isolate_ports.dart new file mode 100644 index 00000000..674501c2 --- /dev/null +++ b/lib/system/logic/initialize_isolate_ports.dart @@ -0,0 +1,22 @@ +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:clock_app/alarm/logic/alarm_isolate.dart'; +import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/settings/types/listener_manager.dart'; + +void initializeIsolatePorts(){ + ReceivePort receivePort = ReceivePort(); + IsolateNameServer.removePortNameMapping(updatePortName); + IsolateNameServer.registerPortWithName(receivePort.sendPort, updatePortName); + printIsolateInfo(); + receivePort.listen((message) { + if (message == "updateAlarms") { + ListenerManager.notifyListeners("alarms"); + } else if (message == "updateTimers") { + ListenerManager.notifyListeners("timers"); + } else if (message == "updateStopwatches") { + ListenerManager.notifyListeners("stopwatch"); + } + }); +} From af30caeacee5f3ad841e3d6fff9b06ab1edbe9a2 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 16:07:53 +0500 Subject: [PATCH 151/177] Update test workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5ee6c00f0a9bacb62a4a7c7640440cc899600332 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 16:13:31 +0500 Subject: [PATCH 152/177] Fix logger --- lib/debug/types/file_logger_output.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/debug/types/file_logger_output.dart b/lib/debug/types/file_logger_output.dart index 6da8f1b9..1fe95ef4 100644 --- a/lib/debug/types/file_logger_output.dart +++ b/lib/debug/types/file_logger_output.dart @@ -24,7 +24,8 @@ class FileLoggerOutput extends LogOutput { if (!Platform.environment.containsKey('FLUTTER_TEST')) { _writeLog(message, event.level); - if (App.navigatorKey.currentContext != null) { + if (event.level.value >= Level.error.value && + App.navigatorKey.currentContext != null) { Future(() { showSnackBar(App.navigatorKey.currentContext!, message, error: true, navBar: false, fab: false); From 120ee4cedf6275d9dd0b2b0fd08e614e24372fad Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 17:19:57 +0500 Subject: [PATCH 153/177] Fix nav bar colors --- lib/navigation/screens/nav_scaffold.dart | 40 ++++++++++++++++++------ lib/navigation/widgets/app_top_bar.dart | 27 ++++++++++------ lib/settings/screens/logs_screen.dart | 0 3 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 lib/settings/screens/logs_screen.dart diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index 0a113fb1..bbfceff9 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -4,6 +4,7 @@ import 'dart:isolate'; import 'package:clock_app/alarm/logic/new_alarm_snackbar.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/navigation/data/tabs.dart'; import 'package:clock_app/navigation/types/quick_action_controller.dart'; @@ -19,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:material_color_utilities/palettes/tonal_palette.dart'; import 'package:receive_intent/receive_intent.dart' as intent_handler; // The callback function should always be a top-level function. @@ -66,6 +68,7 @@ class _NavScaffoldState extends State { late Setting useMaterialNavBarSetting; late Setting swipeActionSetting; late Setting showForegroundSetting; + late Setting useMaterialYouSetting; late StreamSubscription _sub; late PageController _controller; QuickActionController quickActionController = QuickActionController(); @@ -160,6 +163,10 @@ class _NavScaffoldState extends State { .getGroup("Appearance") .getGroup("Style") .getSetting("Use Material Style"); + useMaterialYouSetting = appSettings + .getGroup("Appearance") + .getGroup("Colors") + .getSetting("Use Material You"); swipeActionSetting = appSettings.getGroup("General").getSetting("Swipe Action"); showForegroundSetting = appSettings @@ -168,6 +175,7 @@ class _NavScaffoldState extends State { .getSetting("Show Foreground Notification"); swipeActionSetting.addListener(update); useMaterialNavBarSetting.addListener(update); + useMaterialYouSetting.addListener(update); showForegroundSetting.addListener(_updateForegroundNotification); _controller = PageController(initialPage: widget.initialTabIndex); _selectedTabIndex = widget.initialTabIndex; @@ -178,6 +186,7 @@ class _NavScaffoldState extends State { @override void dispose() { useMaterialNavBarSetting.removeListener(update); + useMaterialYouSetting.removeListener(update); swipeActionSetting.removeListener(update); showForegroundSetting.removeListener(_updateForegroundNotification); _sub.cancel(); @@ -189,19 +198,32 @@ class _NavScaffoldState extends State { Widget build(BuildContext context) { Orientation orientation = MediaQuery.of(context).orientation; final tabs = getTabs(context, quickActionController); + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + ColorScheme colorScheme = theme.colorScheme; + + TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); + + Color materialNavColor = + useMaterialYouSetting.value + ? Color(tonalPalette + .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) + : colorScheme.surface; + return WithForegroundTask( child: Scaffold( appBar: orientation == Orientation.portrait ? AppTopBar( title: Text( tabs[_selectedTabIndex].title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme + style: textTheme.titleMedium?.copyWith( + color: colorScheme .onBackground .withOpacity(0.6), ), + ), + systemNavBarColor: useMaterialNavBarSetting.value ? materialNavColor : null, actions: [ IconButton( onPressed: () { @@ -214,8 +236,7 @@ class _NavScaffoldState extends State { }, icon: const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme + color: colorScheme .onBackground .withOpacity(0.8), ), @@ -228,6 +249,7 @@ class _NavScaffoldState extends State { labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, selectedIndex: _selectedTabIndex, + backgroundColor: materialNavColor, onDestinationSelected: _onTabSelected, destinations: [ for (final tab in tabs) @@ -257,9 +279,8 @@ class _NavScaffoldState extends State { ], leading: Text(tabs[_selectedTabIndex].title, style: - Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context) - .colorScheme + textTheme.headlineSmall?.copyWith( + color: colorScheme .onBackground .withOpacity(0.6), )), @@ -274,8 +295,7 @@ class _NavScaffoldState extends State { }, icon: const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: Theme.of(context) - .colorScheme + color: colorScheme .onBackground .withOpacity(0.8), ), diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index 530fb879..b4c3af8a 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -1,9 +1,11 @@ +import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final Widget? title; final List? actions; + final Color? systemNavBarColor; @override Size get preferredSize => const Size(0, 56); @@ -11,7 +13,7 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { const AppTopBar({ super.key, this.title, - this.actions, + this.actions, this.systemNavBarColor, }); @override @@ -20,8 +22,14 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; - Brightness iconBrightness = - colorScheme.background.computeLuminance() > 0.179 + final systemNavigationBarColor = systemNavBarColor ?? colorScheme.background; + + Brightness statusBarIconBrightness = + colorScheme.surface.computeLuminance() > 0.179 + ? Brightness.dark + : Brightness.light; + Brightness systemNavBarIconBrightness = + systemNavigationBarColor.computeLuminance() > 0.179 ? Brightness.dark : Brightness.light; @@ -31,11 +39,12 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: AppBar( systemOverlayStyle: SystemUiOverlayStyle( - systemNavigationBarColor: colorScheme.background, + systemNavigationBarColor: systemNavigationBarColor, systemNavigationBarDividerColor: Colors.transparent, - systemNavigationBarIconBrightness: iconBrightness, - statusBarColor: colorScheme.background, - statusBarIconBrightness: iconBrightness, // For Android (dark icons) + systemNavigationBarIconBrightness: systemNavBarIconBrightness, + statusBarColor: colorScheme.surface, + statusBarIconBrightness: + statusBarIconBrightness, // For Android (dark icons) ), scrolledUnderElevation: 0, toolbarHeight: preferredSize.height, @@ -43,10 +52,10 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { actions: [...?actions], elevation: 0, iconTheme: IconThemeData( - color: colorScheme.onBackground.withOpacity(0.8), + color: colorScheme.onSurface.withOpacity(0.8), ), titleTextStyle: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground, + color: colorScheme.onSurface, ), backgroundColor: Colors.transparent, ), diff --git a/lib/settings/screens/logs_screen.dart b/lib/settings/screens/logs_screen.dart new file mode 100644 index 00000000..e69de29b From b305b8148a8fb1b76434640239070266f715bd9c Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 10 Sep 2024 20:39:41 +0500 Subject: [PATCH 154/177] Add log screen --- lib/alarm/data/alarm_events_sort_options.dart | 27 +++ lib/alarm/logic/alarm_isolate.dart | 2 +- lib/alarm/screens/alarm_events_screen.dart | 2 + lib/alarm/widgets/alarm_event_card.dart | 59 +++--- lib/common/data/paths.dart | 4 + lib/common/utils/snackbar.dart | 14 +- lib/common/widgets/list/custom_list_view.dart | 32 +-- lib/common/widgets/list/list_filter_bar.dart | 17 +- lib/debug/data/log_list_filters.dart | 24 +++ lib/debug/data/log_sort_options.dart | 17 ++ lib/debug/logic/logger.dart | 3 +- lib/debug/screens/logs_screen.dart | 194 ++++++++++++++++++ lib/debug/types/log.dart | 68 ++++++ lib/debug/widgets/log_card.dart | 78 +++++++ lib/l10n/app_en.arb | 2 + lib/main.dart | 6 +- .../data/developer_settings_schema.dart | 39 +--- lib/settings/screens/logs_screen.dart | 0 lib/system/logic/background_service.dart | 22 +- lib/system/logic/handle_boot.dart | 6 + 20 files changed, 516 insertions(+), 100 deletions(-) create mode 100644 lib/alarm/data/alarm_events_sort_options.dart create mode 100644 lib/debug/data/log_list_filters.dart create mode 100644 lib/debug/data/log_sort_options.dart create mode 100644 lib/debug/screens/logs_screen.dart create mode 100644 lib/debug/types/log.dart create mode 100644 lib/debug/widgets/log_card.dart delete mode 100644 lib/settings/screens/logs_screen.dart 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/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 6239e5f6..8f297163 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -27,7 +27,7 @@ const String setAlarmVolumePortName = "setAlarmVolumePort"; @pragma('vm:entry-point') void triggerScheduledNotification(int scheduleId, Json params) async { FlutterError.onError = (FlutterErrorDetails details) { - logger.f(details.exception.toString()); + logger.f("Error in triggerScheduledNotification isolate: ${details.exception.toString()}"); }; logger.i( diff --git a/lib/alarm/screens/alarm_events_screen.dart b/lib/alarm/screens/alarm_events_screen.dart index 3222f224..1f2378e6 100644 --- a/lib/alarm/screens/alarm_events_screen.dart +++ b/lib/alarm/screens/alarm_events_screen.dart @@ -3,6 +3,7 @@ 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'; @@ -73,6 +74,7 @@ class _AlarmEventsScreenState extends State { placeholderText: "No alarm events", reloadOnPop: true, listFilters: alarmEventsListFilters, + sortOptions: alarmEventSortOptions, ), ), ], 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/common/data/paths.dart b/lib/common/data/paths.dart index d670eb47..b03dd29a 100644 --- a/lib/common/data/paths.dart +++ b/lib/common/data/paths.dart @@ -45,3 +45,7 @@ Future getTimezonesDatabasePath() async { Future getLogsFilePath() async { return path.join(await getAppDataDirectoryPath(), "logs.txt"); } + +String getLogsFilePathSync(){ + return path.join(getAppDataDirectoryPathSync(), "logs.txt"); +} diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index 7a8e4708..81cb801a 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -6,13 +6,18 @@ void showSnackBar(BuildContext context, String text, ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; Color? color = error ? colorScheme.error : null; + 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, color: color)); + ScaffoldMessenger.of(context).showSnackBar(getSnackbar(text, + fab: fab, navBar: navBar, color: color, duration: duration)); } SnackBar getSnackbar(String text, - {bool fab = false, bool navBar = false, Color? color}) { + {bool fab = false, + bool navBar = false, + Color? color, + Duration duration = const Duration(seconds: 4)}) { double left = 20; double right = 20; double bottom = 12; @@ -57,8 +62,9 @@ SnackBar getSnackbar(String text, right: right, bottom: bottom, ), - padding: EdgeInsets.zero, + padding: EdgeInsets.zero, elevation: 2, dismissDirection: DismissDirection.vertical, + duration: duration, ); } diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 1f978950..52547c5f 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -267,8 +267,10 @@ class _CustomListViewState void _handleCustomAction(ListFilterCustomAction action) { final list = _getActionableItems(); - List items = list.where((item) => - widget.listFilters.every((filter) => filter.filterFunction(item))).toList(); + List items = list + .where((item) => + widget.listFilters.every((filter) => filter.filterFunction(item))) + .toList(); action.action(items); _endSelection(); @@ -350,18 +352,20 @@ class _CustomListViewState crossAxisAlignment: CrossAxisAlignment.start, children: [ ListFilterBar( - listFilters: widget.listFilters, - customActions: widget.customActions, - sortOptions: widget.sortOptions, - isSelecting: _isSelecting, - handleCustomAction: _handleCustomAction, - handleEndSelection: _endSelection, - handleDeleteAction: _handleDeleteAction, - handleSelectAll: _handleSelectAll, - selectedIds: _selectedIds, - handleFilterChange: _handleFilterChange, - selectedSortIndex: _selectedSortIndex, - handleSortChange: _handleSortChange), + listFilters: widget.listFilters, + customActions: widget.customActions, + sortOptions: widget.sortOptions, + isSelecting: _isSelecting, + handleCustomAction: _handleCustomAction, + handleEndSelection: _endSelection, + handleDeleteAction: _handleDeleteAction, + handleSelectAll: _handleSelectAll, + selectedIds: _selectedIds, + handleFilterChange: _handleFilterChange, + selectedSortIndex: _selectedSortIndex, + handleSortChange: _handleSortChange, + isDeleteEnabled: widget.isDeleteEnabled, + ), if (widget.header != null) widget.header!, Expanded( flex: 1, diff --git a/lib/common/widgets/list/list_filter_bar.dart b/lib/common/widgets/list/list_filter_bar.dart index a6d87910..9f1a9ded 100644 --- a/lib/common/widgets/list/list_filter_bar.dart +++ b/lib/common/widgets/list/list_filter_bar.dart @@ -21,12 +21,14 @@ class ListFilterBar extends StatelessWidget { required this.selectedIds, required this.handleFilterChange, required this.selectedSortIndex, - required this.handleSortChange}); + required this.handleSortChange, + required this.isDeleteEnabled}); final List> listFilters; final List> customActions; final List> sortOptions; final bool isSelecting; + final bool isDeleteEnabled; final Function(ListFilterCustomAction) handleCustomAction; final Function handleEndSelection; final void Function() handleFilterChange; @@ -67,12 +69,13 @@ class ListFilterBar extends StatelessWidget { action: () => handleCustomAction(action), ), ), - ListFilterAction( - name: AppLocalizations.of(context)!.deleteAllFilteredAction, - icon: Icons.delete_rounded, - color: colorScheme.error, - action: handleDeleteAction, - ) + if (isDeleteEnabled) + ListFilterAction( + name: AppLocalizations.of(context)!.deleteAllFilteredAction, + icon: Icons.delete_rounded, + color: colorScheme.error, + action: handleDeleteAction, + ) ], activeFilterCount: activeFilterCount + (isSelecting ? 1 : 0), ), diff --git a/lib/debug/data/log_list_filters.dart b/lib/debug/data/log_list_filters.dart new file mode 100644 index 00000000..4af09485 --- /dev/null +++ b/lib/debug/data/log_list_filters.dart @@ -0,0 +1,24 @@ +import 'package:clock_app/alarm/types/alarm_event.dart'; +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/utils/date_time.dart'; +import 'package:clock_app/debug/types/log.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logger/logger.dart'; + +final List> logListFilters = [ + ListFilterSelect((context) => AppLocalizations.of(context)!.dateFilterGroup, [ + ListFilter((context) => AppLocalizations.of(context)!.todayFilter, + (log) => log.dateTime.isToday()), + ListFilter((context) => AppLocalizations.of(context)!.tomorrowFilter, + (log) => log.dateTime.isTomorrow()), + ]), + ListFilterMultiSelect( + (context) => AppLocalizations.of(context)!.logTypeFilterGroup, [ + ListFilter((context) => "Debug", (log) => log.level == Level.debug), + ListFilter((context) => "Trace", (log) => log.level == Level.trace), + ListFilter((context) => "Info", (log) => log.level == Level.info), + ListFilter((context) => "Warning", (log) => log.level == Level.warning), + ListFilter((context) => "Error", (log) => log.level == Level.error), + ListFilter((context) => "Fatal", (log) => log.level == Level.fatal), + ]), +]; diff --git a/lib/debug/data/log_sort_options.dart b/lib/debug/data/log_sort_options.dart new file mode 100644 index 00000000..d5a860ec --- /dev/null +++ b/lib/debug/data/log_sort_options.dart @@ -0,0 +1,17 @@ +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/debug/types/log.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +final List> logSortOptions = [ + ListSortOption((context) => "Earlies first", sortDateAscending), + ListSortOption((context) => "Latest first", sortDateDescending), +]; + +int sortDateAscending(Log a, Log b) { + return a.id.compareTo(b.id); +} + +int sortDateDescending(Log a, Log b) { + return b.id.compareTo(a.id); +} + diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart index 6c80b915..8d66a7a9 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -7,7 +7,8 @@ var logger = Logger( filter: FileLogFilter(), output: FileLoggerOutput(), printer: PrettyPrinter( - methodCount: 0, // Number of method calls to be displayed + methodCount: 100, // Number of method calls to be displayed + errorMethodCount: 8, // Number of method calls if stacktrace is provided lineLength: 80, // Width of the output colors: true, // Colorful log messages diff --git a/lib/debug/screens/logs_screen.dart b/lib/debug/screens/logs_screen.dart new file mode 100644 index 00000000..96bee61d --- /dev/null +++ b/lib/debug/screens/logs_screen.dart @@ -0,0 +1,194 @@ +import 'dart:io'; + +import 'package:clock_app/alarm/data/alarm_events_list_filters.dart'; +import 'package:clock_app/common/data/paths.dart'; +import 'package:clock_app/common/types/list_controller.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/fab.dart'; +import 'package:clock_app/common/widgets/list/custom_list_view.dart'; +import 'package:clock_app/debug/data/log_list_filters.dart'; +import 'package:clock_app/debug/data/log_sort_options.dart'; +import 'package:clock_app/debug/types/log.dart'; +import 'package:clock_app/debug/widgets/log_card.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:flutter/material.dart'; +import 'package:pick_or_save/pick_or_save.dart'; + +class LogsScreen extends StatefulWidget { + const LogsScreen({ + super.key, + }); + + @override + State createState() => _LogsScreenState(); +} + +class _LogsScreenState extends State { + List _logs = []; + final _listController = ListController(); + + List _mergeMultilineLogs(List logLines) { + final mergedLogs = []; + final buffer = StringBuffer(); + + for (var line in logLines) { + if (line.startsWith('[')) { + if (buffer.isNotEmpty) { + mergedLogs.add(buffer.toString()); + buffer.clear(); + } + buffer.writeln(line.trim()); + } else { + buffer.writeln(line.trim()); + } + } + + if (buffer.isNotEmpty) { + mergedLogs.add(buffer.toString()); + } + + // remove new lines from the end of the logs + for (var i = 0; i < mergedLogs.length; i++) { + mergedLogs[i] = mergedLogs[i].trim(); + } + + return mergedLogs; + } + + @override + void initState() { + final File file = File(getLogsFilePathSync()); + final content = file.readAsLinesSync(); + final logLines = _mergeMultilineLogs(content); + + for (int i = 0; i < logLines.length; i++) { + _logs.add(Log.fromLine(logLines[i], i)); + } + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + + return Scaffold( + appBar: AppTopBar(title: Text("App Logs", style: textTheme.titleMedium)), + body: Stack( + children: [ + Column( + children: [ + Expanded( + flex: 1, + child: CustomListView( + items: _logs, + listController: _listController, + itemBuilder: (log) => LogCard( + key: ValueKey(log), + log: log, + ), + // onTapItem: (fileItem, index) { + // // widget.setting.setValue(context, themeItem); + // // _listController.reload(); + // }, + // onDeleteItem: (event){}, + isDuplicateEnabled: false, + isReorderable: false, + isDeleteEnabled: false, + // isDeleteEnabled: true, + placeholderText: "No logs", + listFilters: logListFilters, + sortOptions: logSortOptions, + // reloadOnPop: true, + // listFilters: alarmEventsListFilters, + ), + ), + ], + ), + FAB( + icon: Icons.delete_rounded, + bottomPadding: 8, + onPressed: () async { + final File file = File(await getLogsFilePath()); + + await file.writeAsString(""); + + if (context.mounted) showSnackBar(context, "Logs cleared"); + + _listController.clearItems(); + }, + ), + FAB( + index: 1, + icon: Icons.file_download, + bottomPadding: 8, + onPressed: () async { + final File file = File(await getLogsFilePath()); + + if (!(await file.exists())) { + await file.create(recursive: true); + } + + final result = await PickOrSave().fileSaver( + params: FileSaverParams( + saveFiles: [ + SaveFileInfo( + fileData: await file.readAsBytes(), + fileName: + "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", + ) + ], + )); + if (result != null) { + if (context.mounted) { + showSnackBar(context, "Logs saved to device"); + } + } + }), + // 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); + // } + // } + // }), + + // FAB( + // index: 1, + // icon: Icons.folder_rounded, + // bottomPadding: 8, + // onPressed: () async { + // // Item? themeItem = widget.createThemeItem(); + // // await _openCustomizeItemScreen( + // // themeItem, + // // onSave: (newThemeItem) { + // // _listController.addItem(newThemeItem); + // // }, + // // isNewItem: true, + // // ); + // }, + // ) + ], + ), + ); + } +} diff --git a/lib/debug/types/log.dart b/lib/debug/types/log.dart new file mode 100644 index 00000000..468aae1a --- /dev/null +++ b/lib/debug/types/log.dart @@ -0,0 +1,68 @@ +import 'package:clock_app/common/types/json.dart'; +import 'package:clock_app/common/types/list_item.dart'; +import 'package:clock_app/common/utils/id.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; + +class Log extends ListItem { + @override + final int id; + late String message; + late DateTime dateTime; + late Level level; + + Log( + this.id, + this.message, + this.dateTime, + this.level, + ); + + Log.fromLine(String line, int index) : id = index { + final regex = RegExp( + r'\[(\d+-\d+-\d+)\s\|\s(\d+:\d+:\d+)\]\s\[(\w+)\]\s(.*)', + dotAll: true); + final match = regex.firstMatch(line); + + if (match != null) { + final datePart = match.group(1)!; + final timePart = match.group(2)!; + level = Level.values.byName(match.group(3)!); + message = match.group(4)!; + + final dateTimeStr = '$datePart $timePart'; + final dateFormat = DateFormat('d-M-yyyy HH:mm:ss'); + dateTime = dateFormat.parse(dateTimeStr); + } else { + message = "Cannot read log"; + level = Level.off; + dateTime = DateTime.now(); + throw const FormatException('Invalid log format'); + } + } + + @override + copy() { + return Log(id,message, dateTime, level); + } + + @override + void copyFrom(other) { + message = other.message; + dateTime = other.timestamp; + level = other.level; + } + + @override + bool get isDeletable => false; + + @override + Json? toJson() { + return { + "id": id, + "message": message, + "timestamp": dateTime.toIso8601String(), + "level": level.toString(), + }; + } +} diff --git a/lib/debug/widgets/log_card.dart b/lib/debug/widgets/log_card.dart new file mode 100644 index 00000000..7977bcd0 --- /dev/null +++ b/lib/debug/widgets/log_card.dart @@ -0,0 +1,78 @@ +import 'package:clock_app/debug/types/log.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; + +class LogCard extends StatefulWidget { + const LogCard({ + super.key, + required this.log, + }); + + final Log log; + + @override + State createState() => _LogCardState(); +} + +class LevelColor { + Color backgroundColor; + Color textColor; + + LevelColor(this.backgroundColor, this.textColor); +} + +Map levelColors = { + Level.debug: LevelColor(Colors.brown, Colors.white), + Level.trace: LevelColor(Colors.grey, Colors.white), + Level.info: LevelColor(Colors.blue, Colors.white), + Level.warning: LevelColor(Colors.orange, Colors.white), + Level.error: LevelColor(Colors.red, Colors.white), + Level.fatal: LevelColor(Colors.purple, Colors.white), +}; + +class _LogCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: + const EdgeInsets.only(left: 16.0, right: 16.0, top: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: levelColors[widget.log.level]?.backgroundColor, + ), + child: Text(widget.log.level.name, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: levelColors[widget.log.level]?.textColor, + )), + ), + const SizedBox( + width: 8, + ), + Text( + DateFormat('yyyy-MM-dd | kk:mm:ss') + .format(widget.log.dateTime), + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), + ], + ), + Text( + widget.log.message, + style: Theme.of(context).textTheme.bodyMedium, + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // softWrap: false, + ), + ], + )); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4214c373..46c37312 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -169,6 +169,8 @@ "@maxLogsSetting": {}, "alarmLogSetting": "Alarm logs", "@alarmLogSetting": {}, + "appLogs": "App logs", + "@appLogs": {}, "saveLogs": "Save logs", "@saveLogs": {}, "showErrorSnackbars": "Show error snackbars", diff --git a/lib/main.dart b/lib/main.dart index 2b2337a9..358d5cce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,9 +27,9 @@ import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { - FlutterError.onError = (FlutterErrorDetails details) { - logger.f(details.exception.toString()); - }; + // FlutterError.onError = (FlutterErrorDetails details) { + // logger.f(details.exception.toString()); + // }; WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/settings/data/developer_settings_schema.dart b/lib/settings/data/developer_settings_schema.dart index 28285fd3..81a16e2f 100644 --- a/lib/settings/data/developer_settings_schema.dart +++ b/lib/settings/data/developer_settings_schema.dart @@ -1,16 +1,13 @@ import 'dart:io'; import 'package:clock_app/alarm/screens/alarm_events_screen.dart'; -import 'package:clock_app/common/data/paths.dart'; -import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/debug/screens/logs_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; -import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:pick_or_save/pick_or_save.dart'; SettingGroup developerSettingsSchema = SettingGroup( "Developer Options", @@ -40,36 +37,12 @@ SettingGroup developerSettingsSchema = SettingGroup( "alarm_logs", (context) => AppLocalizations.of(context)!.alarmLogSetting, const AlarmEventsScreen()), - SettingAction( - "save_logs", (context) => AppLocalizations.of(context)!.saveLogs, - (context) async { - final File file = File(await getLogsFilePath()); + SettingPageLink( + "app_logs", + (context) => AppLocalizations.of(context)!.appLogs, + const LogsScreen()), - if(!(await file.exists())) { - await file.create(recursive: true); - } - - await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: await file.readAsBytes(), - fileName: - "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", - ) - ], - )); - }), - SettingAction( - "clear_logs", (context) => AppLocalizations.of(context)!.clearLogs, - (context) async { - final File file = File(await getLogsFilePath()); - - await file.writeAsString(""); - - if(context.mounted) showSnackBar(context, "Logs cleared"); - }) - ]), + ]), ], icon: Icons.code_rounded, ); diff --git a/lib/settings/screens/logs_screen.dart b/lib/settings/screens/logs_screen.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart index 2e543172..cf59083c 100644 --- a/lib/system/logic/background_service.dart +++ b/lib/system/logic/background_service.dart @@ -1,7 +1,9 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:flutter/material.dart'; Future initBackgroundService() async { await BackgroundFetch.configure( @@ -20,8 +22,10 @@ Future initBackgroundService() async { // await initializeIsolate(); - await updateAlarms("initBackgroundService(): Update alarms in background service"); - await updateTimers("initBackgroundService(): Update timers in background service"); + await updateAlarms( + "initBackgroundService(): Update alarms in background service"); + await updateTimers( + "initBackgroundService(): Update timers in background service"); // IMPORTANT: You must signal completion of your task or the OS can punish your app // for taking too long in the background. BackgroundFetch.finish(taskId); @@ -36,18 +40,24 @@ Future initBackgroundService() async { // [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` @pragma('vm:entry-point') void handleBackgroundServiceTask(HeadlessTask task) async { + FlutterError.onError = (FlutterErrorDetails details) { + logger.f("Error in handleBackgroundServiceTask isolate: ${details.exception.toString()}"); + }; String taskId = task.taskId; bool isTimeout = task.timeout; if (isTimeout) { - // This task has exceeded its allowed running-time. + // This task has exceeded its allowed running-time. // You must stop what you're doing and immediately .finish(taskId) logger.i("[BackgroundFetch] Headless task timed-out: $taskId"); BackgroundFetch.finish(taskId); return; - } + } + await initializeIsolate(); logger.i('[BackgroundFetch] Headless event received.'); - await updateAlarms("handleBackgroundServiceTask(): Update alarms in background service"); - await updateTimers("handleBackgroundServiceTask(): Update timers in background service"); + await updateAlarms( + "handleBackgroundServiceTask(): Update alarms in background service"); + await updateTimers( + "handleBackgroundServiceTask(): Update timers in background service"); BackgroundFetch.finish(taskId); } diff --git a/lib/system/logic/handle_boot.dart b/lib/system/logic/handle_boot.dart index 742c589b..a994db9a 100644 --- a/lib/system/logic/handle_boot.dart +++ b/lib/system/logic/handle_boot.dart @@ -1,6 +1,8 @@ import 'package:clock_app/alarm/logic/update_alarms.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; +import 'package:flutter/material.dart'; @pragma('vm:entry-point') void handleBoot() async { @@ -11,6 +13,10 @@ void handleBoot() async { // File('$appDataDirectory/log-dart.txt') // .writeAsStringSync(message, mode: FileMode.append); // + FlutterError.onError = (FlutterErrorDetails details) { + logger.f("Error in handleBoot isolate: ${details.exception.toString()}"); + }; + await initializeIsolate(); await updateAlarms("handleBoot(): Update alarms on system boot"); From 5d9a9fbe6b04b19f9c1318b60d281fbf87d9b559 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Wed, 11 Sep 2024 23:43:44 +0500 Subject: [PATCH 155/177] Add option to pick directories --- android/app/build.gradle | 6 + android/app/src/main/AndroidManifest.xml | 2 + .../kotlin/com/vicolo/chrono/MainActivity.kt | 138 +++++++++--- lib/alarm/screens/alarm_events_screen.dart | 62 +++--- lib/audio/logic/ringtones.dart | 30 ++- lib/audio/types/ringtone_player.dart | 3 - lib/debug/screens/logs_screen.dart | 37 ++-- lib/settings/logic/backup.dart | 30 +-- lib/settings/screens/backup_screen.dart | 14 +- lib/settings/screens/ringtones_screen.dart | 86 +++++--- lib/system/types/android_platform_file.dart | 205 +++++++++--------- pubspec.lock | 16 +- pubspec.yaml | 2 +- 13 files changed, 366 insertions(+), 265 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 08ef8d71..0588e16c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -115,6 +115,12 @@ android { } } +dependencies { + implementation "androidx.documentfile:documentfile:1.0.1" +} + + + flutter { source '../..' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0b7fe27d..161e27e9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ + + + when (call.method) { + "getDirectoryPath" -> { + this.result = result + val intent = Intent(this, DirectoryPickerActivity::class.java) + startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE) + } + "listDirectories" -> { + val uriString = call.argument("uri")!! + val uri = Uri.parse(uriString) + val directories = listDirectories(uri) + result.success(directories) + } + "listFiles" -> { + val uriString = call.argument("uri")!! + val uri = Uri.parse(uriString) + val files = listFiles(uri) + result.success(files) + } + "getFileChunk" -> { + val uriString = call.argument("uri")!! + val offset = call.argument("offset")!! + val chunkSize = call.argument("chunkSize")!! + val uri = Uri.parse(uriString) + val chunk = readFileChunk(uri, offset, chunkSize) + if (chunk != null) { + result.success(chunk) + } else { + result.error("READ_ERROR", "Failed to read file chunk", null) + } + } + else -> result.notImplemented() + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + val pickedDirectoryUri = data?.getStringExtra("getDirectoryUri") + if (pickedDirectoryUri != null) { + result.success(pickedDirectoryUri) + } else { + result.error("NO_DIRECTORY_URI", "No directory URI found", null) + } + } + } -class MainActivity: FlutterActivity() { - private val CHANNEL = "com.vicolo.chrono/alarm" - - // // create static method channel - // companion object { - // lateinit var channel: MethodChannel - // } + private fun listDirectories(uri: Uri): List { + val directory = DocumentFile.fromTreeUri(this, uri) + val directories = mutableListOf() - // override fun onCreate(savedInstanceState: Bundle?) { - // super.onCreate(savedInstanceState) - // // MethodChannelHolder.init(flutterView) - // // MethodChannelHolder.invokeMethod("onBoot") - // } + if (directory != null && directory.isDirectory) { + for (item in directory.listFiles()) { + if (item.isDirectory) { + directories.add(item.uri.toString()) + } + } + } - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - // GeneratedPluginRegistrant.registerWith(flutterEngine) - // flutterEngine.plugins.add(InitiateCallsToDartInBgPlugin()) - + return directories + } + + private fun listFiles(uri: Uri): List> { + val directory = DocumentFile.fromTreeUri(this, uri) + val files = mutableListOf>() + + if (directory != null && directory.isDirectory) { + for (item in directory.listFiles()) { + if (item.isFile) { + val fileInfo: Map = mapOf( + "uri" to item.uri.toString(), + "name" to (item.name ?: "Unknown"), + "size" to item.length(), + "modified" to item.lastModified() + ) + files.add(fileInfo) + } + } + } + + return files + } + + private fun readFileChunk(uri: Uri, offset: Int, chunkSize: Int): ByteArray? { + return try { + contentResolver.openInputStream(uri)?.use { inputStream -> + inputStream.skip(offset.toLong()) + val buffer = ByteArray(chunkSize) + val bytesRead = inputStream.read(buffer, 0, chunkSize) + if (bytesRead != -1) { + buffer.copyOf(bytesRead) + } else { + null + } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + companion object { + const val PICK_DIRECTORY_REQUEST_CODE = 1 } } diff --git a/lib/alarm/screens/alarm_events_screen.dart b/lib/alarm/screens/alarm_events_screen.dart index 1f2378e6..f71edde2 100644 --- a/lib/alarm/screens/alarm_events_screen.dart +++ b/lib/alarm/screens/alarm_events_screen.dart @@ -10,10 +10,11 @@ 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/debug/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({ @@ -24,8 +25,6 @@ class AlarmEventsScreen extends StatefulWidget { State createState() => _AlarmEventsScreenState(); } - - class _AlarmEventsScreenState extends State { final _listController = PersistentListController(); List searchedItems = []; @@ -60,8 +59,6 @@ class _AlarmEventsScreenState extends State { itemBuilder: (event) => AlarmEventCard( key: ValueKey(event), event: event, - - ), // onTapItem: (fileItem, index) { // // widget.setting.setValue(context, themeItem); @@ -88,40 +85,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/audio/logic/ringtones.dart b/lib/audio/logic/ringtones.dart index a994014b..e56d6cba 100644 --- a/lib/audio/logic/ringtones.dart +++ b/lib/audio/logic/ringtones.dart @@ -7,8 +7,8 @@ import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/debug/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'; -import 'package:pick_or_save/pick_or_save.dart'; Future> getSystemRingtones() async { final ringtones = (await FlutterSystemRingtones.getAlarmSounds()) @@ -55,28 +55,26 @@ Future> getNRandomRingtoneIndices(int n) async { return indices; } +const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + Future getRingtoneUri(FileItem fileItem) async { switch (fileItem.type) { case FileItemType.directory: try { - // logger.t(fileItem.uri); - // logger.t( - // await Directory(alarm.ringtone.uri).list(recursive: true).toList()); - List? documentFiles = - await PickOrSave().directoryDocumentsPicker( - params: DirectoryDocumentsPickerParams( - directoryUri: fileItem.uri, - // recurseDirectories: true, - mimeTypesFilter: ["audio/*"], - ), - ); - if (documentFiles != null && documentFiles.isNotEmpty) { + 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(documentFiles.length); - DocumentFile documentFile = documentFiles[index]; + int index = random.nextInt(audioFiles.length); + FileSystemEntity documentFile = audioFiles[index]; // logger.t("${documentFile.name} ${documentFile.uri}"); - return documentFile.uri; + return documentFile.uri.toString(); } else { logger.t( "No audio files found in directory ${fileItem.uri}, using default"); diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index 36c82b08..8d1314c5 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -4,12 +4,9 @@ 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/common/types/file_item.dart'; -import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/timer/types/timer.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:pick_or_save/pick_or_save.dart'; import 'package:vibration/vibration.dart'; Random random = Random(); diff --git a/lib/debug/screens/logs_screen.dart b/lib/debug/screens/logs_screen.dart index 96bee61d..99941163 100644 --- a/lib/debug/screens/logs_screen.dart +++ b/lib/debug/screens/logs_screen.dart @@ -8,12 +8,13 @@ import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; import 'package:clock_app/debug/data/log_list_filters.dart'; import 'package:clock_app/debug/data/log_sort_options.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/debug/types/log.dart'; import 'package:clock_app/debug/widgets/log_card.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 LogsScreen extends StatefulWidget { const LogsScreen({ @@ -130,26 +131,24 @@ class _LogsScreenState extends State { icon: Icons.file_download, bottomPadding: 8, onPressed: () async { - final File file = File(await getLogsFilePath()); + try { + final File file = File(await getLogsFilePath()); - if (!(await file.exists())) { - await file.create(recursive: true); - } - - final result = await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: await file.readAsBytes(), - fileName: - "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", - ) - ], - )); - if (result != null) { - if (context.mounted) { - showSnackBar(context, "Logs saved to device"); + if (!(await file.exists())) { + await file.create(recursive: true); + } + final result = await FilePicker.platform.saveFile( + bytes: await file.readAsBytes(), + fileName: + "chrono_logs_${DateTime.now().toIso8601String().split(".")[0]}.txt", + ); + if (result != null) { + if (context.mounted) { + showSnackBar(context, "Logs saved to device"); + } } + } catch (e) { + logger.e("Error saving logs file: ${e.toString()}"); } }), // FAB( diff --git a/lib/settings/logic/backup.dart b/lib/settings/logic/backup.dart index aad286fc..567964c5 100644 --- a/lib/settings/logic/backup.dart +++ b/lib/settings/logic/backup.dart @@ -2,28 +2,22 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:pick_or_save/pick_or_save.dart'; +import 'package:file_picker/file_picker.dart'; -Future?> saveBackupFile(String data) async { - return await PickOrSave().fileSaver( - params: FileSaverParams( - saveFiles: [ - SaveFileInfo( - fileData: Uint8List.fromList(utf8.encode(data)), - fileName: "chrono_backup_${DateTime.now().toIso8601String().split(".")[0]}.json", - ) - ], - )); +Future saveBackupFile(String data) async { + return await FilePicker.platform.saveFile( + bytes: Uint8List.fromList(utf8.encode(data)), + fileName: + "chrono_backup_${DateTime.now().toIso8601String().split(".")[0]}.json", + ); } Future loadBackupFile() async { - List? result = await PickOrSave().filePicker( - params: FilePickerParams( - getCachedFilePath: true, - ), - ); - if (result != null && result.isNotEmpty) { - File file = File(result[0]); + FilePickerResult? result = await FilePicker.platform + .pickFiles(type: FileType.any, allowMultiple: false); + + if (result != null && result.files.isNotEmpty) { + File file = File(result.files.single.path!); return utf8.decode(file.readAsBytesSync()); } return null; diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index 29a48a44..368bdaee 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -75,13 +75,10 @@ class _BackupExportScreenState extends State { if (result == null) return; if (context.mounted) { showSnackBar(context, "Export successful!"); + Navigator.pop(context); } } catch (e) { - logger.e(e.toString()); - if (context.mounted) { - showSnackBar(context, "Error exporting: ${e.toString()}", - error: true); - } + logger.e("Error exporting: $e"); } }, child: @@ -163,13 +160,10 @@ class _BackupImportScreenState extends State { } if (context.mounted) { showSnackBar(context, "Import successful!"); + Navigator.pop(context); } } catch (e) { - logger.e(e.toString()); - if (context.mounted) { - showSnackBar(context, "Error importing: ${e.toString()}", - error: true); - } + logger.e("Error importing: $e"); } }, child: diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index e3615d03..3af880b0 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -6,14 +6,14 @@ 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/debug/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/settings/widgets/settings_top_bar.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:pick_or_save/pick_or_save.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:permission_handler/permission_handler.dart'; class RingtonesScreen extends StatefulWidget { const RingtonesScreen({ @@ -85,50 +85,68 @@ class _RingtonesScreenState extends State { ], ), FAB( - icon: Icons.music_note_rounded, - bottomPadding: 8, - onPressed: () async { - RingtonePlayer.stop(); - FilePickerResult? result = await FilePicker.platform - .pickFiles(type: FileType.audio, allowMultiple: true); + 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); + // 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 { + if (androidInfo!.version.sdkInt >= 33) { + if (!await Permission.audio.isGranted) { + await Permission.audio.request(); + } + } else { + if (!await Permission.storage.isGranted) { + await Permission.storage.request(); + } + } + RingtonePlayer.stop(); - String? selectedDirectory = - await FilePicker.platform.getDirectoryPath(); + try { + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath(); - if (selectedDirectory != null && selectedDirectory.isNotEmpty) { - // logger.t("selectedDirectory: $selectedDirectory"); + if (selectedDirectory != null && selectedDirectory.isNotEmpty) { + // logger.t("selectedDirectory: $selectedDirectory"); - final directory = Directory(selectedDirectory); - final List entities = - await directory.list().toList(); + // final directory = Directory(selectedDirectory); + // final List entities = + // await directory.list().toList(); - // logger.t(entities); + // logger.t(entities); - String name = basename(selectedDirectory - .replaceAll("%3A", "/") - .replaceAll("%2F", "/")); - final fileItem = - FileItem(name, selectedDirectory, FileItemType.directory); - _listController.addItem(fileItem); + 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/system/types/android_platform_file.dart b/lib/system/types/android_platform_file.dart index ccc78b25..4fd971d9 100644 --- a/lib/system/types/android_platform_file.dart +++ b/lib/system/types/android_platform_file.dart @@ -1,97 +1,108 @@ -// import 'dart:async'; -// import 'dart:io'; -// -// import 'package:file_picker/file_picker.dart'; -// import 'package:flutter/foundation.dart'; -// import 'package:flutter/services.dart'; -// -// /// Android-specific. uses a platform channel which calls Scoped Storage APIs -// /// so that we can list directory contents and read files from a selected folder -// class AndroidPlatformFile { -// final File file; -// -// AndroidPlatformFile(this.file); -// -// static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); -// -// Stream> openRead() async* { -// const int chunkSize = 1024 * 1024; // 1MB -// int offset = 0; -// bool moreData = true; -// -// while (moreData) { -// try { -// final Map arguments = { -// 'uri': file.uri, -// 'offset': offset, -// 'chunkSize': chunkSize, -// }; -// final List? chunk = await methodChannel.invokeMethod('getFileChunk', arguments); -// if (chunk == null || chunk.isEmpty) { -// moreData = false; -// } else { -// yield Uint8List.fromList(chunk); -// offset += chunk.length; -// } -// } on PlatformException catch (e) { -// if (kDebugMode) print("Failed to get file chunk: ${e.message}"); -// moreData = false; -// } -// } -// } -// } -// -// -// class AndroidPlatformFolder { -// final String? path; -// -// AndroidPlatformFolder({this.path}); -// -// -// -// static const methodChannel = MethodChannel('com.yourCompany.app/documents'); -// -// dispose() { -// if (Platform.isIOS) { -// methodChannel.invokeMethod('stopAccessingSecurityScopedResource'); -// } -// } -// -// Future> files() async { -// if (path != null) { -// final directory = Directory(path!); -// return (await directory.list().toList()) -// .whereType() -// .map((file) => AndroidPlatformFile(file)) -// .toList(); -// } else { -// // Android -// return (await methodChannel.invokeMethod('listFiles', { 'uri': uri! })) -// .map((file) => AndroidPlatformFile(uri: file['uri'], name: file['name'], size: file['size'], modifiedDate: DateTime.fromMillisecondsSinceEpoch(file['modified']))) -// .cast() -// .toList(); -// } -// } -// -// -// Future> folders() async { -// if (path != null) { -// final directory = Directory(path!); -// return (await directory.list().toList()) -// .whereType() -// .map((directory) => PlatformFile(path: directory.path)) -// .toList(); -// } else { -// // Android -// return (await methodChannel.invokeMethod('listDirectories', { 'uri': uri! })) -// .map((folder) => AndroidPlatformFolder(uri: folder)) -// .cast() -// .toList(); -// } -// } -// -// String get name { -// if (path != null) return p.basename(path!); -// return Uri.decodeFull(uri!).split(RegExp(r'[/:]')).last; // Android -// } -// } +import 'dart:async'; +import 'dart:io'; + +import 'package:clock_app/debug/logic/logger.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart'; + +/// Android-specific. uses a platform channel which calls Scoped Storage APIs +/// so that we can list directory contents and read files from a selected folder +class AndroidFile { + String uri; + String name; + int size; + DateTime modifiedDate; + + AndroidFile( + {required this.uri, + required this.name, + required this.size, + required this.modifiedDate}); + AndroidFile.fromFile(File file) + : this( + uri: file.path, + name: file.path.split('/').last, + size: file.lengthSync(), + modifiedDate: file.lastModifiedSync()); + + static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + + Stream> openRead() async* { + const int chunkSize = 1024 * 1024; // 1MB + int offset = 0; + bool moreData = true; + + while (moreData) { + try { + final Map arguments = { + 'uri': uri, + 'offset': offset, + 'chunkSize': chunkSize, + }; + final List? chunk = + await methodChannel.invokeMethod('getFileChunk', arguments); + if (chunk == null || chunk.isEmpty) { + moreData = false; + } else { + yield Uint8List.fromList(chunk); + offset += chunk.length; + } + } on PlatformException catch (e) { + logger.e("Failed to get file chunk: ${e.message}"); + moreData = false; + } + } + } +} + +class AndroidFolder { + final String? uri; + final String? path; + + AndroidFolder({this.uri, this.path}); + + static const methodChannel = MethodChannel('com.vicolo.chrono/documents'); + + Future> files() async { + if (path != null) { + final directory = Directory(path!); + return (await directory.list().toList()) + .whereType() + .map((file) => AndroidFile.fromFile(file)) + .toList(); + } else { + // Android + return (await methodChannel.invokeMethod('listFiles', {'uri': uri!})) + .map((file) => AndroidFile( + uri: file['uri'], + name: file['name'], + size: file['size'], + modifiedDate: + DateTime.fromMillisecondsSinceEpoch(file['modified']))) + .cast() + .toList(); + } + } + + Future> folders() async { + if (path != null) { + final directory = Directory(uri!); + return (await directory.list().toList()) + .whereType() + .map((directory) => AndroidFolder(uri: directory.path)) + .toList(); + } else { + // Android + return (await methodChannel + .invokeMethod('listDirectories', {'uri': uri!})) + .map((folder) => AndroidFolder(uri: folder)) + .cast() + .toList(); + } + } + + String get name { + if (path != null) return basename(path!); + return Uri.decodeFull(uri!).split(RegExp(r'[/:]')).last; // Android + } +} diff --git a/pubspec.lock b/pubspec.lock index 990ea0e9..8bf03457 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -706,6 +706,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.0" + mime: + dependency: "direct main" + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" move_to_background: dependency: "direct main" description: @@ -850,14 +858,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" - pick_or_save: - dependency: "direct main" - description: - name: pick_or_save - sha256: "5e562e714e8486000b1144e580dfbd6db888a0b4dd02bf4c28501b244dd22fd3" - url: "https://pub.dev" - source: hosted - version: "2.2.4" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 62f34a66..6b5b840c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,6 @@ dependencies: introduction_screen: ^3.1.12 app_settings: ^5.1.1 auto_start_flutter: ^0.1.1 - pick_or_save: ^2.2.4 package_info_plus: ^6.0.0 receive_intent: ^0.2.5 watcher: ^1.1.0 @@ -92,6 +91,7 @@ dependencies: file_picker: ^8.0.7 background_fetch: ^1.3.7 clock: ^1.1.1 + mime: ^1.0.6 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: From 73eb989e846a2d1ba1c26bc53bc421372d551aaa Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Wed, 11 Sep 2024 23:47:15 +0500 Subject: [PATCH 156/177] Fix slidable autoclose not working --- lib/common/widgets/list/custom_list_view.dart | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 52547c5f..77113db9 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -11,6 +11,7 @@ import 'package:clock_app/settings/data/general_settings_schema.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class CustomListView extends StatefulWidget { const CustomListView({ @@ -384,22 +385,24 @@ class _CustomListViewState ), ) : Container(), - AnimatedReorderableListView( - longPressDraggable: false, - buildDefaultDragHandles: false, - proxyDecorator: (widget, index, animation) => - reorderableListDecorator(context, widget), - items: currentList, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - isSameItem: (a, b) => a.id == b.id, - scrollDirection: Axis.vertical, - itemBuilder: _getItemBuilder(), - enterTransition: [FadeIn()], - exitTransition: [FadeIn()], - controller: _scrollController, - insertDuration: const Duration(milliseconds: 300), - removeDuration: const Duration(milliseconds: 300), - onReorder: _handleReorderItems, + SlidableAutoCloseBehavior( + child: AnimatedReorderableListView( + longPressDraggable: false, + buildDefaultDragHandles: false, + proxyDecorator: (widget, index, animation) => + reorderableListDecorator(context, widget), + items: currentList, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + isSameItem: (a, b) => a.id == b.id, + scrollDirection: Axis.vertical, + itemBuilder: _getItemBuilder(), + enterTransition: [FadeIn()], + exitTransition: [FadeIn()], + controller: _scrollController, + insertDuration: const Duration(milliseconds: 300), + removeDuration: const Duration(milliseconds: 300), + onReorder: _handleReorderItems, + ), ) ]), ), From 365867fe9ee6c2e682ed8507c8ca9b811a036af0 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Wed, 11 Sep 2024 23:57:16 +0500 Subject: [PATCH 157/177] Add more logs --- lib/alarm/logic/alarm_isolate.dart | 13 +++++------ .../logic/alarm_notifications.dart | 23 +++++++++++++++---- lib/system/logic/background_service.dart | 16 ++++++------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 8f297163..30f179ba 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -30,8 +30,8 @@ void triggerScheduledNotification(int scheduleId, Json params) async { logger.f("Error in triggerScheduledNotification isolate: ${details.exception.toString()}"); }; - logger.i( - "Alarm isolate triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); + logger.t( + "[triggerScheduledNotification] Alarm isolate triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); // print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}"); if (params == null) { logger.e("Params was null when triggering alarm"); @@ -54,7 +54,6 @@ void triggerScheduledNotification(int scheduleId, Json params) async { IsolateNameServer.registerPortWithName( receivePort.sendPort, stopAlarmPortName); receivePort.listen((message) { - logger.d("Received message: $message"); stopScheduledNotification(message); }); @@ -81,8 +80,8 @@ void stopScheduledNotification(List message) { stopTimer(scheduleId, action); } - logger.i( - "Alarm stop triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); + logger.t( + "[stopScheduledNotification] Alarm stop triggered $scheduleId, isolate: ${Service.getIsolateId(Isolate.current)}"); } void triggerAlarm(int scheduleId, Json params) async { @@ -180,7 +179,7 @@ void setVolume(double volume) { } void stopAlarm(int scheduleId, AlarmStopAction action) async { - logger.i("Stopping alarm $scheduleId with action: ${action.name}"); + 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); @@ -198,7 +197,7 @@ void stopAlarm(int scheduleId, AlarmStopAction action) async { } void triggerTimer(int scheduleId, Json params) async { - logger.i("Timer triggered $scheduleId"); + logger.i("[triggerTimer] Timer triggered $scheduleId"); ClockTimer? timer = getTimerById(scheduleId); if (timer == null || !timer.isRunning) { diff --git a/lib/notifications/logic/alarm_notifications.dart b/lib/notifications/logic/alarm_notifications.dart index 7adab2ec..94eda92d 100644 --- a/lib/notifications/logic/alarm_notifications.dart +++ b/lib/notifications/logic/alarm_notifications.dart @@ -7,6 +7,7 @@ import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/common/types/notification_type.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/notifications/data/action_keys.dart'; import 'package:clock_app/notifications/data/fullscreen_notification_data.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; @@ -31,6 +32,7 @@ void showAlarmNotification({ required String dismissActionLabel, required String snoozeActionLabel, }) { + logger.t("[showAlarmNotification]"); FullScreenNotificationData data = alarmNotificationData[type]!; List actionButtons = []; @@ -84,6 +86,7 @@ void showAlarmNotification({ } Future removeAlarmNotification(ScheduledNotificationType type) async { + logger.t("[removeAlarmNotification]"); FullScreenNotificationData data = alarmNotificationData[type]!; await AwesomeNotifications() @@ -92,6 +95,7 @@ Future removeAlarmNotification(ScheduledNotificationType type) async { } Future closeAlarmNotification(ScheduledNotificationType type) async { + logger.t("[closeAlarmNotification]"); final intent = await ReceiveIntent.getInitialIntent(); await removeAlarmNotification(type); @@ -101,12 +105,16 @@ Future closeAlarmNotification(ScheduledNotificationType type) async { // If app was launched from a notification, close the app when the notification // is closed if (intent?.action == "SELECT_NOTIFICATION") { + logger.t( + "[closeAlarmNotification] Moving app to background because it was launched from notification"); await MoveToBackground.moveTaskToBack(); // SystemNavigator.pop(); } else { // If notification was created while app was in background, move app back // to background when we close the notification if (appVisibilityWhenAlarmNotificationCreated == FGBGType.background) { + logger.t( + "[closeAlarmNotification] Moving app to background because notification moved it to foreground"); appVisibilityWhenAlarmNotificationCreated = FGBGType.foreground; await MoveToBackground.moveTaskToBack(); } @@ -115,6 +123,7 @@ Future closeAlarmNotification(ScheduledNotificationType type) async { // decides to show a heads up notification instead of a full screen one, so // we can't always pop the top screen. Routes.popIf(alarmNotificationData[type]?.route); + logger.t("[closeAlarmNotification] Notification closed"); } Future snoozeAlarm(int scheduleId, ScheduledNotificationType type) async { @@ -134,8 +143,10 @@ Future stopAlarm(int scheduleId, ScheduledNotificationType type, ?.send([scheduleId, type.name, action.name]); } -Future dismissAlarmNotification(int scheduleId, AlarmDismissType dismissType, - ScheduledNotificationType type) async { +Future dismissAlarmNotification(int scheduleId, + AlarmDismissType dismissType, ScheduledNotificationType type) async { + logger.t("[dismissAlarmNotification]"); + switch (dismissType) { case AlarmDismissType.dismiss: await dismissAlarm(scheduleId, type); @@ -159,18 +170,19 @@ Future dismissAlarmNotification(int scheduleId, AlarmDismissType dismissTy await closeAlarmNotification(type); } - - Future openAlarmNotificationScreen( FullScreenNotificationData data, List scheduleIds, { bool tasksOnly = false, AlarmDismissType dismissType = AlarmDismissType.dismiss, }) async { + logger.t("[openAlarmNotificationScreen]"); await FlutterShowWhenLocked().show(); // If we're already on the same notification screen, pop it off the // stack so we don't have two of them on the stack. if (Routes.currentRoute == data.route) { + logger.t( + "[openAlarmNotificationScreen] Popping current route because a new alarm notification needs to be pushed"); Routes.pop(); } App.navigatorKey.currentState?.pushNamedAndRemoveUntil( @@ -185,6 +197,8 @@ Future openAlarmNotificationScreen( Future handleAlarmNotificationDismiss( ReceivedAction action, AlarmDismissType dismissType) async { + logger.t("[handleAlarmNotificationDismiss]"); + Payload payload = action.payload!; final type = ScheduledNotificationType.values.byName((payload['type'])!); FullScreenNotificationData data = alarmNotificationData[type]!; @@ -202,6 +216,7 @@ Future handleAlarmNotificationDismiss( } Future handleAlarmNotificationAction(ReceivedAction action) async { + logger.t("[handleAlarmNotificationAction]"); Payload payload = action.payload!; final type = ScheduledNotificationType.values.byName((payload['type'])!); FullScreenNotificationData data = alarmNotificationData[type]!; diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart index cf59083c..4f6860ec 100644 --- a/lib/system/logic/background_service.dart +++ b/lib/system/logic/background_service.dart @@ -18,21 +18,21 @@ Future initBackgroundService() async { requiredNetworkType: NetworkType.NONE), (String taskId) async { // <-- Event handler // This is the fetch-event callback. - logger.i("[BackgroundFetch] Event received $taskId"); + logger.t("[initBackgroundService] Event received $taskId"); // await initializeIsolate(); await updateAlarms( - "initBackgroundService(): Update alarms in background service"); + "[initBackgroundService] Update alarms in background service"); await updateTimers( - "initBackgroundService(): Update timers in background service"); + "[initBackgroundService] Update timers in background service"); // IMPORTANT: You must signal completion of your task or the OS can punish your app // for taking too long in the background. BackgroundFetch.finish(taskId); }, (String taskId) async { // <-- Task timeout handler. // This task has exceeded its allowed running-time. You must stop what you're doing and immediately .finish(taskId) - logger.i("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + logger.t("[initBackgroundService] Task timed-out taskId: $taskId"); BackgroundFetch.finish(taskId); }); } @@ -48,16 +48,16 @@ void handleBackgroundServiceTask(HeadlessTask task) async { if (isTimeout) { // This task has exceeded its allowed running-time. // You must stop what you're doing and immediately .finish(taskId) - logger.i("[BackgroundFetch] Headless task timed-out: $taskId"); + logger.t("[handleBackgroundServiceTask] Headless task timed-out: $taskId"); BackgroundFetch.finish(taskId); return; } await initializeIsolate(); - logger.i('[BackgroundFetch] Headless event received.'); + logger.t('[handleBackgroundServiceTask] Headless event received.'); await updateAlarms( - "handleBackgroundServiceTask(): Update alarms in background service"); + "[handleBackgroundServiceTask] Update alarms in background service"); await updateTimers( - "handleBackgroundServiceTask(): Update timers in background service"); + "[handleBackgroundServiceTask] Update timers in background service"); BackgroundFetch.finish(taskId); } From bb89da13b43af4abba837cd334b89f2bd6e1812c Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Thu, 12 Sep 2024 18:15:56 +0500 Subject: [PATCH 158/177] Add search to logs --- android/app/build.gradle | 4 - .../vicolo/chrono/DocumentPickerActivity.kt | 31 ----- .../kotlin/com/vicolo/chrono/MainActivity.kt | 128 ++--------------- lib/alarm/logic/schedule_alarm.dart | 4 +- lib/alarm/screens/alarm_events_screen.dart | 3 +- lib/alarm/screens/alarm_screen.dart | 6 - lib/clock/screens/search_city_screen.dart | 2 +- .../widgets/list/action_bottom_sheet.dart | 3 +- .../builder/motion_animated_builder.dart | 6 +- lib/common/widgets/list/custom_list_view.dart | 58 ++++---- .../widgets/list/list_item_measurer.dart | 59 ++++---- .../widgets/list/persistent_list_view.dart | 9 +- lib/common/widgets/list/static_list_view.dart | 22 +++ lib/debug/logic/logger.dart | 3 +- lib/debug/screens/logs_screen.dart | 104 ++++++-------- lib/main.dart | 10 +- lib/navigation/screens/nav_scaffold.dart | 2 +- lib/navigation/widgets/app_top_bar.dart | 39 +++++- lib/navigation/widgets/search_top_bar.dart | 131 ++++++++++++++++++ lib/settings/screens/about_screen.dart | 5 +- lib/settings/screens/backup_screen.dart | 7 +- lib/settings/screens/contributors.dart | 5 +- lib/settings/screens/donors.dart | 5 +- lib/settings/screens/licenses.dart | 5 +- .../screens/restore_defaults_screen.dart | 4 +- lib/settings/screens/ringtones_screen.dart | 4 +- .../screens/settings_group_screen.dart | 103 +++++++------- lib/settings/screens/tags_screen.dart | 4 +- lib/settings/widgets/list_setting_screen.dart | 4 +- lib/settings/widgets/settings_top_bar.dart | 116 ---------------- lib/theme/screens/themes_screen.dart | 2 +- lib/timer/screens/presets_screen.dart | 2 +- lib/timer/screens/timer_fullscreen.dart | 2 +- lib/timer/screens/timer_screen.dart | 6 - pubspec.lock | 27 +--- pubspec.yaml | 9 +- 36 files changed, 395 insertions(+), 539 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/vicolo/chrono/DocumentPickerActivity.kt create mode 100644 lib/common/widgets/list/static_list_view.dart create mode 100644 lib/navigation/widgets/search_top_bar.dart delete mode 100644 lib/settings/widgets/settings_top_bar.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 0588e16c..36c63997 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -115,10 +115,6 @@ android { } } -dependencies { - implementation "androidx.documentfile:documentfile:1.0.1" -} - flutter { diff --git a/android/app/src/main/kotlin/com/vicolo/chrono/DocumentPickerActivity.kt b/android/app/src/main/kotlin/com/vicolo/chrono/DocumentPickerActivity.kt deleted file mode 100644 index 5cc790fe..00000000 --- a/android/app/src/main/kotlin/com/vicolo/chrono/DocumentPickerActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.vicolo.chrono - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts - -class DirectoryPickerActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - resultLauncher.launch(intent) - } - - private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - val returnIntent = Intent() - if (result.resultCode == RESULT_OK) { - val uri: Uri? = result.data?.data - if (uri != null) { - contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - returnIntent.putExtra("getDirectoryUri", uri.toString()) - } - } - setResult(RESULT_OK, returnIntent) - finish() - } -} diff --git a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt index ec29168e..61d3f22b 100644 --- a/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt +++ b/android/app/src/main/kotlin/com/vicolo/chrono/MainActivity.kt @@ -1,123 +1,23 @@ package com.vicolo.chrono -import android.app.Activity +import android.content.Context import android.content.Intent -import android.net.Uri -import androidx.documentfile.provider.DocumentFile +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar +import java.util.ArrayList +import androidx.annotation.NonNull import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel - -class MainActivity : FlutterActivity() { - private val channel = "com.vicolo.chrono/documents" - private lateinit var result: MethodChannel.Result +import io.flutter.plugins.GeneratedPluginRegistrant; - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { +class MainActivity: FlutterActivity() { + private val CHANNEL = "com.vicolo.chrono/alarm" + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel) - .setMethodCallHandler { call, result -> - when (call.method) { - "getDirectoryPath" -> { - this.result = result - val intent = Intent(this, DirectoryPickerActivity::class.java) - startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE) - } - "listDirectories" -> { - val uriString = call.argument("uri")!! - val uri = Uri.parse(uriString) - val directories = listDirectories(uri) - result.success(directories) - } - "listFiles" -> { - val uriString = call.argument("uri")!! - val uri = Uri.parse(uriString) - val files = listFiles(uri) - result.success(files) - } - "getFileChunk" -> { - val uriString = call.argument("uri")!! - val offset = call.argument("offset")!! - val chunkSize = call.argument("chunkSize")!! - val uri = Uri.parse(uriString) - val chunk = readFileChunk(uri, offset, chunkSize) - if (chunk != null) { - result.success(chunk) - } else { - result.error("READ_ERROR", "Failed to read file chunk", null) - } - } - else -> result.notImplemented() - } - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { - val pickedDirectoryUri = data?.getStringExtra("getDirectoryUri") - if (pickedDirectoryUri != null) { - result.success(pickedDirectoryUri) - } else { - result.error("NO_DIRECTORY_URI", "No directory URI found", null) - } - } - } - - private fun listDirectories(uri: Uri): List { - val directory = DocumentFile.fromTreeUri(this, uri) - val directories = mutableListOf() - - if (directory != null && directory.isDirectory) { - for (item in directory.listFiles()) { - if (item.isDirectory) { - directories.add(item.uri.toString()) - } - } - } - - return directories - } - - private fun listFiles(uri: Uri): List> { - val directory = DocumentFile.fromTreeUri(this, uri) - val files = mutableListOf>() - - if (directory != null && directory.isDirectory) { - for (item in directory.listFiles()) { - if (item.isFile) { - val fileInfo: Map = mapOf( - "uri" to item.uri.toString(), - "name" to (item.name ?: "Unknown"), - "size" to item.length(), - "modified" to item.lastModified() - ) - files.add(fileInfo) - } - } - } - - return files - } - - private fun readFileChunk(uri: Uri, offset: Int, chunkSize: Int): ByteArray? { - return try { - contentResolver.openInputStream(uri)?.use { inputStream -> - inputStream.skip(offset.toLong()) - val buffer = ByteArray(chunkSize) - val bytesRead = inputStream.read(buffer, 0, chunkSize) - if (bytesRead != -1) { - buffer.copyOf(bytesRead) - } else { - null - } - } - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - companion object { - const val PICK_DIRECTORY_REQUEST_CODE = 1 } } diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index f0ac0bc1..d8d42505 100644 --- a/lib/alarm/logic/schedule_alarm.dart +++ b/lib/alarm/logic/schedule_alarm.dart @@ -92,7 +92,7 @@ Future scheduleAlarm( }, ); - logger.i( + logger.t( 'Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description'); } } @@ -137,6 +137,6 @@ Future scheduleSnoozeAlarm(int scheduleId, Duration delay, await createSnoozeNotification(scheduleId, DateTime.now().add(delay)); } - logger.i( + 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 f71edde2..b280c2ac 100644 --- a/lib/alarm/screens/alarm_events_screen.dart +++ b/lib/alarm/screens/alarm_events_screen.dart @@ -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( diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 886c1dc2..7fbfefc8 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -21,14 +21,8 @@ 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, this.actionController}); diff --git a/lib/clock/screens/search_city_screen.dart b/lib/clock/screens/search_city_screen.dart index db172c7f..b5047ff0 100644 --- a/lib/clock/screens/search_city_screen.dart +++ b/lib/clock/screens/search_city_screen.dart @@ -68,7 +68,7 @@ class _SearchCityScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: TextField( + titleWidget: TextField( autofocus: true, controller: _filterController, decoration: InputDecoration( 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/builder/motion_animated_builder.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart index 65bd1d39..5f2a1280 100644 --- a/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart'; import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -437,7 +438,7 @@ class MotionBuilderState extends State int _itemIndexToIndex(int itemIndex) { int index = itemIndex; for (final _ActiveItem item in _outgoingItems) { - assert(item.itemIndex != itemIndex); + // assert(item.itemIndex != itemIndex); if (item.itemIndex < itemIndex) { index -= 1; } else { @@ -638,7 +639,8 @@ class MotionBuilderState extends State final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, index); if (outgoingItem != null) { - final child = _items[index]!.widget; + // logger.d("Outgoing item in $_items at index $index"); + final child = _items[index]?.widget ?? Container(); return _removeItemBuilder(outgoingItem, child); } if (_dragInfo != null && index >= _itemsCount) { diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart index 77113db9..21faa7c2 100644 --- a/lib/common/widgets/list/custom_list_view.dart +++ b/lib/common/widgets/list/custom_list_view.dart @@ -68,7 +68,6 @@ class CustomListView extends StatefulWidget { class _CustomListViewState extends State> { late List currentList = List.from(widget.items); - double _itemCardHeight = 0; final _scrollController = ScrollController(); // final _controller = AnimatedListController(); late int _selectedSortIndex = widget.initialSortIndex; @@ -94,7 +93,7 @@ class _CustomListViewState widget.listController.setGetItemIndex(_getItemIndex); widget.listController.setDuplicateItem(_handleDuplicateItem); widget.listController.setReloadItems(_handleReloadItems); - widget.listController.setClearItems(_handleClear); + widget.listController.setClearItems(_handleClearItems); widget.listController.setGetItems(() => widget.items); _updateCurrentList(); // widget.listController.setChangeItemWithId(_handleChangeItemWithId); @@ -136,11 +135,11 @@ class _CustomListViewState int _getItemIndex(Item item) => currentList.indexWhere((element) => element.id == item.id); - void _updateItemHeight() { - if (_itemCardHeight == 0) { - // _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; - } - } + // void _updateItemHeight() { + // if (_itemCardHeight == 0) { + // // _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0; + // } + // } bool _handleReorderItems(int oldIndex, int newIndex) { if (newIndex >= widget.items.length || _selectedSortIndex != 0) { @@ -167,8 +166,8 @@ class _CustomListViewState Future _handleDeleteItem(Item deletedItem, [bool callOnModifyList = true]) async { + widget.items.removeWhere((element) => element.id == deletedItem.id); setState(() { - widget.items.removeWhere((element) => element.id == deletedItem.id); _updateCurrentList(); }); @@ -178,11 +177,12 @@ class _CustomListViewState Future _handleDeleteItemList(List deletedItems) async { for (var item in deletedItems) { - setState(() { - widget.items.removeWhere((element) => element.id == item.id); - _updateCurrentList(); - }); + widget.items.removeWhere((element) => element.id == item.id); } + setState(() { + _updateCurrentList(); + }); + for (var item in deletedItems) { await widget.onDeleteItem?.call(item); } @@ -190,8 +190,8 @@ class _CustomListViewState widget.onModifyList?.call(); } - void _handleClear() { - _handleDeleteItemList(List.from(widget.items)); + void _handleClearItems() async { + await _handleDeleteItemList(List.from(widget.items)); } Future _handleAddItem(Item item, {int index = -1}) async { @@ -206,7 +206,7 @@ class _CustomListViewState int currentListIndex = _getItemIndex(item); _scrollToIndex(currentListIndex); - _updateItemHeight(); + // _updateItemHeight(); widget.onModifyList?.call(); } @@ -215,8 +215,8 @@ class _CustomListViewState } void _scrollToIndex(int index) { - if (_itemCardHeight == 0 && index != 0) return; - _scrollController.animateTo(index * _itemCardHeight, + if (index != 0) return; + _scrollController.animateTo(index.toDouble(), duration: const Duration(milliseconds: 250), curve: Curves.easeIn); } @@ -267,12 +267,7 @@ class _CustomListViewState } void _handleCustomAction(ListFilterCustomAction action) { - final list = _getActionableItems(); - List items = list - .where((item) => - widget.listFilters.every((filter) => filter.filterFunction(item))) - .toList(); - + final items = _getActionableItems(); action.action(items); _endSelection(); } @@ -283,19 +278,19 @@ class _CustomListViewState if (result == null || result == false) return; final list = _getActionableItems(); - final itemsToRemove = List.from(list.where((item) => - item.isDeletable && - widget.listFilters.every((filter) => filter.filterFunction(item)))); + final itemsToRemove = + List.from(list.where((item) => item.isDeletable)); _endSelection(); await _handleDeleteItemList(itemsToRemove); - - widget.onModifyList?.call(); } List _getActionableItems() { return _isSelecting ? widget.items.where((item) => _selectedIds.contains(item.id)).toList() - : widget.items; + : widget.items + .where((item) => widget.listFilters + .every((filter) => filter.filterFunction(item))) + .toList(); } _getItemBuilder() { @@ -385,14 +380,15 @@ class _CustomListViewState ), ) : Container(), - SlidableAutoCloseBehavior( + SlidableAutoCloseBehavior( child: AnimatedReorderableListView( longPressDraggable: false, buildDefaultDragHandles: false, proxyDecorator: (widget, index, animation) => reorderableListDecorator(context, widget), items: currentList, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), isSameItem: (a, b) => a.id == b.id, scrollDirection: Axis.vertical, itemBuilder: _getItemBuilder(), diff --git a/lib/common/widgets/list/list_item_measurer.dart b/lib/common/widgets/list/list_item_measurer.dart index beb6033b..9c3f0eda 100644 --- a/lib/common/widgets/list/list_item_measurer.dart +++ b/lib/common/widgets/list/list_item_measurer.dart @@ -1,31 +1,30 @@ -import 'package:flutter/material.dart'; -import 'package:great_list_view/great_list_view.dart'; +// import 'package:flutter/material.dart'; -class ListItemMeasurer extends StatefulWidget { - const ListItemMeasurer({ - super.key, - required this.controller, - required this.index, - }); - - final AnimatedListController controller; - final int index; - - @override - State createState() => _ListItemMeasurerState(); -} - -class _ListItemMeasurerState extends State { - late double? height; - - @override - void initState() { - super.initState(); - height = widget.controller.computeItemBox(0, true)?.height; - } - - @override - Widget build(BuildContext context) { - return SizedBox(height: height); - } -} +// class ListItemMeasurer extends StatefulWidget { +// const ListItemMeasurer({ +// super.key, +// required this.controller, +// required this.index, +// }); +// +// final AnimatedListController controller; +// final int index; +// +// @override +// State createState() => _ListItemMeasurerState(); +// } +// +// class _ListItemMeasurerState extends State { +// late double? height; +// +// @override +// void initState() { +// super.initState(); +// height = widget.controller.computeItemBox(0, true)?.height; +// } +// +// @override +// Widget build(BuildContext context) { +// return SizedBox(height: height); +// } +// } diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index 16220989..66bc2237 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -3,6 +3,7 @@ import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; +import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:flutter/material.dart'; @@ -119,7 +120,6 @@ class _PersistentListViewState if (widget.saveTag.isNotEmpty) { _items = loadListSync(widget.saveTag); } - // watchList(widget.saveTag, (event) => reloadItems()); ListenerManager.addOnChangeListener(widget.saveTag, _loadItems); if (widget.sortOptions.isNotEmpty) { @@ -134,16 +134,12 @@ class _PersistentListViewState else { _initialSortIndex = 0; } - // ListenerManager.addOnChangeListener( - // "${widget.saveTag}-reload", reloadItems); } @override void dispose() { ListenerManager.removeOnChangeListener(widget.saveTag, _loadItems); - // ListenerManager.removeOnChangeListener( - // "${widget.saveTag}-reload", reloadItems); - // unwatchList(widget.saveTag); + super.dispose(); } @@ -154,6 +150,7 @@ class _PersistentListViewState void _loadItems() { if (widget.saveTag.isNotEmpty) { + logger.d('Loading items from ${widget.saveTag}'); widget.listController.changeItems( (List items) { List newList = loadListSync(widget.saveTag); diff --git a/lib/common/widgets/list/static_list_view.dart b/lib/common/widgets/list/static_list_view.dart new file mode 100644 index 00000000..829c21cb --- /dev/null +++ b/lib/common/widgets/list/static_list_view.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class StaticListView extends StatelessWidget { + const StaticListView({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + ...children, + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/debug/logic/logger.dart b/lib/debug/logic/logger.dart index 8d66a7a9..5fc5077e 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/debug/logic/logger.dart @@ -8,8 +8,7 @@ var logger = Logger( output: FileLoggerOutput(), printer: PrettyPrinter( methodCount: 100, // Number of method calls to be displayed - - errorMethodCount: 8, // Number of method calls if stacktrace is provided + errorMethodCount: 100, // Number of method calls if stacktrace is provided lineLength: 80, // Width of the output colors: true, // Colorful log messages printEmojis: true, // Print an emoji for each log message diff --git a/lib/debug/screens/logs_screen.dart b/lib/debug/screens/logs_screen.dart index 99941163..bd5896f2 100644 --- a/lib/debug/screens/logs_screen.dart +++ b/lib/debug/screens/logs_screen.dart @@ -4,14 +4,17 @@ import 'package:clock_app/alarm/data/alarm_events_list_filters.dart'; import 'package:clock_app/common/data/paths.dart'; import 'package:clock_app/common/types/list_controller.dart'; import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; +import 'package:clock_app/common/widgets/list/static_list_view.dart'; import 'package:clock_app/debug/data/log_list_filters.dart'; import 'package:clock_app/debug/data/log_sort_options.dart'; import 'package:clock_app/debug/logic/logger.dart'; import 'package:clock_app/debug/types/log.dart'; import 'package:clock_app/debug/widgets/log_card.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/types/setting_item.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -27,6 +30,7 @@ class LogsScreen extends StatefulWidget { class _LogsScreenState extends State { List _logs = []; + List _filteredLogs = []; final _listController = ListController(); List _mergeMultilineLogs(List logLines) { @@ -81,35 +85,53 @@ class _LogsScreenState extends State { TextTheme textTheme = theme.textTheme; return Scaffold( - appBar: AppTopBar(title: Text("App Logs", style: textTheme.titleMedium)), + appBar: SearchTopBar( + title: "App Logs", + searchParams: SearchParams( + onSearch: (searchedItems) { + setState(() { + _filteredLogs = searchedItems; + }); + }, + placeholder: "Search logs", + choices: _logs, + searchTermGetter: (log) { + return log.message; + }, + ), + ), body: Stack( children: [ Column( children: [ Expanded( flex: 1, - child: CustomListView( - items: _logs, - listController: _listController, - itemBuilder: (log) => LogCard( - key: ValueKey(log), - log: log, - ), - // onTapItem: (fileItem, index) { - // // widget.setting.setValue(context, themeItem); - // // _listController.reload(); - // }, - // onDeleteItem: (event){}, - isDuplicateEnabled: false, - isReorderable: false, - isDeleteEnabled: false, - // isDeleteEnabled: true, - placeholderText: "No logs", - listFilters: logListFilters, - sortOptions: logSortOptions, - // reloadOnPop: true, - // listFilters: alarmEventsListFilters, - ), + child: _filteredLogs.isEmpty + ? CustomListView( + items: _logs, + listController: _listController, + itemBuilder: (log) => LogCard( + key: ValueKey(log), + log: log, + ), + isDuplicateEnabled: false, + isReorderable: false, + isDeleteEnabled: false, + placeholderText: "No logs", + listFilters: logListFilters, + sortOptions: logSortOptions, + ) + : StaticListView( + children: _filteredLogs + .map( + (log) => CardContainer( + child: LogCard( + key: ValueKey(log), + log: log, + ), + ), + ) + .toList()), ), ], ), @@ -124,6 +146,7 @@ class _LogsScreenState extends State { if (context.mounted) showSnackBar(context, "Logs cleared"); _listController.clearItems(); + setState(() {}); }, ), FAB( @@ -151,41 +174,6 @@ class _LogsScreenState extends State { logger.e("Error saving logs 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); - // } - // } - // }), - - // FAB( - // index: 1, - // icon: Icons.folder_rounded, - // bottomPadding: 8, - // onPressed: () async { - // // Item? themeItem = widget.createThemeItem(); - // // await _openCustomizeItemScreen( - // // themeItem, - // // onSave: (newThemeItem) { - // // _listController.addItem(newThemeItem); - // // }, - // // isNewItem: true, - // // ); - // }, - // ) ], ), ); diff --git a/lib/main.dart b/lib/main.dart index 358d5cce..7b104dc1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,6 @@ import 'dart:core'; -import 'dart:isolate'; -import 'dart:ui'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; -import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/audio/logic/audio_session.dart'; @@ -14,7 +11,6 @@ import 'package:clock_app/navigation/types/app_visibility.dart'; import 'package:clock_app/notifications/logic/foreground_task.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; import 'package:clock_app/settings/logic/initialize_settings.dart'; -import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:clock_app/system/data/app_info.dart'; import 'package:clock_app/system/data/device_info.dart'; import 'package:clock_app/system/logic/background_service.dart'; @@ -27,9 +23,9 @@ import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { - // FlutterError.onError = (FlutterErrorDetails details) { - // logger.f(details.exception.toString()); - // }; + FlutterError.onError = (FlutterErrorDetails details) { + logger.e(details.exception.toString(), stackTrace: details.stack,); + }; WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index bbfceff9..ca307c73 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -214,7 +214,7 @@ class _NavScaffoldState extends State { child: Scaffold( appBar: orientation == Orientation.portrait ? AppTopBar( - title: Text( + titleWidget: Text( tabs[_selectedTabIndex].title, style: textTheme.titleMedium?.copyWith( color: colorScheme diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index b4c3af8a..4eb64efb 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class AppTopBar extends StatelessWidget implements PreferredSizeWidget { - final Widget? title; + final Widget? titleWidget; + final String? title; final List? actions; final Color? systemNavBarColor; @@ -12,8 +13,10 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { const AppTopBar({ super.key, + this.titleWidget, + this.actions, + this.systemNavBarColor, this.title, - this.actions, this.systemNavBarColor, }); @override @@ -22,7 +25,8 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; - final systemNavigationBarColor = systemNavBarColor ?? colorScheme.background; + final systemNavigationBarColor = + systemNavBarColor ?? colorScheme.background; Brightness statusBarIconBrightness = colorScheme.surface.computeLuminance() > 0.179 @@ -33,6 +37,16 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { ? Brightness.dark : Brightness.light; + Widget? barTitleWidget = titleWidget ?? + (title != null + ? Text( + title!, + style: textTheme.titleMedium?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + ), + ) + : null); + return PreferredSize( preferredSize: preferredSize, child: Padding( @@ -48,9 +62,26 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { ), scrolledUnderElevation: 0, toolbarHeight: preferredSize.height, - title: title, + titleSpacing: 0, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (Navigator.of(context).canPop()) ...[ + IconButton( + icon: Icon(Icons.arrow_back, + color: colorScheme.onSurface.withOpacity(0.8)), + onPressed: () => Navigator.of(context).pop(), + padding: EdgeInsets.zero), + const SizedBox(width: 8) + ], + if (!Navigator.of(context).canPop()) const SizedBox(width: 16), + if (barTitleWidget != null) barTitleWidget, + ], + ), actions: [...?actions], elevation: 0, + automaticallyImplyLeading: false, iconTheme: IconThemeData( color: colorScheme.onSurface.withOpacity(0.8), ), diff --git a/lib/navigation/widgets/search_top_bar.dart b/lib/navigation/widgets/search_top_bar.dart new file mode 100644 index 00000000..d30f3648 --- /dev/null +++ b/lib/navigation/widgets/search_top_bar.dart @@ -0,0 +1,131 @@ +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting_item.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SearchParams { + final void Function(List filteredItems)? onSearch; + final List choices; + final String Function(T)? searchTermGetter; + final String placeholder; + + SearchParams( + {required this.onSearch, + required this.placeholder, + required this.choices, + required this.searchTermGetter}); +} + +class SearchTopBar extends StatefulWidget implements PreferredSizeWidget { + @override + Size get preferredSize => const Size(0, 56); + + const SearchTopBar({super.key, this.title, this.actions, this.searchParams}); + + final SearchParams? searchParams; + final List? actions; + final String? title; + + @override + State> createState() => _SearchTopBarState(); +} + +class _SearchTopBarState extends State> { + final TextEditingController _filterController = TextEditingController(); + bool _searching = false; + + _SearchTopBarState() { + _filterController.addListener(() async { + if (widget.searchParams == null) { + return; + } + if (_filterController.text.isEmpty) { + widget.searchParams!.onSearch?.call([]); + } else { + var results = extractTop( + query: _filterController.text, + choices: widget.searchParams!.choices, + // choices: [ + // ...appSettings.settings, + // ...appSettings.settingPageLinks, + // ...appSettings.settingActions + // ], + limit: 10, + cutoff: 50, + getter: widget.searchParams!.searchTermGetter, + // getter: (item) { + // // Search term includes the setting name, as well as the parent group names + // return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; + // }, + ); + + widget.searchParams!.onSearch + ?.call(results.map((result) => result.choice).toList()); + } + }); + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + ColorScheme colorScheme = theme.colorScheme; + AppLocalizations localizations = AppLocalizations.of(context)!; + + if (_searching) { + return AppTopBar( + titleWidget: Expanded( + child: TextField( + autofocus: _filterController.text.isEmpty, + onTapOutside: ((event) { + FocusScope.of(context).unfocus(); + }), + controller: _filterController, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: + const OutlineInputBorder(borderSide: BorderSide.none), + fillColor: Colors.transparent, + hintText: widget.searchParams!.placeholder, + hintStyle: textTheme.bodyLarge, + ), + textAlignVertical: TextAlignVertical.center, + style: textTheme.bodyLarge, + ), + ), + actions: [ + IconButton( + onPressed: () { + _filterController.clear(); + setState(() { + _searching = false; + }); + }, + icon: const Icon(Icons.close), + ) + ], + ); + } else { + return AppTopBar( + title: widget.title, + actions: [ + ...?widget.actions, + if (widget.searchParams != null) + IconButton( + onPressed: () { + setState(() { + _searching = true; + }); + }, + icon: Icon( + Icons.search, + color: colorScheme.onBackground, + ), + ) + ], + ); + } + } +} diff --git a/lib/settings/screens/about_screen.dart b/lib/settings/screens/about_screen.dart index 4084948a..1fe416fb 100644 --- a/lib/settings/screens/about_screen.dart +++ b/lib/settings/screens/about_screen.dart @@ -24,10 +24,7 @@ class AboutScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.aboutSettingGroup, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.aboutSettingGroup, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index 368bdaee..72fbc347 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -3,10 +3,11 @@ import 'dart:convert'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/snackbar.dart'; import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/data/backup_options.dart'; import 'package:clock_app/settings/logic/backup.dart'; import 'package:clock_app/settings/types/backup_option.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -57,7 +58,7 @@ class _BackupExportScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar( + appBar: AppTopBar( actions: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -145,7 +146,7 @@ class _BackupImportScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar( + appBar: SearchTopBar( actions: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), diff --git a/lib/settings/screens/contributors.dart b/lib/settings/screens/contributors.dart index 81e751c1..dfbccce9 100644 --- a/lib/settings/screens/contributors.dart +++ b/lib/settings/screens/contributors.dart @@ -24,10 +24,7 @@ class ContributorsScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.contributorsSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.contributorsSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/donors.dart b/lib/settings/screens/donors.dart index 671c7a07..0ea29c02 100644 --- a/lib/settings/screens/donors.dart +++ b/lib/settings/screens/donors.dart @@ -24,10 +24,7 @@ class DonorsScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.donorsSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.donorsSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/licenses.dart b/lib/settings/screens/licenses.dart index e675d1b7..6a905462 100644 --- a/lib/settings/screens/licenses.dart +++ b/lib/settings/screens/licenses.dart @@ -16,10 +16,7 @@ class LicensesScreen extends StatelessWidget { final TextTheme textTheme = theme.textTheme; return Scaffold( appBar: AppTopBar( - title: Text(AppLocalizations.of(context)!.openSourceLicensesSetting, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onBackground.withOpacity(0.6), - )), + title: AppLocalizations.of(context)!.openSourceLicensesSetting, ), body: SingleChildScrollView( child: Padding( diff --git a/lib/settings/screens/restore_defaults_screen.dart b/lib/settings/screens/restore_defaults_screen.dart index 3c9ec2a1..b2322e5f 100644 --- a/lib/settings/screens/restore_defaults_screen.dart +++ b/lib/settings/screens/restore_defaults_screen.dart @@ -2,7 +2,7 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_item.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -65,7 +65,7 @@ class _RestoreDefaultScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar( + appBar: AppTopBar( title: AppLocalizations.of(context)!.restoreSettingGroup, ), body: SingleChildScrollView( diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index 3af880b0..839438ac 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -6,8 +6,8 @@ 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/debug/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/settings/widgets/settings_top_bar.dart'; import 'package:clock_app/system/data/device_info.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -54,7 +54,7 @@ class _RingtonesScreenState extends State { TextTheme textTheme = theme.textTheme; return Scaffold( - appBar: SettingsTopBar( + appBar: AppTopBar( title: AppLocalizations.of(context)!.melodiesSetting, ), body: Stack( diff --git a/lib/settings/screens/settings_group_screen.dart b/lib/settings/screens/settings_group_screen.dart index 2a888545..27737e21 100644 --- a/lib/settings/screens/settings_group_screen.dart +++ b/lib/settings/screens/settings_group_screen.dart @@ -1,15 +1,14 @@ -import 'package:clock_app/common/data/animations.dart'; +import 'package:clock_app/common/widgets/list/static_list_view.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:clock_app/settings/logic/get_setting_widget.dart'; import 'package:clock_app/settings/screens/restore_defaults_screen.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_item.dart'; import 'package:clock_app/settings/types/setting_link.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:clock_app/settings/widgets/search_setting_card.dart'; import 'package:clock_app/settings/widgets/setting_page_link_card.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; import 'package:flutter/material.dart'; class SettingGroupScreen extends StatefulWidget { @@ -24,7 +23,7 @@ class SettingGroupScreen extends StatefulWidget { } class _SettingGroupScreenState extends State { - List searchedItems = []; + List _searchedItems = []; @override void initState() { @@ -33,59 +32,65 @@ class _SettingGroupScreenState extends State { @override Widget build(BuildContext context) { + AppLocalizations localizations = AppLocalizations.of(context)!; + List getSearchItemWidgets() { - return searchedItems.map((item) { + return _searchedItems.map((item) { return SearchSettingCard(settingItem: item); }).toList(); } return Scaffold( - appBar: SettingsTopBar( + appBar: SearchTopBar( title: widget.settingGroup.displayName(context), - onSearch: (settingItems) { - setState(() { - searchedItems = settingItems; - }); - }, - showSearch: widget.settingGroup.isSearchable, + searchParams: widget.settingGroup.isSearchable + ? SearchParams( + onSearch: (settingItems) { + setState(() { + _searchedItems = settingItems; + }); + }, + placeholder: localizations.searchSettingPlaceholder, + choices: [ + ...appSettings.settings, + ...appSettings.settingPageLinks, + ...appSettings.settingActions + ], + searchTermGetter: (item) { + // Search term includes the setting name, as well as the parent group names and the tags + return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; + }, + ) + : null, + // showSearch: widget.settingGroup.isSearchable, ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: searchedItems.isEmpty - ? [ - ...getSettingWidgets( - widget.settingGroup.settingItems, - checkDependentEnableConditions: () => setState(() {}), - onSettingChanged: () { - if (widget.isAppSettings) { - appSettings.save(); - } - }, - isAppSettings: widget.isAppSettings, - ), - if (widget.isAppSettings) - SettingPageLinkCard( - setting: SettingPageLink( - 'Restore default values', - (context) => AppLocalizations.of(context)! - .restoreSettingGroup, - RestoreDefaultScreen( - settingGroup: widget.settingGroup, - onRestore: () async { - await appSettings.save(); - setState(() {}); - }, - ))), - const SizedBox(height: 16), - ] - : [ - ...getSearchItemWidgets(), - const SizedBox(height: 16), - ], - ), - ), + body: StaticListView( + children: _searchedItems.isEmpty + ? [ + ...getSettingWidgets( + widget.settingGroup.settingItems, + checkDependentEnableConditions: () => setState(() {}), + onSettingChanged: () { + if (widget.isAppSettings) { + appSettings.save(); + } + }, + isAppSettings: widget.isAppSettings, + ), + if (widget.isAppSettings) + SettingPageLinkCard( + setting: SettingPageLink( + 'restore_default_values', + (context) => localizations.restoreSettingGroup, + RestoreDefaultScreen( + settingGroup: widget.settingGroup, + onRestore: () async { + await appSettings.save(); + setState(() {}); + }, + ))), + ] + : getSearchItemWidgets(), ), ); } diff --git a/lib/settings/screens/tags_screen.dart b/lib/settings/screens/tags_screen.dart index 36bff289..4ff9d921 100644 --- a/lib/settings/screens/tags_screen.dart +++ b/lib/settings/screens/tags_screen.dart @@ -3,7 +3,7 @@ import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/widgets/settings_top_bar.dart'; +import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/widgets/tag_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -48,7 +48,7 @@ class _TagsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: SettingsTopBar(title: AppLocalizations.of(context)!.tagsSetting), + appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), body: Stack( children: [ Column( diff --git a/lib/settings/widgets/list_setting_screen.dart b/lib/settings/widgets/list_setting_screen.dart index 159d7e16..9deb0cc8 100644 --- a/lib/settings/widgets/list_setting_screen.dart +++ b/lib/settings/widgets/list_setting_screen.dart @@ -55,9 +55,7 @@ class _ListSettingScreenState @override Widget build(BuildContext context) { return Scaffold( - appBar: AppTopBar( - title: Text(widget.setting.displayName(context)), - ), + appBar: AppTopBar(title: widget.setting.displayName(context)), body: Stack( children: [ Column( diff --git a/lib/settings/widgets/settings_top_bar.dart b/lib/settings/widgets/settings_top_bar.dart deleted file mode 100644 index 6704aaf1..00000000 --- a/lib/settings/widgets/settings_top_bar.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/data/settings_schema.dart'; -import 'package:clock_app/settings/types/setting_item.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class SettingsTopBar extends StatefulWidget implements PreferredSizeWidget { - @override - Size get preferredSize => const Size(0, 56); - - const SettingsTopBar( - {super.key, - this.onSearch, - this.showSearch = false, - this.title, this.actions}); - - final void Function(List settings)? onSearch; - final List? actions; - final String? title; - final bool showSearch; - - @override - State createState() => _SettingsTopBarState(); -} - -class _SettingsTopBarState extends State { - final TextEditingController _filterController = TextEditingController(); - bool _searching = false; - - _SettingsTopBarState() { - _filterController.addListener(() async { - if (_filterController.text.isEmpty) { - widget.onSearch?.call([]); - } else { - var results = extractTop( - query: _filterController.text, - choices: [ - ...appSettings.settings, - ...appSettings.settingPageLinks, - ...appSettings.settingActions - ], - limit: 10, - cutoff: 50, - getter: (item) { - // Search term includes the setting name, as well as the parent group names - return "${item.name} ${item.path.map((group) => group.name).join(" ")} ${item.searchTags.join(" ")}"; - }); - - widget.onSearch?.call(results.map((result) => result.choice).toList()); - } - }); - } - - @override - Widget build(BuildContext context) { - if (_searching) { - return AppTopBar( - title: TextField( - autofocus: _filterController.text.isEmpty, - onTapOutside: ((event) { - FocusScope.of(context).unfocus(); - }), - controller: _filterController, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: - const OutlineInputBorder(borderSide: BorderSide.none), - fillColor: Colors.transparent, - hintText: AppLocalizations.of(context)!.searchSettingPlaceholder, - hintStyle: Theme.of(context).textTheme.bodyLarge, - ), - textAlignVertical: TextAlignVertical.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - actions: [ - IconButton( - onPressed: () { - _filterController.clear(); - setState(() { - _searching = false; - }); - }, - icon: const Icon(Icons.close), - ) - ], - ); - } else { - return AppTopBar( - title: widget.title != null ? Text( - widget.title!, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.6), - ), - ): null, - actions: [ - ...?widget.actions, - if (widget.showSearch) - IconButton( - onPressed: () { - setState(() { - _searching = true; - }); - }, - icon: Icon( - Icons.search, - color: - Theme.of(context).colorScheme.onBackground, - ), - ) - ], - ); - } - } -} diff --git a/lib/theme/screens/themes_screen.dart b/lib/theme/screens/themes_screen.dart index 89ca65e7..eba91cae 100644 --- a/lib/theme/screens/themes_screen.dart +++ b/lib/theme/screens/themes_screen.dart @@ -79,7 +79,7 @@ class _ThemesScreenState Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: Text(widget.setting.displayName(context)), + titleWidget: Text(widget.setting.displayName(context)), ), body: Stack( children: [ diff --git a/lib/timer/screens/presets_screen.dart b/lib/timer/screens/presets_screen.dart index 61d045ef..f928aff1 100644 --- a/lib/timer/screens/presets_screen.dart +++ b/lib/timer/screens/presets_screen.dart @@ -23,7 +23,7 @@ class _PresetsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppTopBar( - title: Text( + titleWidget: Text( AppLocalizations.of(context)!.editPresetsTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: diff --git a/lib/timer/screens/timer_fullscreen.dart b/lib/timer/screens/timer_fullscreen.dart index ea6a0a5d..1e37b4c0 100644 --- a/lib/timer/screens/timer_fullscreen.dart +++ b/lib/timer/screens/timer_fullscreen.dart @@ -66,7 +66,7 @@ class _TimerFullscreenState extends State { // Orientation orientation = MediaQuery.of(context).orientation; return Scaffold( appBar: AppTopBar( - title: Text(timer.label, + titleWidget: Text(timer.label, style: textTheme.titleMedium?.copyWith( color: colorScheme.onBackground.withOpacity(0.6), )), diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index ebccd870..658b4d47 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -23,7 +23,6 @@ import 'package:clock_app/timer/widgets/timer_duration_picker.dart'; import 'package:clock_app/timer/widgets/timer_picker.dart'; import 'package:flutter/material.dart'; // import 'package:flutter_foreground_task/flutter_foreground_task.dart'; -import 'package:great_list_view/great_list_view.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/timer/types/timer.dart'; @@ -109,11 +108,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // } // } -typedef TimerCardBuilder = Widget Function( - BuildContext context, - int index, - AnimatedWidgetBuilderData data, -); class TimerScreen extends StatefulWidget { const TimerScreen({super.key, this.actionController}); diff --git a/pubspec.lock b/pubspec.lock index 8bf03457..5d4e666d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,7 +30,7 @@ packages: description: path: "packages/android_alarm_manager_plus" ref: alarm_show_intent - resolved-ref: "2b9a1f38f0f5f647153507cae3a0f00c896fd06a" + resolved-ref: ae6c11c3eccd66ff79827424cbf8c208f727282e url: "https://github.com/AhsanSarwar45/plus_plugins" source: git version: "4.0.1" @@ -242,14 +242,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - diffutil_dart: - dependency: transitive - description: - name: diffutil_dart - sha256: e0297e4600b9797edff228ed60f4169a778ea357691ec98408fa3b72994c7d06 - url: "https://pub.dev" - source: hosted - version: "3.0.0" dots_indicator: dependency: transitive description: @@ -520,15 +512,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - great_list_view: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: "6fb91f6f3b2480c490bfa57cb7a8b18e4db2c117" - url: "https://github.com/AhsanSarwar45/great_list_view" - source: git - version: "0.2.3" home_widget: dependency: "direct main" description: @@ -1263,14 +1246,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - worker_manager: - dependency: transitive - description: - name: worker_manager - sha256: "42501e49ee0acad9eeda562984e3dcfe6fe3d26f2d8dc410bd76308a86447eb5" - url: "https://pub.dev" - source: hosted - version: "5.0.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6b5b840c..3d062a74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: sqflite: ^2.2.2 flutter_slidable: ^3.1.0 flutter_system_ringtones: ^0.0.6 - # android_alarm_manager_plus: ^4.0.1 + # android_alarm_manager_plus: ^4.0.4 android_alarm_manager_plus: # path: "../plus_plugins/packages/android_alarm_manager_plus/" git: @@ -39,12 +39,6 @@ dependencies: flutter_fgbg: ^0.3.0 move_to_background: ^1.0.2 vibration: ^1.7.6 - great_list_view: - git: - url: https://github.com/AhsanSarwar45/great_list_view - ref: master - # great_list_view: - # path: "../great_list_view" get_storage: ^2.1.1 queue: ^3.1.0+2 table_calendar: ^3.0.8 @@ -92,7 +86,6 @@ dependencies: background_fetch: ^1.3.7 clock: ^1.1.1 mime: ^1.0.6 - # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From 45b205839d844e4f95ca7c855f9db0f150340cb3 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 14 Sep 2024 16:24:46 +0500 Subject: [PATCH 159/177] Add analog clock --- .../data/alarm_app_settings_schema.dart | 0 lib/alarm/logic/alarm_isolate.dart | 2 +- lib/alarm/logic/alarm_time.dart | 2 +- lib/alarm/logic/schedule_alarm.dart | 2 +- lib/alarm/screens/alarm_events_screen.dart | 2 +- .../screens/alarm_notification_screen.dart | 6 +- lib/alarm/screens/alarm_screen.dart | 3 +- lib/alarm/types/alarm.dart | 1 + lib/alarm/widgets/alarm_card.dart | 4 +- lib/alarm/widgets/alarm_time_picker.dart | 6 +- lib/app.dart | 8 +- lib/audio/logic/ringtones.dart | 2 +- lib/audio/types/ringtone_player.dart | 2 +- lib/clock/data/clock_settings_schema.dart | 104 +++++++ lib/clock/logic/timezone_database.dart | 2 +- lib/clock/screens/clock_screen.dart | 62 +++-- lib/clock/widgets/timezone_card.dart | 4 +- lib/clock/widgets/timezone_card_content.dart | 4 +- lib/clock/widgets/timezone_search_card.dart | 5 +- lib/common/logic/card_decoration.dart | 9 +- lib/common/logic/show_select.dart | 2 +- lib/common/types/clock_settings_types.dart | 13 + lib/common/types/list_filter.dart | 2 +- lib/common/utils/json_serialize.dart | 2 +- lib/common/utils/list_storage.dart | 2 +- lib/common/utils/snackbar.dart | 34 ++- lib/common/widgets/card_container.dart | 26 +- .../clock/analog_clock/analog_clock.dart | 68 +++++ .../analog_clock/analog_clock_display.dart | 84 ++++++ .../analog_clock/analog_clock_painter.dart | 254 ++++++++++++++++++ lib/common/widgets/clock/clock.dart | 48 ---- lib/common/widgets/clock/clock_display.dart | 145 ---------- lib/common/widgets/clock/digital_clock.dart | 50 ++++ .../widgets/clock/digital_clock_display.dart | 145 ++++++++++ lib/common/widgets/fab.dart | 18 +- .../builder/motion_animated_builder.dart | 2 +- lib/common/widgets/list/list_filter_chip.dart | 19 ++ .../widgets/list/persistent_list_view.dart | 3 +- .../data/developer_settings_schema.dart | 2 +- .../data/log_list_filters.dart | 2 +- .../data/log_sort_options.dart | 2 +- lib/{debug => developer}/logic/logger.dart | 4 +- .../screens/logs_screen.dart | 10 +- .../types/file_logger_output.dart | 0 lib/{debug => developer}/types/log.dart | 0 .../types/log_filter.dart | 0 .../widgets/log_card.dart | 2 +- lib/l10n/app_en.arb | 20 +- lib/main.dart | 8 +- lib/navigation/screens/nav_scaffold.dart | 65 ++--- lib/navigation/widgets/app_top_bar.dart | 7 +- .../logic/alarm_notifications.dart | 2 +- lib/settings/data/localized_names.dart | 29 -- lib/settings/data/settings_schema.dart | 16 +- lib/settings/screens/backup_screen.dart | 2 +- lib/settings/screens/ringtones_screen.dart | 2 +- .../types/setting_enable_condition.dart | 9 +- lib/settings/types/setting_group.dart | 4 +- lib/settings/types/setting_item.dart | 3 - .../data/stopwatch_settings_schema.dart | 0 lib/system/logic/background_service.dart | 2 +- lib/system/logic/handle_boot.dart | 2 +- lib/system/logic/handle_intents.dart | 2 +- .../logic/initialize_isolate_ports.dart | 2 +- lib/system/types/android_platform_file.dart | 2 +- .../data/appearance_settings_schema.dart | 4 +- lib/theme/logic/theme_extension.dart | 6 + lib/theme/theme.dart | 2 +- lib/theme/types/dark_mode.dart | 1 + lib/theme/types/theme_brightness.dart | 1 + lib/theme/types/theme_extension.dart | 31 +++ lib/theme/utils/color_scheme.dart | 27 +- .../data/timer_app_settings_schema.dart | 0 .../screens/timer_notification_screen.dart | 2 +- lib/timer/screens/timer_screen.dart | 2 +- .../data/widget_settings_schema.dart | 0 lib/widgets/logic/update_widgets.dart | 2 +- pubspec.lock | 20 +- pubspec.yaml | 2 + 79 files changed, 1033 insertions(+), 413 deletions(-) rename lib/{settings => alarm}/data/alarm_app_settings_schema.dart (100%) create mode 100644 lib/clock/data/clock_settings_schema.dart create mode 100644 lib/common/types/clock_settings_types.dart create mode 100644 lib/common/widgets/clock/analog_clock/analog_clock.dart create mode 100644 lib/common/widgets/clock/analog_clock/analog_clock_display.dart create mode 100644 lib/common/widgets/clock/analog_clock/analog_clock_painter.dart delete mode 100644 lib/common/widgets/clock/clock.dart delete mode 100644 lib/common/widgets/clock/clock_display.dart create mode 100644 lib/common/widgets/clock/digital_clock.dart create mode 100644 lib/common/widgets/clock/digital_clock_display.dart rename lib/{settings => developer}/data/developer_settings_schema.dart (96%) rename lib/{debug => developer}/data/log_list_filters.dart (95%) rename lib/{debug => developer}/data/log_sort_options.dart (89%) rename lib/{debug => developer}/logic/logger.dart (84%) rename lib/{debug => developer}/screens/logs_screen.dart (94%) rename lib/{debug => developer}/types/file_logger_output.dart (100%) rename lib/{debug => developer}/types/log.dart (100%) rename lib/{debug => developer}/types/log_filter.dart (100%) rename lib/{debug => developer}/widgets/log_card.dart (97%) delete mode 100644 lib/settings/data/localized_names.dart rename lib/{settings => stopwatch}/data/stopwatch_settings_schema.dart (100%) rename lib/{settings => theme}/data/appearance_settings_schema.dart (98%) create mode 100644 lib/theme/logic/theme_extension.dart create mode 100644 lib/theme/types/dark_mode.dart create mode 100644 lib/theme/types/theme_brightness.dart rename lib/{settings => timer}/data/timer_app_settings_schema.dart (100%) rename lib/{settings => widgets}/data/widget_settings_schema.dart (100%) diff --git a/lib/settings/data/alarm_app_settings_schema.dart b/lib/alarm/data/alarm_app_settings_schema.dart similarity index 100% rename from lib/settings/data/alarm_app_settings_schema.dart rename to lib/alarm/data/alarm_app_settings_schema.dart diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart index 30f179ba..62f868d7 100644 --- a/lib/alarm/logic/alarm_isolate.dart +++ b/lib/alarm/logic/alarm_isolate.dart @@ -5,7 +5,7 @@ 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/debug/logic/logger.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/timer.dart'; diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart index cac48ec0..02bcf06d 100644 --- a/lib/alarm/logic/alarm_time.dart +++ b/lib/alarm/logic/alarm_time.dart @@ -1,6 +1,6 @@ import 'package:clock/clock.dart'; import 'package:clock_app/common/types/time.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; // Calculates the DateTime when the provided `time` will next occur DateTime getScheduleDateForTime( diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart index d8d42505..8062a423 100644 --- a/lib/alarm/logic/schedule_alarm.dart +++ b/lib/alarm/logic/schedule_alarm.dart @@ -8,7 +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/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; Future scheduleAlarm( diff --git a/lib/alarm/screens/alarm_events_screen.dart b/lib/alarm/screens/alarm_events_screen.dart index b280c2ac..b76fb4d8 100644 --- a/lib/alarm/screens/alarm_events_screen.dart +++ b/lib/alarm/screens/alarm_events_screen.dart @@ -10,7 +10,7 @@ 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/debug/logic/logger.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'; diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 16409e40..33dd8f6b 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -4,8 +4,8 @@ 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/debug/logic/logger.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/logic/alarm_notifications.dart'; import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; @@ -127,7 +127,7 @@ class _AlarmNotificationScreenState extends State { style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), - const Clock( + const DigitalClock( // dateTime: Date, horizontalAlignment: ElementAlignment.center, shouldShowDate: false, diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart index 7fbfefc8..df76acd3 100644 --- a/lib/alarm/screens/alarm_screen.dart +++ b/lib/alarm/screens/alarm_screen.dart @@ -129,7 +129,8 @@ class _AlarmScreenState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar(getSnackbar( + ScaffoldMessenger.of(context).showSnackBar(getThemedSnackBar( + context, getNewAlarmText(context, alarm), fab: true, navBar: true)); diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index 66797ac2..bd23d5df 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -307,6 +307,7 @@ class Alarm extends CustomizableListItem { } void handleDismiss() { + _snoozeCount = 0; if (scheduleType == OnceAlarmSchedule && shouldDeleteAfterRinging || shouldDeleteAfterFinish && isFinished) { _markedForDeletion = true; 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_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/app.dart b/lib/app.dart index 9182ab7f..a15cd55b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,7 +8,6 @@ import 'package:clock_app/notifications/data/update_notification_intervals.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,6 +15,7 @@ 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'; @@ -162,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 index e56d6cba..18646ff0 100644 --- a/lib/audio/logic/ringtones.dart +++ b/lib/audio/logic/ringtones.dart @@ -4,7 +4,7 @@ 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/debug/logic/logger.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'; diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart index 8d1314c5..126870bf 100644 --- a/lib/audio/types/ringtone_player.dart +++ b/lib/audio/types/ringtone_player.dart @@ -4,7 +4,7 @@ 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/debug/logic/logger.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'; 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 ab490e1a..cf9ac33c 100644 --- a/lib/clock/logic/timezone_database.dart +++ b/lib/clock/logic/timezone_database.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/clock/screens/clock_screen.dart b/lib/clock/screens/clock_screen.dart index 38668467..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,8 +100,7 @@ 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/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/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart index 71e84180..e2fdfe93 100644 --- a/lib/common/logic/card_decoration.dart +++ b/lib/common/logic/card_decoration.dart @@ -7,6 +7,7 @@ BoxDecoration getCardDecoration(BuildContext context, bool isSelected = false, showShadow = true, elevationMultiplier = 1, + boxShape = BoxShape.rectangle, blurStyle = BlurStyle.normal}) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; @@ -15,7 +16,7 @@ BoxDecoration getCardDecoration(BuildContext context, return BoxDecoration( border: isSelected ? Border.all( color: colorScheme.primary, - width: 1, + width: 2, strokeAlign: BorderSide.strokeAlignOutside ) : showLightBorder @@ -32,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 c8ddcb5b..316895e3 100644 --- a/lib/common/logic/show_select.dart +++ b/lib/common/logic/show_select.dart @@ -1,7 +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/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/material.dart'; Future showSelectBottomSheet( 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/list_filter.dart b/lib/common/types/list_filter.dart index aefdafa4..d5dd69d7 100644 --- a/lib/common/types/list_filter.dart +++ b/lib/common/types/list_filter.dart @@ -1,6 +1,6 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/id.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/common/utils/json_serialize.dart b/lib/common/utils/json_serialize.dart index 562335aa..836b0269 100644 --- a/lib/common/utils/json_serialize.dart +++ b/lib/common/utils/json_serialize.dart @@ -9,7 +9,7 @@ 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/debug/logic/logger.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'; diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart index a2d29329..d3584759 100644 --- a/lib/common/utils/list_storage.dart +++ b/lib/common/utils/list_storage.dart @@ -5,7 +5,7 @@ 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:clock_app/debug/logic/logger.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:queue/queue.dart'; diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart index 81cb801a..5cf3330f 100644 --- a/lib/common/utils/snackbar.dart +++ b/lib/common/utils/snackbar.dart @@ -1,4 +1,5 @@ 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, @@ -6,16 +7,41 @@ void showSnackBar(BuildContext context, String text, 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, color: color, duration: duration)); + fab: fab, + navBar: navBar, + color: color, + useMaterialStyle: themeSettings.useMaterialStyle, + duration: duration)); +} + +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; @@ -38,12 +64,6 @@ SnackBar getSnackbar(String text, bottom = 4; } - final useMaterialStyle = appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Use Material Style") - .value; - if (useMaterialStyle) { bottom += 20; } diff --git a/lib/common/widgets/card_container.dart b/lib/common/widgets/card_container.dart index 0cf3bea9..eab7ca13 100644 --- a/lib/common/widgets/card_container.dart +++ b/lib/common/widgets/card_container.dart @@ -1,5 +1,6 @@ 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'; @@ -11,17 +12,14 @@ TonalPalette toTonalPalette(int value) { } Color getCardColor(BuildContext context, [Color? color]) { - ColorScheme colorScheme = Theme.of(context).colorScheme; - bool useMaterialYou = appSettings - .getGroup("Appearance") - .getGroup("Colors") - .getSetting("Use Material You") - .value; + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + ThemeSettingExtension themeStyle = theme.extension()!; TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); return color ?? - (useMaterialYou + (themeStyle.useMaterialYou ? Color(tonalPalette .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) : colorScheme.surface); @@ -39,7 +37,8 @@ class CardContainer extends StatelessWidget { this.showShadow = true, this.isSelected = false, this.showLightBorder = false, - this.blurStyle = BlurStyle.normal, this.onLongPress, + this.blurStyle = BlurStyle.normal, + this.onLongPress, }); final Widget child; @@ -54,15 +53,6 @@ class CardContainer extends StatelessWidget { final bool showLightBorder; final bool isSelected; - // 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); - // } - @override Widget build(BuildContext context) { Color cardColor = getCardColor(context, color); @@ -87,7 +77,7 @@ class CardContainer extends StatelessWidget { : Material( color: Colors.transparent, child: InkWell( - onLongPress: onLongPress, + 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/list/animated_reorderable_list/builder/motion_animated_builder.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart index 5f2a1280..8a6e0fb5 100644 --- a/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart +++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart'; import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart index 4a1350ff..73b19f64 100644 --- a/lib/common/widgets/list/list_filter_chip.dart +++ b/lib/common/widgets/list/list_filter_chip.dart @@ -14,10 +14,13 @@ class ListFilterChip extends StatelessWidget { super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, + }); final ListFilter listFilter; final VoidCallback onChange; + final bool isEnabled; @override Widget build(BuildContext context) { @@ -57,12 +60,16 @@ class ListButtonChip extends StatelessWidget { this.onTap, required this.icon, this.isActive = false, + this.isEnabled = true, + }); final String? label; final IconData? icon; final Function()? onTap; final bool isActive; + final bool isEnabled; + @override Widget build(BuildContext context) { @@ -113,10 +120,12 @@ class ListFilterActionChip extends StatelessWidget { super.key, required this.actions, required this.activeFilterCount, + this.isEnabled = true, }); final List actions; final int activeFilterCount; + final bool isEnabled; void _showPopupMenu(BuildContext context) async { await showModalBottomSheet>( @@ -179,11 +188,15 @@ class ListFilterActionChip extends StatelessWidget { class ListFilterSelectChip extends StatelessWidget { final FilterSelect listFilter; final VoidCallback onChange; + final bool isEnabled; + const ListFilterSelectChip({ super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, + }); @override @@ -249,11 +262,14 @@ class ListFilterSelectChip extends StatelessWidget { class ListFilterMultiSelectChip extends StatelessWidget { final FilterMultiSelect listFilter; final VoidCallback onChange; + final bool isEnabled; + const ListFilterMultiSelectChip({ super.key, required this.listFilter, required this.onChange, + this.isEnabled = true, }); @override @@ -323,12 +339,15 @@ class ListSortChip extends StatelessWidget { final List sortOptions; final Function(int) onChange; final int selectedIndex; + final bool isEnabled; const ListSortChip({ super.key, required this.sortOptions, required this.onChange, required this.selectedIndex, + this.isEnabled = true, + }); @override diff --git a/lib/common/widgets/list/persistent_list_view.dart b/lib/common/widgets/list/persistent_list_view.dart index 66bc2237..09636b20 100644 --- a/lib/common/widgets/list/persistent_list_view.dart +++ b/lib/common/widgets/list/persistent_list_view.dart @@ -3,7 +3,7 @@ import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; import 'package:flutter/material.dart'; @@ -150,7 +150,6 @@ class _PersistentListViewState void _loadItems() { if (widget.saveTag.isNotEmpty) { - logger.d('Loading items from ${widget.saveTag}'); widget.listController.changeItems( (List items) { List newList = loadListSync(widget.saveTag); diff --git a/lib/settings/data/developer_settings_schema.dart b/lib/developer/data/developer_settings_schema.dart similarity index 96% rename from lib/settings/data/developer_settings_schema.dart rename to lib/developer/data/developer_settings_schema.dart index 81a16e2f..6fb7e26c 100644 --- a/lib/settings/data/developer_settings_schema.dart +++ b/lib/developer/data/developer_settings_schema.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:clock_app/alarm/screens/alarm_events_screen.dart'; -import 'package:clock_app/debug/screens/logs_screen.dart'; +import 'package:clock_app/developer/screens/logs_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; diff --git a/lib/debug/data/log_list_filters.dart b/lib/developer/data/log_list_filters.dart similarity index 95% rename from lib/debug/data/log_list_filters.dart rename to lib/developer/data/log_list_filters.dart index 4af09485..0da6006d 100644 --- a/lib/debug/data/log_list_filters.dart +++ b/lib/developer/data/log_list_filters.dart @@ -1,7 +1,7 @@ import 'package:clock_app/alarm/types/alarm_event.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/utils/date_time.dart'; -import 'package:clock_app/debug/types/log.dart'; +import 'package:clock_app/developer/types/log.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logger/logger.dart'; diff --git a/lib/debug/data/log_sort_options.dart b/lib/developer/data/log_sort_options.dart similarity index 89% rename from lib/debug/data/log_sort_options.dart rename to lib/developer/data/log_sort_options.dart index d5a860ec..f398cadb 100644 --- a/lib/debug/data/log_sort_options.dart +++ b/lib/developer/data/log_sort_options.dart @@ -1,5 +1,5 @@ import 'package:clock_app/common/types/list_filter.dart'; -import 'package:clock_app/debug/types/log.dart'; +import 'package:clock_app/developer/types/log.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; final List> logSortOptions = [ diff --git a/lib/debug/logic/logger.dart b/lib/developer/logic/logger.dart similarity index 84% rename from lib/debug/logic/logger.dart rename to lib/developer/logic/logger.dart index 5fc5077e..e8a68b37 100644 --- a/lib/debug/logic/logger.dart +++ b/lib/developer/logic/logger.dart @@ -1,5 +1,5 @@ -import 'package:clock_app/debug/types/file_logger_output.dart'; -import 'package:clock_app/debug/types/log_filter.dart'; +import 'package:clock_app/developer/types/file_logger_output.dart'; +import 'package:clock_app/developer/types/log_filter.dart'; import 'package:logger/logger.dart'; import 'dart:isolate'; diff --git a/lib/debug/screens/logs_screen.dart b/lib/developer/screens/logs_screen.dart similarity index 94% rename from lib/debug/screens/logs_screen.dart rename to lib/developer/screens/logs_screen.dart index bd5896f2..70437974 100644 --- a/lib/debug/screens/logs_screen.dart +++ b/lib/developer/screens/logs_screen.dart @@ -8,11 +8,11 @@ import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/list/custom_list_view.dart'; import 'package:clock_app/common/widgets/list/static_list_view.dart'; -import 'package:clock_app/debug/data/log_list_filters.dart'; -import 'package:clock_app/debug/data/log_sort_options.dart'; -import 'package:clock_app/debug/logic/logger.dart'; -import 'package:clock_app/debug/types/log.dart'; -import 'package:clock_app/debug/widgets/log_card.dart'; +import 'package:clock_app/developer/data/log_list_filters.dart'; +import 'package:clock_app/developer/data/log_sort_options.dart'; +import 'package:clock_app/developer/logic/logger.dart'; +import 'package:clock_app/developer/types/log.dart'; +import 'package:clock_app/developer/widgets/log_card.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/types/setting_item.dart'; diff --git a/lib/debug/types/file_logger_output.dart b/lib/developer/types/file_logger_output.dart similarity index 100% rename from lib/debug/types/file_logger_output.dart rename to lib/developer/types/file_logger_output.dart diff --git a/lib/debug/types/log.dart b/lib/developer/types/log.dart similarity index 100% rename from lib/debug/types/log.dart rename to lib/developer/types/log.dart diff --git a/lib/debug/types/log_filter.dart b/lib/developer/types/log_filter.dart similarity index 100% rename from lib/debug/types/log_filter.dart rename to lib/developer/types/log_filter.dart diff --git a/lib/debug/widgets/log_card.dart b/lib/developer/widgets/log_card.dart similarity index 97% rename from lib/debug/widgets/log_card.dart rename to lib/developer/widgets/log_card.dart index 7977bcd0..c0b201a8 100644 --- a/lib/debug/widgets/log_card.dart +++ b/lib/developer/widgets/log_card.dart @@ -1,4 +1,4 @@ -import 'package:clock_app/debug/types/log.dart'; +import 'package:clock_app/developer/types/log.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:logger/logger.dart'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 46c37312..8fdf231e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -752,5 +752,23 @@ "notificationPermissionDescription": "Allow notifications to be showed", "@notificationPermissionDescription": {}, "extraAnimationSettingDescription": "Show animations that are not polished and might cause frame drops in low-end devices", - "@extraAnimationSettingDescription": {} + "@extraAnimationSettingDescription": {}, + "clockStyleSettingGroup": "Clock Style", + "@clockStyleSettingGroup": {}, + "clockTypeSetting": "Clock type", + "@clockTypeSetting": {}, + "analogClock": "Analog", + "digitalClock": "Digital", + "showClockTicksSetting": "Show ticks", + "majorTicks": "Major ticks only", + "allTicks": "All ticks", + "showNumbersSetting": "Show numbers", + "quarterNumbers": "Quarter numbers only", + "allNumbers": "All numbers", + "none": "None", + "numeralTypeSetting": "Numeral type", + "romanNumeral": "Roman", + "arabicNumeral": "Arabic", + "showDigitalClock": "Show digital clock" + } diff --git a/lib/main.dart b/lib/main.dart index 7b104dc1..8e5dc5a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:clock_app/app.dart'; import 'package:clock_app/audio/logic/audio_session.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/data/paths.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; import 'package:clock_app/notifications/logic/foreground_task.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; @@ -23,9 +23,9 @@ import 'package:flutter_show_when_locked/flutter_show_when_locked.dart'; import 'package:timezone/data/latest_all.dart'; void main() async { - FlutterError.onError = (FlutterErrorDetails details) { - logger.e(details.exception.toString(), stackTrace: details.stack,); - }; + // FlutterError.onError = (FlutterErrorDetails details) { + // logger.e(details.exception.toString(), stackTrace: details.stack,); + // }; WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/navigation/screens/nav_scaffold.dart b/lib/navigation/screens/nav_scaffold.dart index ca307c73..2a40b788 100644 --- a/lib/navigation/screens/nav_scaffold.dart +++ b/lib/navigation/screens/nav_scaffold.dart @@ -16,6 +16,7 @@ import 'package:clock_app/settings/screens/settings_group_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/system/logic/handle_intents.dart'; import 'package:clock_app/system/logic/quick_actions.dart'; +import 'package:clock_app/theme/types/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -65,10 +66,8 @@ class NavScaffold extends StatefulWidget { class _NavScaffoldState extends State { late int _selectedTabIndex; - late Setting useMaterialNavBarSetting; late Setting swipeActionSetting; late Setting showForegroundSetting; - late Setting useMaterialYouSetting; late StreamSubscription _sub; late PageController _controller; QuickActionController quickActionController = QuickActionController(); @@ -103,10 +102,9 @@ class _NavScaffoldState extends State { ScaffoldMessenger.of(context).removeCurrentSnackBar(); DateTime? nextScheduleDateTime = alarm.currentScheduleDateTime; if (nextScheduleDateTime == null) return; - ScaffoldMessenger.of(context).showSnackBar(getSnackbar( - getNewAlarmText(context, alarm), - fab: true, - navBar: true)); + ScaffoldMessenger.of(context).showSnackBar(getThemedSnackBar( + context, getNewAlarmText(context, alarm), + fab: true, navBar: true)); }); } @@ -159,14 +157,7 @@ class _NavScaffoldState extends State { super.initState(); initializeQuickActions(context, _onTabSelected); initReceiveIntent(); - useMaterialNavBarSetting = appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Use Material Style"); - useMaterialYouSetting = appSettings - .getGroup("Appearance") - .getGroup("Colors") - .getSetting("Use Material You"); + swipeActionSetting = appSettings.getGroup("General").getSetting("Swipe Action"); showForegroundSetting = appSettings @@ -174,8 +165,6 @@ class _NavScaffoldState extends State { .getGroup("Reliability") .getSetting("Show Foreground Notification"); swipeActionSetting.addListener(update); - useMaterialNavBarSetting.addListener(update); - useMaterialYouSetting.addListener(update); showForegroundSetting.addListener(_updateForegroundNotification); _controller = PageController(initialPage: widget.initialTabIndex); _selectedTabIndex = widget.initialTabIndex; @@ -185,8 +174,6 @@ class _NavScaffoldState extends State { @override void dispose() { - useMaterialNavBarSetting.removeListener(update); - useMaterialYouSetting.removeListener(update); swipeActionSetting.removeListener(update); showForegroundSetting.removeListener(_updateForegroundNotification); _sub.cancel(); @@ -200,15 +187,16 @@ class _NavScaffoldState extends State { final tabs = getTabs(context, quickActionController); ThemeData theme = Theme.of(context); TextTheme textTheme = theme.textTheme; + ThemeSettingExtension themeSettings = + theme.extension()!; ColorScheme colorScheme = theme.colorScheme; - TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); + TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value); - Color materialNavColor = - useMaterialYouSetting.value - ? Color(tonalPalette - .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) - : colorScheme.surface; + Color materialNavColor = themeSettings.useMaterialYou + ? Color(tonalPalette + .get(Theme.of(context).brightness == Brightness.light ? 96 : 15)) + : colorScheme.surface; return WithForegroundTask( child: Scaffold( @@ -217,13 +205,11 @@ class _NavScaffoldState extends State { titleWidget: Text( tabs[_selectedTabIndex].title, style: textTheme.titleMedium?.copyWith( - color: colorScheme - .onBackground - .withOpacity(0.6), - ), - + color: colorScheme.onBackground.withOpacity(0.6), + ), ), - systemNavBarColor: useMaterialNavBarSetting.value ? materialNavColor : null, + systemNavBarColor: + themeSettings.useMaterialStyle ? materialNavColor : null, actions: [ IconButton( onPressed: () { @@ -236,15 +222,13 @@ class _NavScaffoldState extends State { }, icon: const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: colorScheme - .onBackground - .withOpacity(0.8), + color: colorScheme.onBackground.withOpacity(0.8), ), ], ) : null, bottomNavigationBar: orientation == Orientation.portrait - ? useMaterialNavBarSetting.value + ? themeSettings.useMaterialStyle ? NavigationBar( labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, @@ -278,12 +262,9 @@ class _NavScaffoldState extends State { ) ], leading: Text(tabs[_selectedTabIndex].title, - style: - textTheme.headlineSmall?.copyWith( - color: colorScheme - .onBackground - .withOpacity(0.6), - )), + style: textTheme.headlineSmall?.copyWith( + color: colorScheme.onBackground.withOpacity(0.6), + )), trailing: IconButton( onPressed: () { ScaffoldMessenger.of(context).removeCurrentSnackBar(); @@ -295,9 +276,7 @@ class _NavScaffoldState extends State { }, icon: const Icon(FluxIcons.settings, semanticLabel: "Settings"), - color: colorScheme - .onBackground - .withOpacity(0.8), + color: colorScheme.onBackground.withOpacity(0.8), ), selectedIndex: _selectedTabIndex, onDestinationSelected: _onTabSelected, diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index 4eb64efb..1857f898 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -1,3 +1,4 @@ +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,6 +26,8 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; + final bool showBackButton = Navigator.of(context).canPop(); + final systemNavigationBarColor = systemNavBarColor ?? colorScheme.background; @@ -67,7 +70,7 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (Navigator.of(context).canPop()) ...[ + if (showBackButton) ...[ IconButton( icon: Icon(Icons.arrow_back, color: colorScheme.onSurface.withOpacity(0.8)), @@ -75,7 +78,7 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { padding: EdgeInsets.zero), const SizedBox(width: 8) ], - if (!Navigator.of(context).canPop()) const SizedBox(width: 16), + if (!showBackButton) const SizedBox(width: 16), if (barTitleWidget != null) barTitleWidget, ], ), diff --git a/lib/notifications/logic/alarm_notifications.dart b/lib/notifications/logic/alarm_notifications.dart index 94eda92d..79513988 100644 --- a/lib/notifications/logic/alarm_notifications.dart +++ b/lib/notifications/logic/alarm_notifications.dart @@ -7,7 +7,7 @@ import 'package:clock_app/alarm/logic/alarm_isolate.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/common/types/notification_type.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/notifications/data/action_keys.dart'; import 'package:clock_app/notifications/data/fullscreen_notification_data.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; diff --git a/lib/settings/data/localized_names.dart b/lib/settings/data/localized_names.dart deleted file mode 100644 index 06845ff6..00000000 --- a/lib/settings/data/localized_names.dart +++ /dev/null @@ -1,29 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// -// String getLocalizedSettingName(String name, BuildContext context) { -// switch (name) { -// case "General": -// return AppLocalizations.of(context)!.generalSettingGroup; -// case "Appearance": -// return AppLocalizations.of(context)!.appearanceSettingGroup; -// case "Alarm": -// return AppLocalizations.of(context)!.alarmTitle; -// case "Timer": -// return AppLocalizations.of(context)!.timerTitle; -// case "Stopwatch": -// return AppLocalizations.of(context)!.stopwatchTitle; -// case "Clock": -// return AppLocalizations.of(context)!.clockTitle; -// case "Developer Options" -// default: -// return name; -// } -// } -// -// String getLocalizedSettingDescription(String name, BuildContext context) { -// switch (name) { -// default: -// return name; -// } -// } diff --git a/lib/settings/data/settings_schema.dart b/lib/settings/data/settings_schema.dart index e9706cfa..3df580f2 100644 --- a/lib/settings/data/settings_schema.dart +++ b/lib/settings/data/settings_schema.dart @@ -1,21 +1,22 @@ +import 'package:clock_app/alarm/data/alarm_app_settings_schema.dart'; +import 'package:clock_app/clock/data/clock_settings_schema.dart'; +import 'package:clock_app/developer/data/developer_settings_schema.dart'; import 'package:clock_app/settings/data/accessibility_settings_schema.dart'; -import 'package:clock_app/settings/data/alarm_app_settings_schema.dart'; -import 'package:clock_app/settings/data/appearance_settings_schema.dart'; import 'package:clock_app/settings/data/backup_settings_schema.dart'; -import 'package:clock_app/settings/data/developer_settings_schema.dart'; import 'package:clock_app/settings/data/general_settings_schema.dart'; -import 'package:clock_app/settings/data/stopwatch_settings_schema.dart'; -import 'package:clock_app/settings/data/timer_app_settings_schema.dart'; -import 'package:clock_app/settings/data/widget_settings_schema.dart'; import 'package:clock_app/settings/screens/about_screen.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; +import 'package:clock_app/stopwatch/data/stopwatch_settings_schema.dart'; +import 'package:clock_app/theme/data/appearance_settings_schema.dart'; +import 'package:clock_app/timer/data/timer_app_settings_schema.dart'; +import 'package:clock_app/widgets/data/widget_settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Increment this after every schema change -const int settingsSchemaVersion = 6; +const int settingsSchemaVersion = 7; SettingGroup appSettings = SettingGroup( "Settings", @@ -26,6 +27,7 @@ SettingGroup appSettings = SettingGroup( generalSettingsSchema, appearanceSettingsSchema, alarmAppSettingsSchema, + clockSettingsSchema, timerAppSettingsSchema, stopwatchSettingsSchema, widgetSettingSchema, diff --git a/lib/settings/screens/backup_screen.dart b/lib/settings/screens/backup_screen.dart index 72fbc347..c6762786 100644 --- a/lib/settings/screens/backup_screen.dart +++ b/lib/settings/screens/backup_screen.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/snackbar.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; import 'package:clock_app/settings/data/backup_options.dart'; import 'package:clock_app/settings/logic/backup.dart'; diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/settings/screens/ringtones_screen.dart index 839438ac..340c8f08 100644 --- a/lib/settings/screens/ringtones_screen.dart +++ b/lib/settings/screens/ringtones_screen.dart @@ -5,7 +5,7 @@ import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/file_item_card.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; -import 'package:clock_app/debug/logic/logger.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'; diff --git a/lib/settings/types/setting_enable_condition.dart b/lib/settings/types/setting_enable_condition.dart index 439ab64b..17248d12 100644 --- a/lib/settings/types/setting_enable_condition.dart +++ b/lib/settings/types/setting_enable_condition.dart @@ -4,13 +4,13 @@ import 'package:clock_app/settings/types/setting_item.dart'; // TODO: OMG ALL THESE NAMES ARE SO BAD, PLEASE THINK OF NEW ONES :( +// Allows us to check conditions for enabling settings abstract class EnableConditionParameter { void setupEnableSettings(SettingGroup group, SettingItem item); void setupChangesEnableCondition(SettingGroup group, SettingItem item); EnableConditionEvaluator getEvaluator(SettingGroup group); } - class GeneralCondition extends EnableConditionParameter { bool Function() condition; @@ -18,17 +18,16 @@ class GeneralCondition extends EnableConditionParameter { @override EnableConditionEvaluator getEvaluator(SettingGroup group) { - return GeneralConditionEvaluator (condition); + return GeneralConditionEvaluator(condition); } @override void setupEnableSettings(SettingGroup group, SettingItem item) { item.enableSettings.add(getEvaluator(group)); - } + } @override - void setupChangesEnableCondition(SettingGroup group, SettingItem item) { - } + void setupChangesEnableCondition(SettingGroup group, SettingItem item) {} } class ValueCondition extends EnableConditionParameter { diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart index 75b6fee4..f72263f7 100644 --- a/lib/settings/types/setting_group.dart +++ b/lib/settings/types/setting_group.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/utils/list_storage.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; @@ -104,7 +104,7 @@ class SettingGroup extends SettingItem { try { return _settingGroups.firstWhere((item) => item.name == name); } catch (e) { - logger.e("Could not find setting group $name: $e"); + logger.e("Could not find setting group '$name'"); rethrow; } } diff --git a/lib/settings/types/setting_item.dart b/lib/settings/types/setting_item.dart index 8c34d768..49d162c3 100644 --- a/lib/settings/types/setting_item.dart +++ b/lib/settings/types/setting_item.dart @@ -1,4 +1,3 @@ -import 'package:clock_app/settings/data/localized_names.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; import 'package:clock_app/settings/types/setting_group.dart'; @@ -14,10 +13,8 @@ abstract class SettingItem { List get settingListeners => _settingListeners; List searchTags = []; List enableConditions; - // List compoundEnableConditions; // Settings which influence whether this setting is enabled List enableSettings; - // List compoundEnableSettings; String displayName(BuildContext context) => getLocalizedName(context); diff --git a/lib/settings/data/stopwatch_settings_schema.dart b/lib/stopwatch/data/stopwatch_settings_schema.dart similarity index 100% rename from lib/settings/data/stopwatch_settings_schema.dart rename to lib/stopwatch/data/stopwatch_settings_schema.dart diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart index 4f6860ec..179be98a 100644 --- a/lib/system/logic/background_service.dart +++ b/lib/system/logic/background_service.dart @@ -1,6 +1,6 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:clock_app/alarm/logic/update_alarms.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; diff --git a/lib/system/logic/handle_boot.dart b/lib/system/logic/handle_boot.dart index a994db9a..67c16986 100644 --- a/lib/system/logic/handle_boot.dart +++ b/lib/system/logic/handle_boot.dart @@ -1,5 +1,5 @@ import 'package:clock_app/alarm/logic/update_alarms.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; diff --git a/lib/system/logic/handle_intents.dart b/lib/system/logic/handle_intents.dart index 593badaf..f794e656 100644 --- a/lib/system/logic/handle_intents.dart +++ b/lib/system/logic/handle_intents.dart @@ -4,7 +4,7 @@ import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/alarm/types/schedules/weekly_alarm_schedule.dart'; import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/utils/list_storage.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/app_visibility.dart'; import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; diff --git a/lib/system/logic/initialize_isolate_ports.dart b/lib/system/logic/initialize_isolate_ports.dart index 674501c2..0722ca35 100644 --- a/lib/system/logic/initialize_isolate_ports.dart +++ b/lib/system/logic/initialize_isolate_ports.dart @@ -2,7 +2,7 @@ import 'dart:isolate'; import 'dart:ui'; import 'package:clock_app/alarm/logic/alarm_isolate.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/types/listener_manager.dart'; void initializeIsolatePorts(){ diff --git a/lib/system/types/android_platform_file.dart b/lib/system/types/android_platform_file.dart index 4fd971d9..e9a9af4b 100644 --- a/lib/system/types/android_platform_file.dart +++ b/lib/system/types/android_platform_file.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; diff --git a/lib/settings/data/appearance_settings_schema.dart b/lib/theme/data/appearance_settings_schema.dart similarity index 98% rename from lib/settings/data/appearance_settings_schema.dart rename to lib/theme/data/appearance_settings_schema.dart index d53a9431..9057c042 100644 --- a/lib/settings/data/appearance_settings_schema.dart +++ b/lib/theme/data/appearance_settings_schema.dart @@ -7,13 +7,11 @@ import 'package:clock_app/theme/screens/themes_screen.dart'; import 'package:clock_app/theme/theme.dart'; import 'package:clock_app/theme/types/color_scheme.dart'; import 'package:clock_app/theme/types/style_theme.dart'; +import 'package:clock_app/theme/types/theme_brightness.dart'; import 'package:clock_app/theme/utils/color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -enum ThemeBrightness { light, dark, system } - -enum DarkMode { user, system, nightDay } SettingGroup appearanceSettingsSchema = SettingGroup( "Appearance", diff --git a/lib/theme/logic/theme_extension.dart b/lib/theme/logic/theme_extension.dart new file mode 100644 index 00000000..2581ed35 --- /dev/null +++ b/lib/theme/logic/theme_extension.dart @@ -0,0 +1,6 @@ +// import 'package:flutter/material.dart'; +// +// T getThemeExtension (ThemeData theme) { +// return theme.extension() ?? const T(); +// +// } diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 81a390e5..8355f2b8 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -39,6 +39,6 @@ ThemeData defaultTheme = ThemeData( sliderTheme: getSliderTheme(defaultColorScheme), bottomSheetTheme: getBottomSheetTheme(defaultColorScheme, defaultStyleTheme), toggleButtonsTheme: toggleButtonsTheme, - extensions: const >[ThemeStyleExtension()], + extensions: const >[ThemeStyleExtension(), ThemeSettingExtension()], popupMenuTheme: getPopupMenuTheme(defaultColorScheme, defaultStyleTheme), ); diff --git a/lib/theme/types/dark_mode.dart b/lib/theme/types/dark_mode.dart new file mode 100644 index 00000000..94647f6a --- /dev/null +++ b/lib/theme/types/dark_mode.dart @@ -0,0 +1 @@ +enum DarkMode { user, system, nightDay } diff --git a/lib/theme/types/theme_brightness.dart b/lib/theme/types/theme_brightness.dart new file mode 100644 index 00000000..4660cd26 --- /dev/null +++ b/lib/theme/types/theme_brightness.dart @@ -0,0 +1 @@ +enum ThemeBrightness { light, dark, system } diff --git a/lib/theme/types/theme_extension.dart b/lib/theme/types/theme_extension.dart index cfbdad4f..87088f8c 100644 --- a/lib/theme/types/theme_extension.dart +++ b/lib/theme/types/theme_extension.dart @@ -55,3 +55,34 @@ class ThemeStyleExtension extends ThemeExtension { ); } } + +class ThemeSettingExtension extends ThemeExtension { + final bool useMaterialYou; + final bool useMaterialStyle; + + const ThemeSettingExtension({ + this.useMaterialYou = false, + this.useMaterialStyle = false, + }); + + @override + ThemeExtension copyWith({ + bool? useMaterialYou, + bool? useMaterialStyle, + }) { + return ThemeSettingExtension( + useMaterialYou: useMaterialYou ?? this.useMaterialYou, + useMaterialStyle: useMaterialStyle ?? this.useMaterialStyle); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, double t) { + if (other is! ThemeSettingExtension) return this; + + return ThemeSettingExtension( + useMaterialYou: t < 0.5 ? useMaterialYou : other.useMaterialYou, + useMaterialStyle: t < 0.5 ? useMaterialStyle : other.useMaterialStyle, + ); + } +} diff --git a/lib/theme/utils/color_scheme.dart b/lib/theme/utils/color_scheme.dart index 3d3cf4a9..e4319abb 100644 --- a/lib/theme/utils/color_scheme.dart +++ b/lib/theme/utils/color_scheme.dart @@ -1,4 +1,5 @@ import 'package:clock_app/settings/data/settings_schema.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/theme/bottom_sheet.dart'; import 'package:clock_app/theme/popup_menu.dart'; import 'package:clock_app/theme/slider.dart'; @@ -38,20 +39,25 @@ ThemeData getTheme( {ColorScheme? colorScheme, ColorSchemeData? colorSchemeData, StyleTheme? styleTheme}) { - styleTheme ??= appSettings - .getGroup("Appearance") - .getGroup("Style") - .getSetting("Style Theme") +SettingGroup appearanceSettings = appSettings + .getGroup("Appearance"); + SettingGroup colorSettings = appearanceSettings.getGroup("Colors"); + SettingGroup styleSettings = appearanceSettings.getGroup("Style"); + + styleTheme ??= styleSettings.getSetting("Style Theme") .value; colorSchemeData ??= colorScheme != null ? getColorSchemeData(colorScheme) - : appSettings - .getGroup("Appearance") - .getGroup("Colors") - .getSetting("Color Scheme") + : colorSettings.getSetting("Color Scheme") .value; + bool useMaterialYou = colorSettings.getSetting("Use Material You") + .value; + bool useMaterialStyle = styleSettings.getSetting("Use Material Style") + .value; + + if (styleTheme == null || colorSchemeData == null) { return defaultTheme; } @@ -95,6 +101,11 @@ ThemeData getTheme( borderWidth: styleTheme.borderWidth, ) ?? const ThemeStyleExtension(), + defaultTheme.extension()?.copyWith( + useMaterialYou: useMaterialYou, + useMaterialStyle: useMaterialStyle, + ) ?? + const ThemeSettingExtension(), ], ); } diff --git a/lib/settings/data/timer_app_settings_schema.dart b/lib/timer/data/timer_app_settings_schema.dart similarity index 100% rename from lib/settings/data/timer_app_settings_schema.dart rename to lib/timer/data/timer_app_settings_schema.dart diff --git a/lib/timer/screens/timer_notification_screen.dart b/lib/timer/screens/timer_notification_screen.dart index 86605da8..1886e3a5 100644 --- a/lib/timer/screens/timer_notification_screen.dart +++ b/lib/timer/screens/timer_notification_screen.dart @@ -1,6 +1,6 @@ import 'package:clock_app/common/types/notification_type.dart'; import 'package:clock_app/common/widgets/card_container.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/routes.dart'; import 'package:clock_app/notifications/logic/alarm_notifications.dart'; import 'package:clock_app/notifications/types/alarm_notification_arguments.dart'; diff --git a/lib/timer/screens/timer_screen.dart b/lib/timer/screens/timer_screen.dart index 658b4d47..01b8f48b 100644 --- a/lib/timer/screens/timer_screen.dart +++ b/lib/timer/screens/timer_screen.dart @@ -8,7 +8,7 @@ import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/list_filter.dart'; import 'package:clock_app/common/types/picker_result.dart'; import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/navigation/types/quick_action_controller.dart'; import 'package:clock_app/notifications/data/notification_channel.dart'; import 'package:clock_app/notifications/data/update_notification_intervals.dart'; diff --git a/lib/settings/data/widget_settings_schema.dart b/lib/widgets/data/widget_settings_schema.dart similarity index 100% rename from lib/settings/data/widget_settings_schema.dart rename to lib/widgets/data/widget_settings_schema.dart diff --git a/lib/widgets/logic/update_widgets.dart b/lib/widgets/logic/update_widgets.dart index 3b38fb69..982b7534 100644 --- a/lib/widgets/logic/update_widgets.dart +++ b/lib/widgets/logic/update_widgets.dart @@ -1,5 +1,5 @@ import 'package:clock_app/common/utils/time_format.dart'; -import 'package:clock_app/debug/logic/logger.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; diff --git a/pubspec.lock b/pubspec.lock index 5d4e666d..a3c9c2ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "61.0.0" + analog_clock: + dependency: "direct main" + description: + name: analog_clock + sha256: "6550dc371c5834d76295ec7b50c44b96c52f60e9bb2dd49568a3cb100e4b289e" + url: "https://pub.dev" + source: hosted + version: "0.1.1" analyzer: dependency: transitive description: @@ -34,6 +42,14 @@ packages: url: "https://github.com/AhsanSarwar45/plus_plugins" source: git version: "4.0.1" + animated_analog_clock: + dependency: "direct main" + description: + name: animated_analog_clock + sha256: "00ef12809ad0a95d1f6e50264e5b109a65448d97ba677be23d766261fb3378ce" + url: "https://pub.dev" + source: hosted + version: "0.1.0" ansicolor: dependency: transitive description: @@ -1106,10 +1122,10 @@ packages: dependency: "direct main" description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.4" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d062a74..88a1fd13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,8 @@ dependencies: background_fetch: ^1.3.7 clock: ^1.1.1 mime: ^1.0.6 + analog_clock: ^0.1.1 + animated_analog_clock: ^0.1.0 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From 809b9ab8ed4ec711534d916c6bb382f100a6040a Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 14 Sep 2024 16:28:27 +0500 Subject: [PATCH 160/177] Move ringtones_screen --- lib/{settings => audio}/screens/ringtones_screen.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/{settings => audio}/screens/ringtones_screen.dart (100%) diff --git a/lib/settings/screens/ringtones_screen.dart b/lib/audio/screens/ringtones_screen.dart similarity index 100% rename from lib/settings/screens/ringtones_screen.dart rename to lib/audio/screens/ringtones_screen.dart From 88e3cc0adf6e089cd71f4ea596bb39229356978e Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sun, 15 Sep 2024 23:27:02 +0500 Subject: [PATCH 161/177] Fix error on timezone screen --- lib/alarm/data/alarm_app_settings_schema.dart | 25 ++++++ lib/alarm/data/alarm_settings_schema.dart | 33 ++------ lib/alarm/widgets/try_alarm_task_button.dart | 3 +- lib/clock/screens/search_city_screen.dart | 26 +++--- lib/common/types/list_filter.dart | 12 +-- .../data/general_settings_schema.dart | 2 +- lib/settings/logic/get_setting_widget.dart | 2 +- .../screens/list_filter_settings_screen.dart | 67 +++++++++++++++ lib/settings/screens/tags_screen.dart | 1 - lib/settings/types/setting.dart | 82 +++++++++++++++++-- .../widgets/list_filter_setting_card.dart | 49 +++++++++++ .../list_setting_add_bottom_sheet.dart | 6 +- lib/settings/widgets/list_setting_card.dart | 4 +- lib/settings/widgets/list_setting_screen.dart | 14 ++-- lib/timer/data/timer_settings_schema.dart | 2 +- 15 files changed, 261 insertions(+), 67 deletions(-) create mode 100644 lib/settings/screens/list_filter_settings_screen.dart create mode 100644 lib/settings/widgets/list_filter_setting_card.dart diff --git a/lib/alarm/data/alarm_app_settings_schema.dart b/lib/alarm/data/alarm_app_settings_schema.dart index eaf9076a..46214132 100644 --- a/lib/alarm/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,6 +74,30 @@ 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", diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index ed64315e..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,13 +20,11 @@ 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'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/timer/types/time_duration.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -78,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, @@ -195,14 +180,13 @@ SettingGroup alarmSettingsSchema = SettingGroup( ], // shouldCloseOnSelect: false, ), - SwitchSetting( + SwitchSetting( "start_melody_at_random_pos", (context) => AppLocalizations.of(context)!.startMelodyAtRandomPos, false, - getDescription: (context) => AppLocalizations.of(context)!.startMelodyAtRandomPosDescription, - + getDescription: (context) => AppLocalizations.of(context)! + .startMelodyAtRandomPosDescription, ), - SliderSetting( "Volume", (context) => AppLocalizations.of(context)!.volumeSetting, @@ -309,13 +293,13 @@ SettingGroup alarmSettingsSchema = SettingGroup( "Length", ], ), - ListSetting( + CustomizableListSetting( "Tasks", (context) => AppLocalizations.of(context)!.tasksSetting, [], // kDebugMode - // ? [AlarmTask(AlarmTaskType.math), AlarmTask(AlarmTaskType.sequence)] - // : [], + // ? [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( @@ -328,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/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/clock/screens/search_city_screen.dart b/lib/clock/screens/search_city_screen.dart index b5047ff0..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( - titleWidget: 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/common/types/list_filter.dart b/lib/common/types/list_filter.dart index d5dd69d7..f454136d 100644 --- a/lib/common/types/list_filter.dart +++ b/lib/common/types/list_filter.dart @@ -1,21 +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/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; @@ -45,7 +45,6 @@ class ListFilter extends ListFilterItem { @override bool Function(Item) get filterFunction { - // print("Filtering $name $isSelected"); return isSelected ? _filterFunction : (Item item) => true; } @@ -64,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) { diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index 7ac16d6b..d54245f0 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:app_settings/app_settings.dart'; import 'package:auto_start_flutter/auto_start_flutter.dart'; import 'package:clock_app/app.dart'; +import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/clock/types/time.dart'; import 'package:clock_app/common/data/weekdays.dart'; import 'package:clock_app/common/types/weekday.dart'; @@ -12,7 +13,6 @@ import 'package:clock_app/common/utils/time_format.dart'; import 'package:clock_app/icons/flux_icons.dart'; import 'package:clock_app/l10n/language_local.dart'; import 'package:clock_app/notifications/logic/notifications.dart'; -import 'package:clock_app/settings/screens/ringtones_screen.dart'; import 'package:clock_app/settings/screens/tags_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; diff --git a/lib/settings/logic/get_setting_widget.dart b/lib/settings/logic/get_setting_widget.dart index 78e32542..71ac63fc 100644 --- a/lib/settings/logic/get_setting_widget.dart +++ b/lib/settings/logic/get_setting_widget.dart @@ -184,7 +184,7 @@ Widget? getSettingItemWidget( setting: item, showAsCard: showAsCard, ); - } else if (item is ListSetting) { + } else if (item is CustomizableListSetting) { return ListSettingCard( setting: item, showAsCard: showAsCard, diff --git a/lib/settings/screens/list_filter_settings_screen.dart b/lib/settings/screens/list_filter_settings_screen.dart new file mode 100644 index 00000000..2d8cfb58 --- /dev/null +++ b/lib/settings/screens/list_filter_settings_screen.dart @@ -0,0 +1,67 @@ +import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/widgets/fab.dart'; +import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; +import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:clock_app/settings/widgets/tag_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ListFilterSettingsScreen extends StatefulWidget { + const ListFilterSettingsScreen({ + super.key, + }); + + @override + State createState() => + _ListFilterSettingsScreenState(); +} + +class _ListFilterSettingsScreenState extends State { + final _listController = PersistentListController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), + body: Stack( + children: [ + Column( + children: [ + Expanded( + child: PersistentListView( + saveTag: 'tags', + listController: _listController, + itemBuilder: (tag) => TagCard( + key: ValueKey(tag), + tag: tag, + onPressDelete: () => _listController.deleteItem(tag), + onPressDuplicate: () => _listController.duplicateItem(tag), + ), + onTapItem: (tag, index) async { + Tag? newTag = await showTagEditor(tag); + if (newTag == null) return; + tag.copyFrom(newTag); + _listController.changeItems((tags) {}); + }, + // onDeleteItem: _handleDeleteTimer, + placeholderText: "No tags created", + reloadOnPop: true, + isSelectable: true, + ), + ), + ], + ), + FAB( + bottomPadding: 8, + onPressed: () async { + Tag? tag = await showTagEditor(); + if (tag == null) return; + _listController.addItem(tag); + }, + ) + ], + ), + ); + } +} diff --git a/lib/settings/screens/tags_screen.dart b/lib/settings/screens/tags_screen.dart index 4ff9d921..1173c84c 100644 --- a/lib/settings/screens/tags_screen.dart +++ b/lib/settings/screens/tags_screen.dart @@ -3,7 +3,6 @@ import 'package:clock_app/common/widgets/fab.dart'; import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/navigation/widgets/search_top_bar.dart'; import 'package:clock_app/settings/widgets/tag_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/settings/types/setting.dart b/lib/settings/types/setting.dart index a699cf64..af975507 100644 --- a/lib/settings/types/setting.dart +++ b/lib/settings/types/setting.dart @@ -68,16 +68,16 @@ abstract class Setting extends SettingItem { } } -class ListSetting extends Setting> { +class CustomizableListSetting extends Setting> { List possibleItems; Widget Function(T item, [VoidCallback?, VoidCallback?]) cardBuilder; Widget Function(T item) addCardBuilder; Widget Function(T item)? itemPreviewBuilder; // The widget that will be used to display the value of this setting. - Widget Function(BuildContext context, ListSetting setting) + Widget Function(BuildContext context, CustomizableListSetting setting) valueDisplayBuilder; - ListSetting( + CustomizableListSetting( String name, String Function(BuildContext) getLocalizedName, List defaultValue, @@ -104,8 +104,8 @@ class ListSetting extends Setting> { ); @override - ListSetting copy() { - return ListSetting( + CustomizableListSetting copy() { + return CustomizableListSetting( name, getLocalizedName, _value, @@ -151,6 +151,78 @@ class ListSetting extends Setting> { } } +class ListSetting extends Setting> { + List possibleItems; + Widget Function(T item, [VoidCallback?, VoidCallback?]) cardBuilder; + Widget Function(T item) addCardBuilder; + // The widget that will be used to display the value of this setting. + + ListSetting( + String name, + String Function(BuildContext) getLocalizedName, + List defaultValue, + this.possibleItems, { + required this.cardBuilder, + required this.addCardBuilder, + String Function(BuildContext) getDescription = defaultDescription, + void Function(BuildContext, List)? onChange, + bool isVisual = true, + List enableConditions = const [], + List searchTags = const [], + }) : super( + name, + getLocalizedName, + getDescription, + copyItemList(defaultValue), + onChange, + enableConditions, + searchTags, + isVisual, + valueCopyGetter: copyItemList, + ); + + @override + ListSetting copy() { + return ListSetting( + name, + getLocalizedName, + _value, + possibleItems, + cardBuilder: cardBuilder, + addCardBuilder: addCardBuilder, + getDescription: getDescription, + onChange: onChange, + enableConditions: enableConditions, + isVisual: isVisual, + searchTags: searchTags, + ); + } + + + + Widget getItemAddCard(T item) { + return addCardBuilder(item); + } + + Widget getItemCard(T item, + {VoidCallback? onDelete, VoidCallback? onDuplicate}) { + return cardBuilder(item, onDelete, onDuplicate); + } + + + @override + dynamic valueToJson() { + return _value.map((e) => e.toJson()).toList(); + } + + @override + void loadValueFromJson(dynamic value) { + if (value == null) return; + _value = (value as List).map((e) => fromJsonFactories[T]!(e) as T).toList(); + } +} + + class CustomSetting extends Setting { // The screen that will be navigated to when this setting is tapped. Widget Function(BuildContext, CustomSetting) screenBuilder; diff --git a/lib/settings/widgets/list_filter_setting_card.dart b/lib/settings/widgets/list_filter_setting_card.dart new file mode 100644 index 00000000..903d5a10 --- /dev/null +++ b/lib/settings/widgets/list_filter_setting_card.dart @@ -0,0 +1,49 @@ +import 'package:clock_app/common/types/list_filter.dart'; +import 'package:clock_app/common/types/tag.dart'; +import 'package:clock_app/common/utils/popup_action.dart'; +import 'package:clock_app/common/widgets/card_edit_menu.dart'; +import 'package:flutter/material.dart'; + +class ListFilterSettingCard extends StatefulWidget { + const ListFilterSettingCard({ + super.key, + required this.listFilter, + required this.onEnabledChange, + }); + + final ListFilterItem listFilter; + final void Function(bool) onEnabledChange; + + @override + State createState() => _ListFilterSettingCardState(); +} + +class _ListFilterSettingCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16.0, right: 4, top: 8, bottom: 8), + child: Row( + children: [ + Expanded( + flex: 999, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.listFilter.displayName(context), + style: Theme.of(context).textTheme.displaySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ], + ), + ), + Switch( + value: widget.listFilter.isEnabled, + onChanged: widget.onEnabledChange) + ], + )); + } +} diff --git a/lib/settings/widgets/list_setting_add_bottom_sheet.dart b/lib/settings/widgets/list_setting_add_bottom_sheet.dart index 347252d8..707457a9 100644 --- a/lib/settings/widgets/list_setting_add_bottom_sheet.dart +++ b/lib/settings/widgets/list_setting_add_bottom_sheet.dart @@ -2,14 +2,14 @@ import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:flutter/material.dart'; -class ListSettingAddBottomSheet +class CustomizableListSettingAddBottomSheet extends StatelessWidget { - const ListSettingAddBottomSheet({ + const CustomizableListSettingAddBottomSheet({ required this.setting, super.key, }); - final ListSetting setting; + final CustomizableListSetting setting; @override Widget build(BuildContext context) { diff --git a/lib/settings/widgets/list_setting_card.dart b/lib/settings/widgets/list_setting_card.dart index a5d4805f..2724a29a 100644 --- a/lib/settings/widgets/list_setting_card.dart +++ b/lib/settings/widgets/list_setting_card.dart @@ -11,7 +11,7 @@ class ListSettingCard extends StatefulWidget { this.showAsCard = true, }); - final ListSetting setting; + final CustomizableListSetting setting; final bool showAsCard; final void Function(BuildContext context) onChanged; @@ -28,7 +28,7 @@ class _ListSettingCardState extends State { onTap: () async { await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => ListSettingScreen( + builder: (context) => CustomizableListSettingScreen( setting: widget.setting, onChanged: widget.onChanged), ), ); diff --git a/lib/settings/widgets/list_setting_screen.dart b/lib/settings/widgets/list_setting_screen.dart index 9deb0cc8..7963eff7 100644 --- a/lib/settings/widgets/list_setting_screen.dart +++ b/lib/settings/widgets/list_setting_screen.dart @@ -9,23 +9,23 @@ import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/widgets/list_setting_add_bottom_sheet.dart'; import 'package:flutter/material.dart'; -class ListSettingScreen +class CustomizableListSettingScreen extends StatefulWidget { - const ListSettingScreen({ + const CustomizableListSettingScreen({ super.key, required this.setting, required this.onChanged, }); - final ListSetting setting; + final CustomizableListSetting setting; final void Function(BuildContext context) onChanged; @override - State createState() => _ListSettingScreenState(); + State createState() => _CustomizableListSettingScreenState(); } -class _ListSettingScreenState - extends State> { +class _CustomizableListSettingScreenState + extends State> { final _listController = ListController(); Future _openAddBottomSheet() async { @@ -33,7 +33,7 @@ class _ListSettingScreenState return await showModalBottomSheet( context: context, - builder: (context) => ListSettingAddBottomSheet(setting: widget.setting), + builder: (context) => CustomizableListSettingAddBottomSheet(setting: widget.setting), ); } diff --git a/lib/timer/data/timer_settings_schema.dart b/lib/timer/data/timer_settings_schema.dart index 2b189dfd..8bbfe1da 100644 --- a/lib/timer/data/timer_settings_schema.dart +++ b/lib/timer/data/timer_settings_schema.dart @@ -1,12 +1,12 @@ import 'package:audio_session/audio_session.dart'; import 'package:clock_app/audio/audio_channels.dart'; +import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/logic/tags.dart'; import 'package:clock_app/common/types/file_item.dart'; import 'package:clock_app/common/types/popup_action.dart'; import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/utils/ringtones.dart'; -import 'package:clock_app/settings/screens/ringtones_screen.dart'; import 'package:clock_app/settings/screens/tags_screen.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_enable_condition.dart'; From cd0fd9d84ef8f09f2b2bbc86bb52b91d85907ea4 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 16 Sep 2024 00:19:48 +0500 Subject: [PATCH 162/177] Fix title bar color --- lib/navigation/widgets/app_top_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index 1857f898..c6ad9dee 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -59,7 +59,7 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { systemNavigationBarColor: systemNavigationBarColor, systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarIconBrightness: systemNavBarIconBrightness, - statusBarColor: colorScheme.surface, + statusBarColor: colorScheme.background, statusBarIconBrightness: statusBarIconBrightness, // For Android (dark icons) ), From 70228bb496cbeb460f9c1a5c3a7c91ada41a77fd Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Mon, 16 Sep 2024 23:59:36 +0500 Subject: [PATCH 163/177] Add setting for background service interval --- lib/l10n/app_en.arb | 5 +- .../data/general_settings_schema.dart | 213 ++++++++++-------- lib/system/logic/background_service.dart | 11 +- 3 files changed, 127 insertions(+), 102 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8fdf231e..86275552 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -769,6 +769,7 @@ "numeralTypeSetting": "Numeral type", "romanNumeral": "Roman", "arabicNumeral": "Arabic", - "showDigitalClock": "Show digital clock" - + "showDigitalClock": "Show digital clock", + "backgroundServiceIntervalSetting": "Background service interval", + "backgroundServiceIntervalSettingDescription": "Lower interval will help keep the app alive, at the cost of some battery life" } diff --git a/lib/settings/data/general_settings_schema.dart b/lib/settings/data/general_settings_schema.dart index d54245f0..1492892f 100644 --- a/lib/settings/data/general_settings_schema.dart +++ b/lib/settings/data/general_settings_schema.dart @@ -18,6 +18,7 @@ import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_action.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:clock_app/settings/types/setting_link.dart'; +import 'package:clock_app/system/logic/background_service.dart'; import 'package:clock_app/system/logic/permissions.dart'; import 'package:clock_app/widgets/logic/update_widgets.dart'; import 'package:flutter/foundation.dart'; @@ -187,18 +188,17 @@ SettingGroup generalSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.pickerSpinner, DurationPickerType.spinner, ), - SelectSettingOption( + SelectSettingOption( (context) => AppLocalizations.of(context)!.pickerNumpad, DurationPickerType.numpad, ), - ], searchTags: [ "duration", "rings", "time", "numpad" - "picker", + "picker", "dial", "input", "spinner", @@ -229,7 +229,7 @@ SettingGroup generalSettingsSchema = SettingGroup( "Long Press Action", (context) => AppLocalizations.of(context)!.longPressActionSetting, [ - SelectSettingOption( + SelectSettingOption( (context) => AppLocalizations.of(context)!.longPressSelectAction, LongPressAction.multiSelect, ), @@ -237,7 +237,7 @@ SettingGroup generalSettingsSchema = SettingGroup( (context) => AppLocalizations.of(context)!.longPressReorderAction, LongPressAction.reorder, ), - ], + ], ), ]), SettingPageLink( @@ -254,98 +254,120 @@ SettingGroup generalSettingsSchema = SettingGroup( searchTags: ["tags", "groups", "filter"], icon: Icons.label_outline_rounded, ), - SettingGroup("Reliability", - (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, [ - SwitchSetting( - "Show Foreground Notification", - (context) => AppLocalizations.of(context)!.showForegroundNotification, - false, - getDescription: (context) => - AppLocalizations.of(context)!.showForegroundNotificationDescription, - ), - SettingAction( - "Ignore Battery Optimizations", - (context) => - AppLocalizations.of(context)!.ignoreBatteryOptimizationSetting, - (context) async { - requestBatteryOptimizationPermission( - onAlreadyGranted: () => { - showSnackBar( - context, - AppLocalizations.of(context)! - .ignoreBatteryOptimizationAlreadyGranted) - }); - }, - getDescription: (context) => - AppLocalizations.of(context)!.batteryOptimizationSettingDescription, - ), - SettingAction( - "Notifications", - (context) => - AppLocalizations.of(context)!.notificationPermissionSetting, - (context) async { - requestNotificationPermissions( - onAlreadyGranted: () => { - showSnackBar( - context, - AppLocalizations.of(context)! - .notificationPermissionAlreadyGranted) - }); - }, - getDescription: (context) => - AppLocalizations.of(context)!.notificationPermissionDescription, - ), - SettingAction( - "Vendor Specific", - (context) => AppLocalizations.of(context)!.vendorSetting, - (context) => launchUrl(Uri.parse("https://dontkillmyapp.com")), - getDescription: (context) => - AppLocalizations.of(context)!.vendorSettingDescription, - ), - SettingAction( - "Disable Battery Optimization", - (context) => AppLocalizations.of(context)!.batteryOptimizationSetting, - (context) async { - AppSettings.openAppSettings( - type: AppSettingsType.batteryOptimization); - }, - getDescription: (context) => - AppLocalizations.of(context)!.batteryOptimizationSettingDescription, - ), - SettingAction( - "Allow Notifications", - (context) => AppLocalizations.of(context)!.allowNotificationSetting, - (context) async { - AppSettings.openAppSettings(type: AppSettingsType.notification); - }, - getDescription: (context) => - AppLocalizations.of(context)!.allowNotificationSettingDescription, - ), - SettingAction( - "Auto Start", - (context) => AppLocalizations.of(context)!.autoStartSetting, - (context) async { - try { - //check auto-start availability. - var test = (await isAutoStartAvailable) ?? false; - //if available then navigate to auto-start setting page. - if (test) { - await getAutoStartPermission(); - } else { - // ignore: use_build_context_synchronously - if (context.mounted) { - showSnackBar( - context, "Auto Start is not available for your device"); + SettingGroup( + "Reliability", + (context) => AppLocalizations.of(context)!.reliabilitySettingGroup, + [ + SwitchSetting( + "Show Foreground Notification", + (context) => AppLocalizations.of(context)!.showForegroundNotification, + false, + getDescription: (context) => AppLocalizations.of(context)! + .showForegroundNotificationDescription, + searchTags: ["foreground", "notification"], + ), + SliderSetting( + "backgroundServiceInterval", + (context) => + AppLocalizations.of(context)!.backgroundServiceIntervalSetting, + 15, + 300, + 60, + unit: "m", + snapLength: 15, + getDescription: (context) => AppLocalizations.of(context)! + .backgroundServiceIntervalSettingDescription, + searchTags: ["background", "service", "interval"], + onChange: (context, value) { + initBackgroundService(interval: value.toInt()); + }, + ), + SettingAction( + "Ignore Battery Optimizations", + (context) => + AppLocalizations.of(context)!.ignoreBatteryOptimizationSetting, + (context) async { + requestBatteryOptimizationPermission( + onAlreadyGranted: () => { + showSnackBar( + context, + AppLocalizations.of(context)! + .ignoreBatteryOptimizationAlreadyGranted) + }); + }, + getDescription: (context) => AppLocalizations.of(context)! + .batteryOptimizationSettingDescription, + ), + SettingAction( + "Notifications", + (context) => + AppLocalizations.of(context)!.notificationPermissionSetting, + (context) async { + requestNotificationPermissions( + onAlreadyGranted: () => { + showSnackBar( + context, + AppLocalizations.of(context)! + .notificationPermissionAlreadyGranted) + }); + }, + getDescription: (context) => + AppLocalizations.of(context)!.notificationPermissionDescription, + ), + SettingAction( + "Vendor Specific", + (context) => AppLocalizations.of(context)!.vendorSetting, + (context) => launchUrl(Uri.parse("https://dontkillmyapp.com")), + getDescription: (context) => + AppLocalizations.of(context)!.vendorSettingDescription, + ), + SettingAction( + "Disable Battery Optimization", + (context) => AppLocalizations.of(context)!.batteryOptimizationSetting, + (context) async { + AppSettings.openAppSettings( + type: AppSettingsType.batteryOptimization); + }, + getDescription: (context) => AppLocalizations.of(context)! + .batteryOptimizationSettingDescription, + + ), + SettingAction( + "Allow Notifications", + (context) => AppLocalizations.of(context)!.allowNotificationSetting, + (context) async { + AppSettings.openAppSettings(type: AppSettingsType.notification); + }, + getDescription: (context) => + AppLocalizations.of(context)!.allowNotificationSettingDescription, + ), + SettingAction( + "Auto Start", + (context) => AppLocalizations.of(context)!.autoStartSetting, + (context) async { + try { + //check auto-start availability. + var test = (await isAutoStartAvailable) ?? false; + //if available then navigate to auto-start setting page. + if (test) { + await getAutoStartPermission(); + } else { + // ignore: use_build_context_synchronously + if (context.mounted) { + showSnackBar( + context, "Auto Start is not available for your device"); + } } + } on PlatformException catch (e) { + if (kDebugMode) print(e.message); } - } on PlatformException catch (e) { - if (kDebugMode) print(e.message); - } - }, - getDescription: (context) => - AppLocalizations.of(context)!.autoStartSettingDescription, - ), - ]), + }, + getDescription: (context) => + AppLocalizations.of(context)!.autoStartSettingDescription, + ), + ], + searchTags: ["reliability", "battery", "optimization", "notifications"], + ), SelectSetting( "Default Tab", (context) => AppLocalizations.of(context)!.defaultPageSetting, @@ -368,7 +390,6 @@ SettingGroup generalSettingsSchema = SettingGroup( ), ], ), - ], icon: FluxIcons.settings, getDescription: (context) => diff --git a/lib/system/logic/background_service.dart b/lib/system/logic/background_service.dart index 179be98a..cd73e2f0 100644 --- a/lib/system/logic/background_service.dart +++ b/lib/system/logic/background_service.dart @@ -5,10 +5,12 @@ import 'package:clock_app/system/logic/initialize_isolate.dart'; import 'package:clock_app/timer/logic/update_timers.dart'; import 'package:flutter/material.dart'; -Future initBackgroundService() async { +Future initBackgroundService({int interval = 60}) async { + assert( + interval >= 15, "Interval must be greater than or equal to 15 minutes."); await BackgroundFetch.configure( BackgroundFetchConfig( - minimumFetchInterval: 30, + minimumFetchInterval: interval, stopOnTerminate: false, enableHeadless: true, requiresBatteryNotLow: false, @@ -40,8 +42,9 @@ Future initBackgroundService() async { // [Android-only] This "Headless Task" is run when the Android app is terminated with `enableHeadless: true` @pragma('vm:entry-point') void handleBackgroundServiceTask(HeadlessTask task) async { - FlutterError.onError = (FlutterErrorDetails details) { - logger.f("Error in handleBackgroundServiceTask isolate: ${details.exception.toString()}"); + FlutterError.onError = (FlutterErrorDetails details) { + logger.f( + "Error in handleBackgroundServiceTask isolate: ${details.exception.toString()}"); }; String taskId = task.taskId; bool isTimeout = task.timeout; From 4c771e9dc28266cb2e31e2095c373c8e7b60873e Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 17 Sep 2024 13:44:11 +0500 Subject: [PATCH 164/177] Fix app bar back button --- lib/navigation/widgets/app_top_bar.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/navigation/widgets/app_top_bar.dart b/lib/navigation/widgets/app_top_bar.dart index c6ad9dee..7c6a93d0 100644 --- a/lib/navigation/widgets/app_top_bar.dart +++ b/lib/navigation/widgets/app_top_bar.dart @@ -26,7 +26,7 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; - final bool showBackButton = Navigator.of(context).canPop(); + // final bool showBackButton = Navigator.of(context).canPop(); final systemNavigationBarColor = systemNavBarColor ?? colorScheme.background; @@ -50,6 +50,9 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget { ) : null); + final ModalRoute? parentRoute = ModalRoute.of(context); + final bool showBackButton = parentRoute?.impliesAppBarDismissal ?? false; + return PreferredSize( preferredSize: preferredSize, child: Padding( From 3d3460ef192322786a2b3af256e5a8adfe4f538d Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 17 Sep 2024 13:51:20 +0500 Subject: [PATCH 165/177] Fix timer picker intitial duration --- lib/timer/widgets/duration_picker.dart | 2 +- lib/timer/widgets/numpad_duration_picker.dart | 7 +++++-- lib/timer/widgets/timer_picker.dart | 2 +- lib/timer/widgets/timer_preset_picker.dart | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/timer/widgets/duration_picker.dart b/lib/timer/widgets/duration_picker.dart index 226fd668..2641a25f 100644 --- a/lib/timer/widgets/duration_picker.dart +++ b/lib/timer/widgets/duration_picker.dart @@ -10,7 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; Future showDurationPicker( BuildContext context, { TimeDuration initialTimeDuration = - const TimeDuration(hours: 0, minutes: 5, seconds: 0), + const TimeDuration(hours: 0, minutes: 0, seconds: 0), bool showHours = true, }) async { final theme = Theme.of(context); diff --git a/lib/timer/widgets/numpad_duration_picker.dart b/lib/timer/widgets/numpad_duration_picker.dart index 2a617b1f..cea39231 100644 --- a/lib/timer/widgets/numpad_duration_picker.dart +++ b/lib/timer/widgets/numpad_duration_picker.dart @@ -112,6 +112,7 @@ class _NumpadDurationPickerState extends State { ); } else if (index == 9) { return TimerButton( + isHighlighted: true, label: "00", onTap: () { _addDigit("0", 2); @@ -123,6 +124,7 @@ class _NumpadDurationPickerState extends State { ); } else { return TimerButton( + isHighlighted: true, icon: Icons.backspace_outlined, onTap: _removeDigit, ); @@ -139,9 +141,10 @@ class TimerButton extends StatelessWidget { final String? label; final IconData? icon; final VoidCallback onTap; + final bool isHighlighted; const TimerButton( - {super.key, this.label, required this.onTap, this.icon}); + {super.key, this.label, required this.onTap, this.icon, this.isHighlighted = false}); @override Widget build(BuildContext context) { @@ -153,7 +156,7 @@ class TimerButton extends StatelessWidget { borderRadius: BorderRadius.circular(100), child: Container( decoration: BoxDecoration( - color: colorScheme.onBackground.withOpacity(0.1), + color: isHighlighted ? colorScheme.primary.withOpacity(0.2) : colorScheme.onBackground.withOpacity(0.1), borderRadius: BorderRadius.circular(100), ), child: Center( diff --git a/lib/timer/widgets/timer_picker.dart b/lib/timer/widgets/timer_picker.dart index 81e35163..d99ea7e4 100644 --- a/lib/timer/widgets/timer_picker.dart +++ b/lib/timer/widgets/timer_picker.dart @@ -25,7 +25,7 @@ Future?> showTimerPicker( context: context, builder: (BuildContext context) { ClockTimer timer = ClockTimer.from( - initialTimer ?? ClockTimer(const TimeDuration(minutes: 5))); + initialTimer ?? ClockTimer(const TimeDuration(minutes: 0))); TimerPreset? selectedPreset; List presets = loadListSync("timer_presets"); diff --git a/lib/timer/widgets/timer_preset_picker.dart b/lib/timer/widgets/timer_preset_picker.dart index 2dfe4dc9..1da3804d 100644 --- a/lib/timer/widgets/timer_preset_picker.dart +++ b/lib/timer/widgets/timer_preset_picker.dart @@ -16,7 +16,7 @@ Future showTimerPresetPicker(BuildContext context, context: context, builder: (BuildContext context) { TimerPreset timerPreset = TimerPreset.from(initialTimerPreset ?? - TimerPreset("New Preset", const TimeDuration(minutes: 5))); + TimerPreset("New Preset", const TimeDuration(minutes: 0))); TextEditingController controller = TextEditingController( text: timerPreset.name, From 5f9ba3895353e1d60ded9234e821de991454971e Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sun, 8 Sep 2024 09:25:14 +0000 Subject: [PATCH 166/177] Translated using Weblate (Spanish) Currently translated at 100.0% (365 of 365 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/es/ --- lib/l10n/app_es.arb | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ce27f7d7..ac6ac51b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -293,7 +293,7 @@ "@showIstantAlarmButtonSetting": {}, "showIstantTimerButtonSetting": "Mostrar botón de temporizador instantáneo", "@showIstantTimerButtonSetting": {}, - "maxLogsSetting": "Registros máximos", + "maxLogsSetting": "Registro máximo de alarmas", "@maxLogsSetting": {}, "alarmLogSetting": "Registros de alarmas", "@alarmLogSetting": {}, @@ -704,5 +704,29 @@ "showForegroundNotificationDescription": "Mostrar una notificación persistente para mantener activa la aplicación", "@showForegroundNotificationDescription": {}, "extraAnimationSettingDescription": "Muestra animaciones que no están pulidas y que pueden provocar caídas de cuadros en dispositivos de gama baja", - "@extraAnimationSettingDescription": {} + "@extraAnimationSettingDescription": {}, + "longPressActionSetting": "Acción de pulsación prolongada", + "@longPressActionSetting": {}, + "longPressReorderAction": "Reordenar", + "@longPressReorderAction": {}, + "longPressSelectAction": "Selección múltiple", + "@longPressSelectAction": {}, + "saveLogs": "Guardar los registros", + "@saveLogs": {}, + "clearLogs": "Borrar registros", + "@clearLogs": {}, + "selectionStatus": "{n} seleccionado(s)", + "@selectionStatus": {}, + "pickerNumpad": "Teclado numérico", + "@pickerNumpad": {}, + "interactionsSettingGroup": "Interacciones", + "@interactionsSettingGroup": {}, + "showErrorSnackbars": "Mostrar notificaciones con los errores", + "@showErrorSnackbars": {}, + "volumeWhileTasks": "Volumen al resolver tareas", + "@volumeWhileTasks": {}, + "selectAll": "Seleccionar todo", + "@selectAll": {}, + "reorder": "Reordenar", + "@reorder": {} } From 50005ee392c0767f97bc4f59db975ea8db52379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Ng=E1=BB=8Dc=20Quang?= Date: Sun, 8 Sep 2024 10:59:09 +0000 Subject: [PATCH 167/177] Translated using Weblate (Vietnamese) Currently translated at 78.3% (286 of 365 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/vi/ --- lib/l10n/app_vi.arb | 336 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 333 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index ec9a416c..e20707a1 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -3,7 +3,7 @@ "@sequenceGridSizeSetting": {}, "scheduleTypeWeekDescription": "Sẽ lặp lại vào các ngày trong tuần được chỉ định", "@scheduleTypeWeekDescription": {}, - "soundSettingGroup": "Âm thanh và Rung", + "soundSettingGroup": "Âm thanh", "@soundSettingGroup": {}, "saveReminderAlert": "Bạn có muốn rời đi mà không lưu?", "@saveReminderAlert": {}, @@ -95,7 +95,7 @@ "@appearanceSettingGroupDescription": {}, "backupSettingGroupDescription": "Xuất hoặc nhập thiết đặt của bạn cục bộ", "@backupSettingGroupDescription": {}, - "saveButton": "Nút lưu", + "saveButton": "Lưu", "@saveButton": {}, "labelField": "Nhãn", "@labelField": {}, @@ -252,5 +252,335 @@ "pickerInput": "Đầu vào", "@pickerInput": {}, "longDateFormatSetting": "Định dạng ngày theo thứ, tháng, ngày và năm", - "@longDateFormatSetting": {} + "@longDateFormatSetting": {}, + "firstDayOfWeekSetting": "Ngày đầu tuần", + "@firstDayOfWeekSetting": {}, + "translateDescription": "Giúp dịch ứng dụng", + "@translateDescription": {}, + "interactionsSettingGroup": "Các tương tác", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "Hành động nhấn giữ", + "@longPressActionSetting": {}, + "longPressReorderAction": "Thay đổi thứ tự", + "@longPressReorderAction": {}, + "longPressSelectAction": "Chọn nhiều", + "@longPressSelectAction": {}, + "allowNotificationSetting": "Cho phép tất cả các thông báo thủ công", + "@allowNotificationSetting": {}, + "animationSettingGroup": "Hoạt ảnh", + "@animationSettingGroup": {}, + "animationSpeedSetting": "Tốc độ hoạt ảnh", + "@animationSpeedSetting": {}, + "extraAnimationSetting": "Hoạt ảnh bổ sung", + "@extraAnimationSetting": {}, + "nameField": "Tên", + "@nameField": {}, + "colorSetting": "Màu sắc", + "@colorSetting": {}, + "textColorSetting": "Màu chữ", + "@textColorSetting": {}, + "colorSchemeBackgroundSettingGroup": "Nền", + "@colorSchemeBackgroundSettingGroup": {}, + "colorSchemeErrorSettingGroup": "Lỗi", + "@colorSchemeErrorSettingGroup": {}, + "saveLogs": "Lưu nhật ký", + "@saveLogs": {}, + "clearLogs": "Xoá nhật ký", + "@clearLogs": {}, + "errorLabel": "Lỗi", + "@errorLabel": {}, + "materialBrightnessSystem": "Hệ thống", + "@materialBrightnessSystem": {}, + "materialBrightnessLight": "Sáng", + "@materialBrightnessLight": {}, + "materialBrightnessDark": "Tối", + "@materialBrightnessDark": {}, + "soundAndVibrationSettingGroup": "Âm thanh và rung", + "@soundAndVibrationSettingGroup": {}, + "audioChannelRingtone": "Nhạc chuông", + "@audioChannelRingtone": {}, + "numberOfProblemsSetting": "Số lượng bài toàn", + "@numberOfProblemsSetting": {}, + "logTypeFilterGroup": "Loại", + "@logTypeFilterGroup": {}, + "stateFilterGroup": "Trạng thái", + "@stateFilterGroup": {}, + "activeFilter": "Đang hoạt động", + "@activeFilter": {}, + "inactiveFilter": "Không hoạt động", + "@inactiveFilter": {}, + "reorder": "Xắp xếp lại", + "@reorder": {}, + "defaultLabel": "Mặc định", + "@defaultLabel": {}, + "durationAsc": "Ngắn nhất", + "@durationAsc": {}, + "durationDesc": "Dài nhất", + "@durationDesc": {}, + "nameAsc": "Nhãn A-Z", + "@nameAsc": {}, + "nameDesc": "Nhãn Z-A", + "@nameDesc": {}, + "filterActions": "Hành động lọc", + "@filterActions": {}, + "disableAllFilteredAlarmsAction": "Tắt tất cả báo thức được lọc", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Bỏ qua tất cả báo thức được lọc", + "@skipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Xoá tất cả các mục được lọc", + "@deleteAllFilteredAction": {}, + "exportSettingsSettingDescription": "Xuất cài đặt ra tệp tin cục bộ", + "@exportSettingsSettingDescription": {}, + "importSettingsSettingDescription": "Nhập cài đặt từ tệp tin cục bộ", + "@importSettingsSettingDescription": {}, + "packageNameLabel": "Tên gói", + "@packageNameLabel": {}, + "licenseLabel": "Giấy phép", + "@licenseLabel": {}, + "emailLabel": "Email", + "@emailLabel": {}, + "viewOnGithubLabel": "Xem trên Github", + "@viewOnGithubLabel": {}, + "wednesdayFull": "Thứ 4", + "@wednesdayFull": {}, + "thursdayFull": "Thứ 5", + "@thursdayFull": {}, + "fridayFull": "Thứ 6", + "@fridayFull": {}, + "tuesdayLetter": "3", + "@tuesdayLetter": {}, + "wednesdayLetter": "4", + "@wednesdayLetter": {}, + "saturdayLetter": "7", + "@saturdayLetter": {}, + "alignmentBottom": "Dưới cùng", + "@alignmentBottom": {}, + "alignmentCenter": "Trung tâm", + "@alignmentCenter": {}, + "alignmentRight": "Bên phải", + "@alignmentRight": {}, + "alignmentLeft": "Bên trái", + "@alignmentLeft": {}, + "alignmentJustify": "Căn chỉnh", + "@alignmentJustify": {}, + "fontWeightSetting": "Độ đậm chữ", + "@fontWeightSetting": {}, + "dateSettingGroup": "Ngày", + "@dateSettingGroup": {}, + "timeSettingGroup": "Thời gian", + "@timeSettingGroup": {}, + "sizeSetting": "Kích cỡ", + "@sizeSetting": {}, + "defaultPageSetting": "Tab mặc định", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Hiển thị AM/PM", + "@showMeridiemSetting": {}, + "alarmRingInMessage": "Báo thức sẽ reo trong {duration}", + "@alarmRingInMessage": {}, + "pickerNumpad": "Bàn phím số", + "@pickerNumpad": {}, + "autoStartSettingDescription": "Một số thiết bị yêu cầu phải bật Auto Start để báo thức có thể đổ chuông khi ứng dụng đang đóng", + "@autoStartSettingDescription": {}, + "thursdayLetter": "5", + "@thursdayLetter": {}, + "colorSchemeNamePlaceholder": "Bảng màu", + "@colorSchemeNamePlaceholder": {}, + "showIstantAlarmButtonSetting": "Hiển thị nút báo động tức thì", + "@showIstantAlarmButtonSetting": {}, + "styleThemeOutlineWidthSetting": "Độ rộng", + "@styleThemeOutlineWidthSetting": {}, + "styleThemeOutlineSettingGroup": "Viền", + "@styleThemeOutlineSettingGroup": {}, + "showIstantTimerButtonSetting": "Hiển thị nút hẹn giờ tức thì", + "@showIstantTimerButtonSetting": {}, + "logsSettingGroup": "Nhật ký", + "@logsSettingGroup": {}, + "selectAll": "Chọn tất cả", + "@selectAll": {}, + "selectionStatus": "Đã chọn {n}", + "@selectionStatus": {}, + "clearFiltersAction": "Xoá tất cả bộ lọc", + "@clearFiltersAction": {}, + "enableAllFilteredAlarmsAction": "Bật tất cả báo thức được lọc", + "@enableAllFilteredAlarmsAction": {}, + "fridayLetter": "6", + "@fridayLetter": {}, + "importSettingsSetting": "Nhập", + "@importSettingsSetting": {}, + "combinedTime": "{hours} và {minutes}", + "@combinedTime": {}, + "shortHoursString": "{hours} giờ", + "@shortHoursString": {}, + "shortMinutesString": "{minutes} phút", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds} giây", + "@shortSecondsString": {}, + "permissionsSettingGroup": "Quyền", + "@permissionsSettingGroup": {}, + "ignoreBatteryOptimizationSetting": "Bỏ qua tối ưu hóa pin", + "@ignoreBatteryOptimizationSetting": {}, + "notificationPermissionSetting": "Quyền thông báo", + "@notificationPermissionSetting": {}, + "notificationPermissionAlreadyGranted": "Quyền thông báo đã được cấp", + "@notificationPermissionAlreadyGranted": {}, + "ignoreBatteryOptimizationAlreadyGranted": "Quyền bỏ qua tối ưu hóa pin đã được cấp", + "@ignoreBatteryOptimizationAlreadyGranted": {}, + "colorSchemeCardSettingGroup": "Thẻ", + "@colorSchemeCardSettingGroup": {}, + "colorSchemeOutlineSettingGroup": "Viền", + "@colorSchemeOutlineSettingGroup": {}, + "colorSchemeShadowSettingGroup": "Đổ bóng", + "@colorSchemeShadowSettingGroup": {}, + "styleThemeNamePlaceholder": "Phong cách chủ đề", + "@styleThemeNamePlaceholder": {}, + "styleThemeShadowSettingGroup": "Đổ bóng", + "@styleThemeShadowSettingGroup": {}, + "styleThemeRadiusSetting": "Bo góc", + "@styleThemeRadiusSetting": {}, + "styleThemeOpacitySetting": "Độ trong suốt", + "@styleThemeOpacitySetting": {}, + "styleThemeBlurSetting": "Độ mờ", + "@styleThemeBlurSetting": {}, + "resetButton": "Đặt lại", + "@resetButton": {}, + "previewLabel": "Xem trước", + "@previewLabel": {}, + "cardLabel": "Thẻ", + "@cardLabel": {}, + "alarmIntervalDaily": "Hàng ngày", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Hàng tuần", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterFinishingSetting": "Xoá sau khi hoàn tất", + "@alarmDeleteAfterFinishingSetting": {}, + "audioChannelNotification": "Thông báo", + "@audioChannelNotification": {}, + "snoozePreventDeletionSetting": "Ngăn chặn xoá", + "@snoozePreventDeletionSetting": {}, + "noAlarmMessage": "Không có báo thức nào được tạo", + "@noAlarmMessage": {}, + "createdDateFilterGroup": "Ngày tạo", + "@createdDateFilterGroup": {}, + "showNotificationSetting": "Hiển thị thông báo", + "@showNotificationSetting": {}, + "stopwatchTimeFormatSettingGroup": "Định dạng thời gian", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Hiển thị mili-giây", + "@stopwatchShowMillisecondsSetting": {}, + "openSourceLicensesSetting": "Giấy phép mã nguồn mở", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Đóng góp", + "@contributorsSetting": {}, + "donorsSetting": "Tài trợ", + "@donorsSetting": {}, + "donateButton": "Ủng hộ", + "@donateButton": {}, + "cityAlreadyInFavorites": "Thành phố này đã nằm trong danh sách yêu thích của bạn", + "@cityAlreadyInFavorites": {}, + "saturdayFull": "Thứ 7", + "@saturdayFull": {}, + "sundayFull": "Chủ nhật", + "@sundayFull": {}, + "mondayShort": "T2", + "@mondayShort": {}, + "tuesdayShort": "T3", + "@tuesdayShort": {}, + "wednesdayShort": "T4", + "@wednesdayShort": {}, + "thursdayShort": "T5", + "@thursdayShort": {}, + "fridayShort": "T6", + "@fridayShort": {}, + "mondayLetter": "2", + "@mondayLetter": {}, + "donateDescription": "Quyên góp để hỗ trợ sự phát triển của ứng dụng", + "@donateDescription": {}, + "translateLink": "Dịch", + "@translateLink": {}, + "timePickerModeButton": "Chế độ", + "@timePickerModeButton": {}, + "scheduleTypeDate": "Vào những ngày cụ thể", + "@scheduleTypeDate": {}, + "mathEasyDifficulty": "Dễ (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Trung bình (X × Y)", + "@mathMediumDifficulty": {}, + "mathHardDifficulty": "Khó (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Rất khó (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "leftHandedSetting": "Chế độ thuận tay trái", + "@leftHandedSetting": {}, + "runningTimerFilter": "Đang chạy", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Tạm dừng", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Đã dừng", + "@stoppedTimerFilter": {}, + "sortGroup": "Sắp xếp", + "@sortGroup": {}, + "alarmDescriptionWeekly": "Mỗi {days}", + "@alarmDescriptionWeekly": {}, + "sameTime": "Cùng thời gian", + "@sameTime": {}, + "searchCityPlaceholder": "Tìm kiếm thành phố", + "@searchCityPlaceholder": {}, + "durationPickerTitle": "Chọn thời lượng", + "@durationPickerTitle": {}, + "editButton": "Chỉnh sửa", + "@editButton": {}, + "mondayFull": "Thứ 2", + "@mondayFull": {}, + "tuesdayFull": "Thứ 3", + "@tuesdayFull": {}, + "saturdayShort": "T7", + "@saturdayShort": {}, + "sundayShort": "CN", + "@sundayShort": {}, + "sundayLetter": "CN", + "@sundayLetter": {}, + "pickerSpinner": "Vòng xoay", + "@pickerSpinner": {}, + "tagsSetting": "Nhãn", + "@tagsSetting": {}, + "batteryOptimizationSettingDescription": "Vô hiệu hóa tối ưu hóa pin cho ứng dụng này để ngăn chặn việc báo thức bị trễ", + "@batteryOptimizationSettingDescription": {}, + "allowNotificationSettingDescription": "Cho phép thông báo trên màn hình khóa cho báo thức và hẹn giờ", + "@allowNotificationSettingDescription": {}, + "autoStartSetting": "Tự khởi chạy", + "@autoStartSetting": {}, + "digitalClockSettingGroup": "Đồng hồ kỹ thuật số", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Bố cục", + "@layoutSettingGroup": {}, + "lessThanOneMinute": "ít hơn một phút", + "@lessThanOneMinute": {}, + "remainingTimeDesc": "Thời gian còn lại ít nhất", + "@remainingTimeDesc": {}, + "remainingTimeAsc": "Thời gian còn lại nhiều nhất", + "@remainingTimeAsc": {}, + "showFiltersSetting": "Hiển thị các bộ lọc", + "@showFiltersSetting": {}, + "showSortSetting": "Hiển thị sắp xếp", + "@showSortSetting": {}, + "notificationsSettingGroup": "Các thông báo", + "@notificationsSettingGroup": {}, + "defaultSettingGroup": "Cài đặt mặc định", + "@defaultSettingGroup": {}, + "filtersSettingGroup": "Bộ lọc", + "@filtersSettingGroup": {}, + "exportSettingsSetting": "Xuất", + "@exportSettingsSetting": {}, + "versionLabel": "Phiên bản", + "@versionLabel": {}, + "showDateSetting": "Hiển thị ngày", + "@showDateSetting": {}, + "textSettingGroup": "Văn bản", + "@textSettingGroup": {}, + "settingsTitle": "Cài đặt", + "@settingsTitle": {}, + "alignmentTop": "Trên cùng", + "@alignmentTop": {}, + "searchSettingPlaceholder": "Tìm kiếm cài đặt", + "@searchSettingPlaceholder": {} } From 39a7d192642c7cfbc8c68da00acb9ab678024656 Mon Sep 17 00:00:00 2001 From: enly sure Date: Sun, 8 Sep 2024 04:25:18 +0000 Subject: [PATCH 168/177] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (365 of 365 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/zh_Hans/ --- lib/l10n/app_zh.arb | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 40207dc3..77632bc2 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -712,5 +712,29 @@ "showForegroundNotificationDescription": "显示持久通知以保持应用运行", "@showForegroundNotificationDescription": {}, "notificationPermissionDescription": "允许显示通知", - "@notificationPermissionDescription": {} + "@notificationPermissionDescription": {}, + "interactionsSettingGroup": "互动", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "长按操作", + "@longPressActionSetting": {}, + "longPressReorderAction": "重新购买", + "@longPressReorderAction": {}, + "longPressSelectAction": "多选", + "@longPressSelectAction": {}, + "saveLogs": "保存日志", + "@saveLogs": {}, + "showErrorSnackbars": "显示错误提示", + "@showErrorSnackbars": {}, + "pickerNumpad": "数字键盘", + "@pickerNumpad": {}, + "volumeWhileTasks": "响铃时的音量", + "@volumeWhileTasks": {}, + "selectionStatus": "选择{n}", + "@selectionStatus": {}, + "selectAll": "选择全部", + "@selectAll": {}, + "clearLogs": "清除日志", + "@clearLogs": {}, + "reorder": "重新购买", + "@reorder": {} } From bd78b28d97c2c3c7a0094818feda8dc72a87682f Mon Sep 17 00:00:00 2001 From: Reno Tx Date: Sun, 8 Sep 2024 09:10:32 +0000 Subject: [PATCH 169/177] Translated using Weblate (Serbian) Currently translated at 100.0% (365 of 365 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/sr/ --- lib/l10n/app_sr.arb | 538 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 537 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_sr.arb b/lib/l10n/app_sr.arb index 59eac2ac..8059b049 100644 --- a/lib/l10n/app_sr.arb +++ b/lib/l10n/app_sr.arb @@ -200,5 +200,541 @@ "styleThemeSetting": "Стил теме", "@styleThemeSetting": {}, "systemDarkModeSetting": "Системски тамни режим", - "@systemDarkModeSetting": {} + "@systemDarkModeSetting": {}, + "longPressSelectAction": "Вишеструки избор", + "@longPressSelectAction": {}, + "alarmDeleteAfterFinishingSetting": "Обриши након завршетка", + "@alarmDeleteAfterFinishingSetting": {}, + "scheduleTypeOnce": "Једном", + "@scheduleTypeOnce": {}, + "scheduleTypeDaily": "Дневно", + "@scheduleTypeDaily": {}, + "scheduleTypeRange": "Опсег датума", + "@scheduleTypeRange": {}, + "scheduleTypeDateDescription": "Понављаће се у одређене датуме", + "@scheduleTypeDateDescription": {}, + "audioChannelSetting": "Аудио канал", + "@audioChannelSetting": {}, + "audioChannelAlarm": "Аларм", + "@audioChannelAlarm": {}, + "audioChannelNotification": "Обавештење", + "@audioChannelNotification": {}, + "saveLogs": "Сачувај записе", + "@saveLogs": {}, + "showErrorSnackbars": "Прикажи грешке у обавештењима", + "@showErrorSnackbars": {}, + "clearLogs": "Очисти записе", + "@clearLogs": {}, + "risingVolumeSetting": "Повећавање јачине звука", + "@risingVolumeSetting": {}, + "chooseTaskTitle": "Изабери задатак за додавање", + "@chooseTaskTitle": {}, + "mathTask": "Математички проблеми", + "@mathTask": {}, + "mathHardDifficulty": "Тешко (X × Y + Z)", + "@mathHardDifficulty": {}, + "mathVeryHardDifficulty": "Веома тешко (X × Y × Z)", + "@mathVeryHardDifficulty": {}, + "retypeTask": "Поново унеси текст", + "@retypeTask": {}, + "sequenceTask": "Секвенца", + "@sequenceTask": {}, + "taskTryButton": "Пробај", + "@taskTryButton": {}, + "mathTaskDifficultySetting": "Тежина", + "@mathTaskDifficultySetting": {}, + "retypeNumberChars": "Број карактера", + "@retypeNumberChars": {}, + "retypeIncludeNumSetting": "Укључи бројеве", + "@retypeIncludeNumSetting": {}, + "retypeLowercaseSetting": "Укључи мала слова", + "@retypeLowercaseSetting": {}, + "sequenceLengthSetting": "Дужина секвенце", + "@sequenceLengthSetting": {}, + "sequenceGridSizeSetting": "Величина мреже", + "@sequenceGridSizeSetting": {}, + "dismissAlarmButton": "Одложи", + "@dismissAlarmButton": {}, + "allFilter": "Све", + "@allFilter": {}, + "dateFilterGroup": "Датум", + "@dateFilterGroup": {}, + "scheduleDateFilterGroup": "Распоред датума", + "@scheduleDateFilterGroup": {}, + "logTypeFilterGroup": "Тип", + "@logTypeFilterGroup": {}, + "createdDateFilterGroup": "Датум креирања", + "@createdDateFilterGroup": {}, + "completedFilter": "Завршен", + "@completedFilter": {}, + "runningTimerFilter": "У току", + "@runningTimerFilter": {}, + "pausedTimerFilter": "Паузиран", + "@pausedTimerFilter": {}, + "stoppedTimerFilter": "Заустављен", + "@stoppedTimerFilter": {}, + "sortGroup": "Сортирај", + "@sortGroup": {}, + "defaultLabel": "Подразумевано", + "@defaultLabel": {}, + "remainingTimeDesc": "Најмање времена преостало", + "@remainingTimeDesc": {}, + "enableAllFilteredAlarmsAction": "Омогући све филтриране аларме", + "@enableAllFilteredAlarmsAction": {}, + "disableAllFilteredAlarmsAction": "Онемогући све филтриране аларме", + "@disableAllFilteredAlarmsAction": {}, + "skipAllFilteredAlarmsAction": "Прескочи све филтриране аларме", + "@skipAllFilteredAlarmsAction": {}, + "cancelSkipAllFilteredAlarmsAction": "Откажи прескакање свих филтрираних аларма", + "@cancelSkipAllFilteredAlarmsAction": {}, + "deleteAllFilteredAction": "Обриши све филтриране ставке", + "@deleteAllFilteredAction": {}, + "alarmDescriptionToday": "Само данас", + "@alarmDescriptionToday": {}, + "alarmDescriptionTomorrow": "Само сутра", + "@alarmDescriptionTomorrow": {}, + "alarmDescriptionEveryDay": "Сваког дана", + "@alarmDescriptionEveryDay": {}, + "alarmDescriptionWeekend": "Сваког викенда", + "@alarmDescriptionWeekend": {}, + "volumeWhileTasks": "Јачина звука током решавања задатака", + "@volumeWhileTasks": {}, + "selectionStatus": "{n} изабрано", + "@selectionStatus": {}, + "selectAll": "Изабери све", + "@selectAll": {}, + "reorder": "Прераспореди", + "@reorder": {}, + "alarmDescriptionWeekday": "Сваког радног дана", + "@alarmDescriptionWeekday": {}, + "alarmDescriptionDays": "На {days}", + "@alarmDescriptionDays": {}, + "alarmDescriptionRange": "{interval, select, daily{Дневно} weekly{Недељно} other{Остало}} од {startDate} до {endDate}", + "@alarmDescriptionRange": {}, + "stopwatchSlowest": "Најспорије", + "@stopwatchSlowest": {}, + "stopwatchAverage": "Просечно", + "@stopwatchAverage": {}, + "sundayShort": "Нед", + "@sundayShort": {}, + "widgetsSettingGroup": "Виџети", + "@widgetsSettingGroup": {}, + "alignmentTop": "Врх", + "@alignmentTop": {}, + "alignmentBottom": "Дно", + "@alignmentBottom": {}, + "alignmentLeft": "Лево", + "@alignmentLeft": {}, + "alignmentCenter": "Центар", + "@alignmentCenter": {}, + "alignmentRight": "Десно", + "@alignmentRight": {}, + "alignmentJustify": "Поравнај", + "@alignmentJustify": {}, + "fontWeightSetting": "Тежина фонта", + "@fontWeightSetting": {}, + "dateSettingGroup": "Датум", + "@dateSettingGroup": {}, + "timeSettingGroup": "Време", + "@timeSettingGroup": {}, + "sizeSetting": "Величина", + "@sizeSetting": {}, + "defaultPageSetting": "Подразумевана картица", + "@defaultPageSetting": {}, + "showMeridiemSetting": "Прикажи АМ/ПМ", + "@showMeridiemSetting": {}, + "editPresetsTitle": "Уреди предефинисане поставке", + "@editPresetsTitle": {}, + "firstDayOfWeekSetting": "Први дан у недељи", + "@firstDayOfWeekSetting": {}, + "translateLink": "Преведи", + "@translateLink": {}, + "translateDescription": "Помозите у превођењу апликације", + "@translateDescription": {}, + "yearsString": "{count, plural, =0{} =1{1 година} other{{count} година}}", + "@yearsString": {}, + "lessThanOneMinute": "мање од 1 минут", + "@lessThanOneMinute": {}, + "alarmRingInMessage": "Аларм ће се огласити за {duration}", + "@alarmRingInMessage": {}, + "nextAlarmIn": "Следеће: {duration}", + "@nextAlarmIn": {}, + "showForegroundNotificationDescription": "Прикажи упорно обавештење да би апликација остала активна", + "@showForegroundNotificationDescription": {}, + "notificationPermissionDescription": "Дозволи приказивање обавештења", + "@notificationPermissionDescription": {}, + "extraAnimationSettingDescription": "Прикажи анимације које нису полиране и могу изазвати пад кадрова на уређајима нижег ранга", + "@extraAnimationSettingDescription": {}, + "defaultSettingGroup": "Подразумевана подешавања", + "@defaultSettingGroup": {}, + "filtersSettingGroup": "Филтери", + "@filtersSettingGroup": {}, + "audioChannelRingtone": "Звоно", + "@audioChannelRingtone": {}, + "settings": "Подешавања", + "@settings": {}, + "notificationsSettingGroup": "Обавештења", + "@notificationsSettingGroup": {}, + "showNotificationSetting": "Прикажи обавештење", + "@showNotificationSetting": {}, + "packageNameLabel": "Име пакета", + "@packageNameLabel": {}, + "timeToFullVolumeSetting": "Време до пуне јачине звука", + "@timeToFullVolumeSetting": {}, + "snoozeSettingGroup": "Одлагање", + "@snoozeSettingGroup": {}, + "snoozeEnableSetting": "Омогућено", + "@snoozeEnableSetting": {}, + "snoozeLengthSetting": "Дужина", + "@snoozeLengthSetting": {}, + "tasksSetting": "Задаци", + "@tasksSetting": {}, + "audioChannelMedia": "Медији", + "@audioChannelMedia": {}, + "volumeSetting": "Јачина звука", + "@volumeSetting": {}, + "maxSnoozesSetting": "Максимално одлагања", + "@maxSnoozesSetting": {}, + "snoozePreventDisablingSetting": "Спречи онемогућавање", + "@snoozePreventDisablingSetting": {}, + "snoozePreventDeletionSetting": "Спречи брисање", + "@snoozePreventDeletionSetting": {}, + "whileSnoozedSettingGroup": "Док је одложено", + "@whileSnoozedSettingGroup": {}, + "noItemMessage": "Још увек нема додатих {ставки}", + "@noItemMessage": {}, + "showSortSetting": "Прикажи сортирање", + "@showSortSetting": {}, + "tuesdayFull": "Уторак", + "@tuesdayFull": {}, + "stopwatchPrevious": "Претходно", + "@stopwatchPrevious": {}, + "stopwatchFastest": "Најбрже", + "@stopwatchFastest": {}, + "alarmDescriptionWeekly": "Сваког {days}", + "@alarmDescriptionWeekly": {}, + "combinedTime": "{hours} и {minutes}", + "@combinedTime": {}, + "pickerNumpad": "Нумпад", + "@pickerNumpad": {}, + "alarmDescriptionDates": "На {date}{count, plural, =0{} =1{ и још 1 датум} other{ и још {count} датума}}", + "@alarmDescriptionDates": {}, + "showUpcomingAlarmNotificationSetting": "Прикажи обавештења о предстојећим алармима", + "@showUpcomingAlarmNotificationSetting": {}, + "upcomingLeadTimeSetting": "Време предстојећег обавештења", + "@upcomingLeadTimeSetting": {}, + "alarmsDefaultSettingGroupDescription": "Постави подразумеване вредности за нове аларме", + "@alarmsDefaultSettingGroupDescription": {}, + "timerDefaultSettingGroupDescription": "Постави подразумеване вредности за нове тајмере", + "@timerDefaultSettingGroupDescription": {}, + "showFiltersSetting": "Прикажи филтере", + "@showFiltersSetting": {}, + "showSnoozeNotificationSetting": "Прикажи обавештења о одлагању", + "@showSnoozeNotificationSetting": {}, + "licenseLabel": "Лиценца", + "@licenseLabel": {}, + "elapsedTime": "Протекло време", + "@elapsedTime": {}, + "mondayFull": "Понедељак", + "@mondayFull": {}, + "wednesdayFull": "Среда", + "@wednesdayFull": {}, + "thursdayFull": "Четвртак", + "@thursdayFull": {}, + "versionLabel": "Верзија", + "@versionLabel": {}, + "addLengthSetting": "Додај дужину", + "@addLengthSetting": {}, + "fridayFull": "Петак", + "@fridayFull": {}, + "saturdayFull": "Субота", + "@saturdayFull": {}, + "tagNamePlaceholder": "Назив ознаке", + "@tagNamePlaceholder": {}, + "hoursString": "{count, plural, =0{} =1{1 сат} other{{count} сати}}", + "@hoursString": {}, + "shortHoursString": "{hours}ч", + "@shortHoursString": {}, + "shortMinutesString": "{minutes}м", + "@shortMinutesString": {}, + "shortSecondsString": "{seconds}с", + "@shortSecondsString": {}, + "showForegroundNotification": "Прикажи обавештење у првом плану", + "@showForegroundNotification": {}, + "showNextAlarm": "Прикажи следећи аларм", + "@showNextAlarm": {}, + "interactionsSettingGroup": "Интеракције", + "@interactionsSettingGroup": {}, + "longPressActionSetting": "Акција дугог притиска", + "@longPressActionSetting": {}, + "longPressReorderAction": "Прераспореди", + "@longPressReorderAction": {}, + "colorSchemeSetting": "Шема боја", + "@colorSchemeSetting": {}, + "darkColorSchemeSetting": "Тамна шема боја", + "@darkColorSchemeSetting": {}, + "clockSettingGroup": "Сат", + "@clockSettingGroup": {}, + "timerSettingGroup": "Тајмер", + "@timerSettingGroup": {}, + "stopwatchSettingGroup": "Штоперица", + "@stopwatchSettingGroup": {}, + "backupSettingGroupDescription": "Извези или увези своја подешавања локално", + "@backupSettingGroupDescription": {}, + "alarmWeekdaysSetting": "Дани у недељи", + "@alarmWeekdaysSetting": {}, + "alarmDatesSetting": "Датуми", + "@alarmDatesSetting": {}, + "alarmRangeSetting": "Распон датума", + "@alarmRangeSetting": {}, + "alarmIntervalSetting": "Интервал", + "@alarmIntervalSetting": {}, + "alarmIntervalDaily": "Дневно", + "@alarmIntervalDaily": {}, + "alarmIntervalWeekly": "Недељно", + "@alarmIntervalWeekly": {}, + "alarmDeleteAfterRingingSetting": "Обриши након одлагања", + "@alarmDeleteAfterRingingSetting": {}, + "cannotDisableAlarmWhileSnoozedSnackbar": "Не може се онемогућити аларм док је одложен", + "@cannotDisableAlarmWhileSnoozedSnackbar": {}, + "selectTime": "Изабери време", + "@selectTime": {}, + "timePickerModeButton": "Режим", + "@timePickerModeButton": {}, + "cancelButton": "Откажи", + "@cancelButton": {}, + "customizeButton": "Прилагоди", + "@customizeButton": {}, + "saveButton": "Сачувај", + "@saveButton": {}, + "labelField": "Ознака", + "@labelField": {}, + "labelFieldPlaceholder": "Додај ознаку", + "@labelFieldPlaceholder": {}, + "alarmScheduleSettingGroup": "Распоред", + "@alarmScheduleSettingGroup": {}, + "scheduleTypeField": "Тип", + "@scheduleTypeField": {}, + "scheduleTypeOnceDescription": "Звониће при следећем појављивању времена", + "@scheduleTypeOnceDescription": {}, + "scheduleTypeDailyDescription": "Звониће сваког дана", + "@scheduleTypeDailyDescription": {}, + "scheduleTypeWeek": "У одређене дане у недељи", + "@scheduleTypeWeek": {}, + "scheduleTypeWeekDescription": "Понављаће се у одређене дане у недељи", + "@scheduleTypeWeekDescription": {}, + "scheduleTypeDate": "У одређене датуме", + "@scheduleTypeDate": {}, + "scheduleTypeRangeDescription": "Понављаће се током одређеног распона датума", + "@scheduleTypeRangeDescription": {}, + "soundAndVibrationSettingGroup": "Звук и вибрација", + "@soundAndVibrationSettingGroup": {}, + "soundSettingGroup": "Звук", + "@soundSettingGroup": {}, + "settingGroupMore": "Више", + "@settingGroupMore": {}, + "melodySetting": "Мелодија", + "@melodySetting": {}, + "vibrationSetting": "Вибрација", + "@vibrationSetting": {}, + "mathEasyDifficulty": "Лако (X + Y)", + "@mathEasyDifficulty": {}, + "mathMediumDifficulty": "Средње (X × Y)", + "@mathMediumDifficulty": {}, + "noAlarmMessage": "Нема креираних аларма", + "@noAlarmMessage": {}, + "noTimerMessage": "Нема креираних тајмера", + "@noTimerMessage": {}, + "numberOfProblemsSetting": "Број проблема", + "@numberOfProblemsSetting": {}, + "saveReminderAlert": "Да ли желите да изађете без чувања?", + "@saveReminderAlert": {}, + "yesButton": "Да", + "@yesButton": {}, + "noButton": "Не", + "@noButton": {}, + "cancelSkipAlarmButton": "Откажи прескакање", + "@cancelSkipAlarmButton": {}, + "noTagsMessage": "Нема креираних ознака", + "@noTagsMessage": {}, + "noStopwatchMessage": "Нема креираних штоперица", + "@noStopwatchMessage": {}, + "noTaskMessage": "Нема креираних задатака", + "@noTaskMessage": {}, + "noPresetsMessage": "Нема креираних унапред подешених вредности", + "@noPresetsMessage": {}, + "noLogsMessage": "Нема дневника аларма", + "@noLogsMessage": {}, + "deleteButton": "Обриши", + "@deleteButton": {}, + "duplicateButton": "Дуплирај", + "@duplicateButton": {}, + "skipAlarmButton": "Прескочи следећи аларм", + "@skipAlarmButton": {}, + "todayFilter": "Данас", + "@todayFilter": {}, + "tomorrowFilter": "Сутра", + "@tomorrowFilter": {}, + "stateFilterGroup": "Стање", + "@stateFilterGroup": {}, + "activeFilter": "Активан", + "@activeFilter": {}, + "inactiveFilter": "Неактиван", + "@inactiveFilter": {}, + "snoozedFilter": "Одложен", + "@snoozedFilter": {}, + "disabledFilter": "Онемогућен", + "@disabledFilter": {}, + "clearFiltersAction": "Очисти све филтере", + "@clearFiltersAction": {}, + "remainingTimeAsc": "Највише времена преостало", + "@remainingTimeAsc": {}, + "durationAsc": "Најкраће", + "@durationAsc": {}, + "durationDesc": "Најдуже", + "@durationDesc": {}, + "nameDesc": "Ознака Ш-А", + "@nameDesc": {}, + "nameAsc": "Ознака А-Ш", + "@nameAsc": {}, + "timeOfDayAsc": "Рани сати први", + "@timeOfDayAsc": {}, + "timeOfDayDesc": "Касни сати први", + "@timeOfDayDesc": {}, + "filterActions": "Филтер акције", + "@filterActions": {}, + "skippingDescriptionSuffix": "(прескакање следећег појављивања)", + "@skippingDescriptionSuffix": {}, + "alarmDescriptionSnooze": "Одложено до {date}", + "@alarmDescriptionSnooze": {}, + "alarmDescriptionFinished": "Нема будућих датума", + "@alarmDescriptionFinished": {}, + "alarmDescriptionNotScheduled": "Није заказано", + "@alarmDescriptionNotScheduled": {}, + "presetsSetting": "Пресети", + "@presetsSetting": {}, + "newPresetPlaceholder": "Нови пресет", + "@newPresetPlaceholder": {}, + "dismissActionSetting": "Тип акције за одбацивање", + "@dismissActionSetting": {}, + "dismissActionSlide": "Клизање", + "@dismissActionSlide": {}, + "dismissActionButtons": "Дугмад", + "@dismissActionButtons": {}, + "dismissActionAreaButtons": "Дугмад у области", + "@dismissActionAreaButtons": {}, + "stopwatchTimeFormatSettingGroup": "Формат времена", + "@stopwatchTimeFormatSettingGroup": {}, + "stopwatchShowMillisecondsSetting": "Прикажи милисекунде", + "@stopwatchShowMillisecondsSetting": {}, + "comparisonLapBarsSettingGroup": "Поређење кругова", + "@comparisonLapBarsSettingGroup": {}, + "showPreviousLapSetting": "Прикажи претходни круг", + "@showPreviousLapSetting": {}, + "showFastestLapSetting": "Прикажи најбржи круг", + "@showFastestLapSetting": {}, + "showAverageLapSetting": "Прикажи просечан круг", + "@showAverageLapSetting": {}, + "showSlowestLapSetting": "Прикажи најспорији круг", + "@showSlowestLapSetting": {}, + "leftHandedSetting": "Режим за леворуке", + "@leftHandedSetting": {}, + "exportSettingsSetting": "Извоз", + "@exportSettingsSetting": {}, + "exportSettingsSettingDescription": "Извези подешавања у локални фајл", + "@exportSettingsSettingDescription": {}, + "importSettingsSetting": "Увоз", + "@importSettingsSetting": {}, + "importSettingsSettingDescription": "Увези подешавања из локалног фајла", + "@importSettingsSettingDescription": {}, + "emailLabel": "Имејл", + "@emailLabel": {}, + "viewOnGithubLabel": "Погледај на GitHub-у", + "@viewOnGithubLabel": {}, + "openSourceLicensesSetting": "Лиценце отвореног кода", + "@openSourceLicensesSetting": {}, + "contributorsSetting": "Сарадници", + "@contributorsSetting": {}, + "donorsSetting": "Донатори", + "@donorsSetting": {}, + "donateButton": "Донација", + "@donateButton": {}, + "relativeTime": "{hours}ч {relative, select, ahead{унапред} behind{уназад} other{Остало}}", + "@relativeTime": {}, + "sameTime": "Исто време", + "@sameTime": {}, + "searchSettingPlaceholder": "Претражи подешавање", + "@searchSettingPlaceholder": {}, + "searchCityPlaceholder": "Претражи град", + "@searchCityPlaceholder": {}, + "cityAlreadyInFavorites": "Овај град је већ у вашим омиљеним", + "@cityAlreadyInFavorites": {}, + "durationPickerTitle": "Изабери трајање", + "@durationPickerTitle": {}, + "editButton": "Уреди", + "@editButton": {}, + "noLapsMessage": "Још нема кругова", + "@noLapsMessage": {}, + "sundayFull": "Недеља", + "@sundayFull": {}, + "mondayShort": "Пон", + "@mondayShort": {}, + "tuesdayShort": "Уто", + "@tuesdayShort": {}, + "wednesdayShort": "Сре", + "@wednesdayShort": {}, + "thursdayShort": "Чет", + "@thursdayShort": {}, + "fridayShort": "Пет", + "@fridayShort": {}, + "saturdayShort": "Суб", + "@saturdayShort": {}, + "contributorsDescription": "Људи који омогућавају ову апликацију", + "@contributorsDescription": {}, + "mondayLetter": "П", + "@mondayLetter": {}, + "tuesdayLetter": "У", + "@tuesdayLetter": {}, + "wednesdayLetter": "С", + "@wednesdayLetter": {}, + "thursdayLetter": "Ч", + "@thursdayLetter": {}, + "fridayLetter": "П", + "@fridayLetter": {}, + "saturdayLetter": "С", + "@saturdayLetter": {}, + "sundayLetter": "Н", + "@sundayLetter": {}, + "donateDescription": "Донирајте за подршку развоју апликације", + "@donateDescription": {}, + "donorsDescription": "Наши великодушни патрони", + "@donorsDescription": {}, + "digitalClockSettingGroup": "Дигитални сат", + "@digitalClockSettingGroup": {}, + "layoutSettingGroup": "Распоред", + "@layoutSettingGroup": {}, + "textSettingGroup": "Текст", + "@textSettingGroup": {}, + "showDateSetting": "Прикажи датум", + "@showDateSetting": {}, + "settingsTitle": "Подешавања", + "@settingsTitle": {}, + "horizontalAlignmentSetting": "Хоризонтално поравнање", + "@horizontalAlignmentSetting": {}, + "verticalAlignmentSetting": "Вертикално поравнање", + "@verticalAlignmentSetting": {}, + "separatorSetting": "Сепаратор", + "@separatorSetting": {}, + "editTagLabel": "Уреди ознаку", + "@editTagLabel": {}, + "minutesString": "{count, plural, =0{} =1{1 минут} other{{count} минута}}", + "@minutesString": {}, + "secondsString": "{count, plural, =0{} =1{1 секунда} other{{count} секунди}}", + "@secondsString": {}, + "daysString": "{count, plural, =0{} =1{1 дан} other{{count} дана}}", + "@daysString": {}, + "weeksString": "{count, plural, =0{} =1{1 недеља} other{{count} недеља}}", + "@weeksString": {}, + "monthsString": "{count, plural, =0{} =1{1 месец} other{{count} месеци}}", + "@monthsString": {} } From 7454f82ca299819c02cfbd268a0900886296b995 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Tue, 17 Sep 2024 14:21:05 +0500 Subject: [PATCH 170/177] Fix translation error --- lib/l10n/app_sr.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_sr.arb b/lib/l10n/app_sr.arb index 8059b049..7511817c 100644 --- a/lib/l10n/app_sr.arb +++ b/lib/l10n/app_sr.arb @@ -401,7 +401,7 @@ "@snoozePreventDeletionSetting": {}, "whileSnoozedSettingGroup": "Док је одложено", "@whileSnoozedSettingGroup": {}, - "noItemMessage": "Још увек нема додатих {ставки}", + "noItemMessage": "Још увек нема додатих {items}", "@noItemMessage": {}, "showSortSetting": "Прикажи сортирање", "@showSortSetting": {}, From 7bfb00a0536c1162a2ab722cf133dacbe1539504 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Wed, 18 Sep 2024 07:18:08 +0500 Subject: [PATCH 171/177] Prevent picking folder without storage permission --- lib/audio/screens/ringtones_screen.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/audio/screens/ringtones_screen.dart b/lib/audio/screens/ringtones_screen.dart index 340c8f08..2594114a 100644 --- a/lib/audio/screens/ringtones_screen.dart +++ b/lib/audio/screens/ringtones_screen.dart @@ -2,6 +2,7 @@ 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'; @@ -114,16 +115,18 @@ class _RingtonesScreenState extends State { icon: Icons.create_new_folder_rounded, bottomPadding: 8, onPressed: () async { - if (androidInfo!.version.sdkInt >= 33) { - if (!await Permission.audio.isGranted) { - await Permission.audio.request(); - } - } else { - if (!await Permission.storage.isGranted) { - await Permission.storage.request(); + 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 = From 141a7bddc303f2bc9306dedb3753254334876f52 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Wed, 18 Sep 2024 08:02:12 +0500 Subject: [PATCH 172/177] Update contributors --- assets/contributors/avatars/170783727?v=4.jpg | Bin 0 -> 2082 bytes assets/contributors/git.json | 5 +++ .../metadata/android/en-US/changelogs/261.txt | 32 +++++++++++++++++ .../metadata/android/en-US/changelogs/262.txt | 32 +++++++++++++++++ .../metadata/android/en-US/changelogs/263.txt | 33 ++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 assets/contributors/avatars/170783727?v=4.jpg create mode 100644 fastlane/metadata/android/en-US/changelogs/261.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/262.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/263.txt diff --git a/assets/contributors/avatars/170783727?v=4.jpg b/assets/contributors/avatars/170783727?v=4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d6cdc70eca95e5f830b172b698054fa21667e49 GIT binary patch literal 2082 zcmbW13s6&68pls^19IXJQZ zXG3d%6$vDRWC-UB@K!j;3WwGM26j#p+>(J`42OpZCZ+E{F&s- ztJkuxe|6*QZ)Eb@1$PvMMR)H%C@ZgcSo!F2-FNj*zi()4YHok7>Ug2pH5>H8r*V4PW3N ze4@psP*y3J-CHg&#G*hMW=!Dq7z2q27X@Q#6db*knOk^J=iSV@_hp-$Uf=^~du7$r zgnY2%>G_5O?ez4)o%Dc&+!8m!l^j12o68sKP1R{|FKlOw7Hf|f&(LWu^GP0lhKERS zcl*hV8)Ca&+q$?agqJ)06fTL4shxcy9eJkIVG~j)^-bcHwN&1e+(mW3dPBS;E9d3V zgt$m*tX(LYYE-Hkke`qs!__{zhEEH?(3k*oTuxBfWHUV9E7bB)uw9OVYW@xh@jS;@ za<(wpkcPj-r;%89I1$(R?y5r0(7z18u<%RiZ&9j$E_rB&%Ckw5>!0^6gY7#?p}JfhBLjJ))RiJx zeLaJ|+51DMDgWVY$#w0Yi*DZEBL25OJL*_R@YRu@;%K!>6>@oGO?t2W2(=22+#b00*VJCO!oe4Ai|pvtG8gXQnSQ&_?k*n zd4%idUX@qKXId^ji>vty3f`6WvIJFu8H9UU-TmehQ@Ef=~e84{t>3Q^jTrF zVt8slT`~%5mOWSYm$VwE;JV+S$~1bz8tOaEar_kG@ZfpW^1hX8P6q5;R+A~FsBg!2 zKE78R_zm}>gqf#Zk|T@F_3QWbD_j_W%$|1ZAQV(I89z!b9eY|Y4%ah=uNu_ktRa^C zb&Z(VXWJF$jrdq&dW>F3J2OX@Rf&mKMyaQZF@Ef4X1Ej~w+Z)T4Z&WEo9>=4`K5^# z8wuRzTKvIjAn6)TOSqXM2otd{2>N}7lFG9ERI83I$CVGHSbW!#MCBGg(aErK2(g+u zSHcu=W0I$#4+YIthJ6zci1XcFw667>(=v>@b(bdICzoh;3VRihR5Z3Obs}~l9UhU0 zMV5c1&L~~X+*QcA)SGmHK*Z!y+rJ*6m)IF@prDJxKBJv)e4GsgpdB(n!H9% zSpX5A$SOXL40+BQSg?lHq3pjFHZv6>=6m1jer21ODe(Cr#sWULH{)_!1gR!#wYzIV z$RR#0GqR)L;Nf Date: Tue, 17 Sep 2024 09:33:16 +0000 Subject: [PATCH 173/177] Translated using Weblate (Spanish) Currently translated at 99.7% (389 of 390 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/es/ --- lib/l10n/app_es.arb | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ac6ac51b..1e9f756f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -728,5 +728,53 @@ "selectAll": "Seleccionar todo", "@selectAll": {}, "reorder": "Reordenar", - "@reorder": {} + "@reorder": {}, + "startMelodyAtRandomPos": "Posición aleatoria", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "La melodía comenzará en una posición aleatoria", + "@startMelodyAtRandomPosDescription": {}, + "shuffleAlarmMelodiesAction": "Melodías aleatorias para todas las alarmas filtradas", + "@shuffleAlarmMelodiesAction": {}, + "resetAllFilteredTimersAction": "Reiniciar todos los temporizadores filtrados", + "@resetAllFilteredTimersAction": {}, + "playAllFilteredTimersAction": "Reproducir todos los temporizadores filtrados", + "@playAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Pausar todos los temporizadores filtrados", + "@pauseAllFilteredTimersAction": {}, + "appLogs": "Registros de aplicaciones", + "@appLogs": {}, + "shuffleTimerMelodiesAction": "Melodías aleatorias para todos los temporizadores filtrados", + "@shuffleTimerMelodiesAction": {}, + "showNumbersSetting": "Ver números", + "@showNumbersSetting": {}, + "clockStyleSettingGroup": "Estilo reloj", + "@clockStyleSettingGroup": {}, + "clockTypeSetting": "Tipo de reloj", + "@clockTypeSetting": {}, + "analogClock": "Analógico", + "@analogClock": {}, + "digitalClock": "Digital", + "@digitalClock": {}, + "allNumbers": "Todos los números", + "@allNumbers": {}, + "none": "Ninguno", + "@none": {}, + "arabicNumeral": "Arábigas", + "@arabicNumeral": {}, + "showDigitalClock": "Mostrar reloj digital", + "@showDigitalClock": {}, + "numeralTypeSetting": "Tipo de numeral", + "@numeralTypeSetting": {}, + "romanNumeral": "Romanos", + "@romanNumeral": {}, + "backgroundServiceIntervalSettingDescription": "Un intervalo más bajo ayudará a mantener activa la aplicación, a costa de algo de duración en la batería", + "@backgroundServiceIntervalSettingDescription": {}, + "showClockTicksSetting": "Mostrar marcas", + "@showClockTicksSetting": {}, + "majorTicks": "Solo las mejores marcas", + "@majorTicks": {}, + "allTicks": "Toda las marcas", + "@allTicks": {}, + "backgroundServiceIntervalSetting": "Intervalo del servicio en segundo plano", + "@backgroundServiceIntervalSetting": {} } From 278524192f08c6a77af3f1c0f82a1dcd8477d0ac Mon Sep 17 00:00:00 2001 From: Kuzmich55 Date: Tue, 17 Sep 2024 10:20:43 +0000 Subject: [PATCH 174/177] Translated using Weblate (Russian) Currently translated at 100.0% (390 of 390 strings) Translation: Chrono/App Translate-URL: https://hosted.weblate.org/projects/chrono/app/ru/ --- lib/l10n/app_ru.arb | 82 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 85a67bf3..eedf2e6b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -35,7 +35,7 @@ "@overrideAccentSetting": {}, "materialBrightnessSetting": "Тема", "@materialBrightnessSetting": {}, - "styleThemeSetting": "Тема оформления", + "styleThemeSetting": "Выбор стиля", "@styleThemeSetting": {}, "useMaterialStyleSetting": "Использовать Material дизайн", "@useMaterialStyleSetting": {}, @@ -195,7 +195,7 @@ "@durationPickerSetting": {}, "showIstantAlarmButtonSetting": "Показать кнопку быстрого будильника", "@showIstantAlarmButtonSetting": {}, - "maxLogsSetting": "Макс. журналов", + "maxLogsSetting": "Макс. журналов будильников", "@maxLogsSetting": {}, "snoozeLengthSetting": "Длина", "@snoozeLengthSetting": {}, @@ -669,7 +669,7 @@ "@showMeridiemSetting": {}, "dismissActionSlide": "Скольжение", "@dismissActionSlide": {}, - "relativeTime": "{hours}ч {relative, select, ahead{впереди} behind{позади} other{Другое}}", + "relativeTime": "на {hours}ч {relative, select, ahead{опережает} behind{отстаёт} other{другое}}", "@relativeTime": {}, "alarmDescriptionDates": "{date}{count, plural, =0{} =1{ и ещё 1 дата} other{ и ещё {count} дат(ы)}}", "@alarmDescriptionDates": {}, @@ -712,5 +712,79 @@ "monthsString": "{count, plural, =0{} =1{1 месяц} other{{count} месяца(ев)}}", "@monthsString": {}, "yearsString": "{count, plural, =0{} =1{1 год} other{{count} года(лет)}}", - "@yearsString": {} + "@yearsString": {}, + "appLogs": "Журналы приложения", + "@appLogs": {}, + "showErrorSnackbars": "Показать снэкбары ошибок", + "@showErrorSnackbars": {}, + "clearLogs": "Очистить журналы", + "@clearLogs": {}, + "selectionStatus": "{n} выбрано", + "@selectionStatus": {}, + "shuffleTimerMelodiesAction": "Случайно перемешать мелодии для всех отобранных таймеров", + "@shuffleTimerMelodiesAction": {}, + "pickerNumpad": "Цифровая клавиатура", + "@pickerNumpad": {}, + "saveLogs": "Сохранить журналы", + "@saveLogs": {}, + "interactionsSettingGroup": "Взаимодействие", + "@interactionsSettingGroup": {}, + "longPressReorderAction": "Изменить порядок", + "@longPressReorderAction": {}, + "longPressActionSetting": "Действие при долгом нажатии", + "@longPressActionSetting": {}, + "longPressSelectAction": "Множественный выбор", + "@longPressSelectAction": {}, + "startMelodyAtRandomPos": "Случайная позиция", + "@startMelodyAtRandomPos": {}, + "startMelodyAtRandomPosDescription": "Мелодия начнётся с произвольной позиции", + "@startMelodyAtRandomPosDescription": {}, + "volumeWhileTasks": "Громкость при решении заданий", + "@volumeWhileTasks": {}, + "selectAll": "Выбрать все", + "@selectAll": {}, + "reorder": "Изменить порядок", + "@reorder": {}, + "shuffleAlarmMelodiesAction": "Случайно перемешать мелодии для всех отобранных сигналов будильника", + "@shuffleAlarmMelodiesAction": {}, + "resetAllFilteredTimersAction": "Сбросить все отобранные таймеры", + "@resetAllFilteredTimersAction": {}, + "pauseAllFilteredTimersAction": "Приостановить все отобранные таймеры", + "@pauseAllFilteredTimersAction": {}, + "clockTypeSetting": "Тип часов", + "@clockTypeSetting": {}, + "playAllFilteredTimersAction": "Воспроизвести все отобранные таймеры", + "@playAllFilteredTimersAction": {}, + "clockStyleSettingGroup": "Стиль часов", + "@clockStyleSettingGroup": {}, + "analogClock": "Стрелочные", + "@analogClock": {}, + "digitalClock": "Цифровые", + "@digitalClock": {}, + "showClockTicksSetting": "Показать деления", + "@showClockTicksSetting": {}, + "majorTicks": "Только крупные деления", + "@majorTicks": {}, + "allTicks": "Все деления", + "@allTicks": {}, + "showNumbersSetting": "Показать цифры", + "@showNumbersSetting": {}, + "quarterNumbers": "Только четверти часа", + "@quarterNumbers": {}, + "allNumbers": "Все цифры", + "@allNumbers": {}, + "none": "Нет", + "@none": {}, + "numeralTypeSetting": "Тип цифр", + "@numeralTypeSetting": {}, + "arabicNumeral": "Арабские", + "@arabicNumeral": {}, + "romanNumeral": "Римские", + "@romanNumeral": {}, + "showDigitalClock": "Показать цифровые часы", + "@showDigitalClock": {}, + "backgroundServiceIntervalSetting": "Интервал обновления фоновой службы", + "@backgroundServiceIntervalSetting": {}, + "backgroundServiceIntervalSettingDescription": "Установка более короткого интервала позволит сохранить приложение активным, но может привести к увеличению расхода батареи", + "@backgroundServiceIntervalSettingDescription": {} } From e9dd39cb6223417e5d9e38119ba1a12fbd757cb8 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 20 Sep 2024 00:00:55 +0500 Subject: [PATCH 175/177] Fix snoozing alarm not closing notification screen --- lib/alarm/screens/alarm_notification_screen.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index 33dd8f6b..ec92e689 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -49,8 +49,8 @@ class _AlarmNotificationScreenState extends State { widget.onPop!(); Navigator.of(context).pop(true); } else { - dismissAlarmNotification(widget.scheduleId, - widget.dismissType, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, widget.dismissType, + ScheduledNotificationType.alarm); } } else { IsolateNameServer.lookupPortByName(setAlarmVolumePortName) @@ -68,8 +68,8 @@ class _AlarmNotificationScreenState extends State { Alarm? currentAlarm = getAlarmById(widget.scheduleId); if (currentAlarm == null) { - dismissAlarmNotification(widget.scheduleId, - widget.dismissType, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, widget.dismissType, + ScheduledNotificationType.alarm); return; } alarm = currentAlarm; @@ -96,8 +96,8 @@ class _AlarmNotificationScreenState extends State { } void _snoozeAlarm() { - snoozeAlarm( - widget.scheduleId, ScheduledNotificationType.alarm); + dismissAlarmNotification(widget.scheduleId, AlarmDismissType.snooze, + ScheduledNotificationType.alarm); } @override From 756e50c63b2735241f82fac3c476552404cff4ef Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Fri, 20 Sep 2024 00:33:07 +0500 Subject: [PATCH 176/177] Fix alarm label on notification screen --- .../screens/alarm_notification_screen.dart | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart index ec92e689..d4e82fe6 100644 --- a/lib/alarm/screens/alarm_notification_screen.dart +++ b/lib/alarm/screens/alarm_notification_screen.dart @@ -117,28 +117,34 @@ class _AlarmNotificationScreenState extends State { children: [ if (_currentIndex <= 0) Expanded( - flex: 1, - child: Column( - children: [ - const Spacer(), - if (alarm.label.isNotEmpty) + 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.label, - style: Theme.of(context).textTheme.titleMedium, + "Alarm", + style: Theme.of(context).textTheme.headlineMedium, ), - 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( From 662c329d1e968fd2ac9348a6835116700c2b0ee3 Mon Sep 17 00:00:00 2001 From: AhsanSarwar45 Date: Sat, 21 Sep 2024 14:37:45 +0500 Subject: [PATCH 177/177] Fix duplicate items when importing backup --- lib/alarm/types/alarm.dart | 4 ++ lib/common/types/tag.dart | 6 +++ lib/common/types/time.dart | 14 ++++++ lib/settings/data/backup_options.dart | 70 ++++++++++++++++----------- lib/settings/types/setting.dart | 7 +-- lib/settings/types/setting_group.dart | 4 ++ lib/settings/types/setting_item.dart | 10 ++-- lib/theme/types/color_scheme.dart | 16 ++++++ lib/theme/types/style_theme.dart | 12 ++++- lib/timer/types/time_duration.dart | 14 ++++++ lib/timer/types/timer.dart | 6 ++- lib/timer/types/timer_preset.dart | 5 ++ 12 files changed, 128 insertions(+), 40 deletions(-) diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart index bd23d5df..d4f004e3 100644 --- a/lib/alarm/types/alarm.dart +++ b/lib/alarm/types/alarm.dart @@ -450,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/common/types/tag.dart b/lib/common/types/tag.dart index d665fbfe..0e1fae0e 100644 --- a/lib/common/types/tag.dart +++ b/lib/common/types/tag.dart @@ -49,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/settings/data/backup_options.dart b/lib/settings/data/backup_options.dart index c6fc30f3..01c76d5a 100644 --- a/lib/settings/data/backup_options.dart +++ b/lib/settings/data/backup_options.dart @@ -4,6 +4,7 @@ import 'package:clock_app/alarm/logic/update_alarms.dart'; import 'package:clock_app/alarm/types/alarm.dart'; import 'package:clock_app/app.dart'; import 'package:clock_app/clock/types/city.dart'; +import 'package:clock_app/common/types/tag.dart'; import 'package:clock_app/common/utils/json_serialize.dart'; import 'package:clock_app/common/utils/list_storage.dart'; import 'package:clock_app/settings/data/settings_schema.dart'; @@ -27,11 +28,13 @@ final backupOptions = [ return await loadTextFile("tags"); }, decode: (context, value) async { - await saveList("tags", [ - ...listFromString(value) - .map((tag) => TimerPreset.from(tag)), - ...await loadList("tags") - ]); + final existingItems = await loadList("tags"); + final itemsToAdd = listFromString(value) + .where((tag) => + !existingItems.any((existingTag) => existingTag.isEqualTo(tag))) + .map((tag) => Tag.from(tag)); + + await saveList("tags", [...itemsToAdd, ...existingItems]); }, ), BackupOption( @@ -45,10 +48,15 @@ final backupOptions = [ return listToString(customColorSchemes); }, decode: (context, value) async { + final existingItems = await loadList("color_schemes"); + final itemsToAdd = listFromString(value) + .where((colorScheme) => !existingItems.any((existingColorScheme) => + existingColorScheme.isEqualTo(colorScheme))) + .map((scheme) => ColorSchemeData.from(scheme)); + await saveList("color_schemes", [ - ...listFromString(value) - .map((scheme) => ColorSchemeData.from(scheme)), - ...await loadList("color_schemes") + ...itemsToAdd, + ...existingItems, ]); if (context.mounted) App.refreshTheme(context); }, @@ -63,11 +71,13 @@ final backupOptions = [ return listToString(customThemes); }, decode: (context, value) async { - await saveList("style_themes", [ - ...listFromString(value) - .map((theme) => StyleTheme.from(theme)), - ...await loadList("style_themes") - ]); + final existingItems = await loadList("style_themes"); + final itemsToAdd = listFromString(value) + .where((theme) => !existingItems + .any((existingTheme) => existingTheme.isEqualTo(theme))) + .map((theme) => StyleTheme.from(theme)); + await saveList( + "style_themes", [...itemsToAdd, ...existingItems]); if (context.mounted) App.refreshTheme(context); }, ), @@ -94,10 +104,12 @@ final backupOptions = [ return await loadTextFile("alarms"); }, decode: (context, value) async { - await saveList("alarms", [ - ...listFromString(value).map((alarm) => Alarm.fromAlarm(alarm)), - ...await loadList("alarms") - ]); + final existingItems = await loadList("alarms"); + final itemsToAdd = listFromString(value) + .where((alarm) => !existingItems + .any((existingAlarm) => existingAlarm.isEqualTo(alarm))) + .map((alarm) => Alarm.fromAlarm(alarm)); + await saveList("alarms", [...itemsToAdd, ...existingItems]); await updateAlarms("Updated alarms on importing backup"); }, ), @@ -108,11 +120,12 @@ final backupOptions = [ return await loadTextFile("timers"); }, decode: (context, value) async { - await saveList("timers", [ - ...listFromString(value) - .map((timer) => ClockTimer.from(timer)), - ...await loadList("timers") - ]); + final existingItems = await loadList("timers"); + final itemsToAdd = listFromString(value) + .where((timer) => !existingItems + .any((existingTimer) => existingTimer.isEqualTo(timer))) + .map((timer) => ClockTimer.from(timer)); + await saveList("timers", [...itemsToAdd, ...existingItems]); await updateTimers("Updated timers on importing backup"); }, ), @@ -151,11 +164,14 @@ final backupOptions = [ return await loadTextFile("timer_presets"); }, decode: (context, value) async { - await saveList("timer_presets", [ - ...listFromString(value) - .map((preset) => TimerPreset.from(preset)), - ...await loadList("timer_presets") - ]); + final existingItems = await loadList("timer_presets"); + final itemsToAdd = listFromString(value) + .where((preset) => !existingItems + .any((existingPreset) => existingPreset.isEqualTo(preset))) + .map((preset) => TimerPreset.from(preset)); + + await saveList( + "timer_presets", [...itemsToAdd, ...existingItems]); }, ), ]; diff --git a/lib/settings/types/setting.dart b/lib/settings/types/setting.dart index af975507..1acbdae7 100644 --- a/lib/settings/types/setting.dart +++ b/lib/settings/types/setting.dart @@ -68,7 +68,8 @@ abstract class Setting extends SettingItem { } } -class CustomizableListSetting extends Setting> { +class CustomizableListSetting + extends Setting> { List possibleItems; Widget Function(T item, [VoidCallback?, VoidCallback?]) cardBuilder; Widget Function(T item) addCardBuilder; @@ -198,8 +199,6 @@ class ListSetting extends Setting> { ); } - - Widget getItemAddCard(T item) { return addCardBuilder(item); } @@ -209,7 +208,6 @@ class ListSetting extends Setting> { return cardBuilder(item, onDelete, onDuplicate); } - @override dynamic valueToJson() { return _value.map((e) => e.toJson()).toList(); @@ -222,7 +220,6 @@ class ListSetting extends Setting> { } } - class CustomSetting extends Setting { // The screen that will be navigated to when this setting is tapped. Widget Function(BuildContext, CustomSetting) screenBuilder; diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart index f72263f7..c2a7cc41 100644 --- a/lib/settings/types/setting_group.dart +++ b/lib/settings/types/setting_group.dart @@ -171,6 +171,10 @@ class SettingGroup extends SettingItem { } } + bool isEqualTo(SettingGroup other) { + return json.encode(valueToJson()) == json.encode(other.valueToJson()); + } + @override dynamic valueToJson() { Json json = {}; diff --git a/lib/settings/types/setting_item.dart b/lib/settings/types/setting_item.dart index 49d162c3..606c8587 100644 --- a/lib/settings/types/setting_item.dart +++ b/lib/settings/types/setting_item.dart @@ -16,11 +16,9 @@ abstract class SettingItem { // Settings which influence whether this setting is enabled List enableSettings; - String displayName(BuildContext context) => - getLocalizedName(context); + String displayName(BuildContext context) => getLocalizedName(context); - String displayDescription(BuildContext context) => - getDescription(context); + String displayDescription(BuildContext context) => getDescription(context); bool get isEnabled { for (var enableSetting in enableSettings) { @@ -48,8 +46,8 @@ abstract class SettingItem { return path.reversed.toList(); } - SettingItem( - this.name, this.getLocalizedName, this.getDescription, this.searchTags, this.enableConditions ) + SettingItem(this.name, this.getLocalizedName, this.getDescription, + this.searchTags, this.enableConditions) : id = name, _settingListeners = [], enableSettings = []; diff --git a/lib/theme/types/color_scheme.dart b/lib/theme/types/color_scheme.dart index 24be3a5c..ece52d7e 100644 --- a/lib/theme/types/color_scheme.dart +++ b/lib/theme/types/color_scheme.dart @@ -97,6 +97,22 @@ class ColorSchemeData extends ThemeItem { ColorSchemeData.fromJson(Json json) : super.fromJson(json, colorSchemeSettingsSchema.copy()); + + bool isEqualTo(ColorSchemeData other) { + return background == other.background && + error == other.error && + accent == other.accent && + onError == other.onError && + card == other.card && + onCard == other.onCard && + onAccent == other.onAccent && + onBackground == other.onBackground && + shadow == other.shadow && + outline == other.outline && + useAccentAsShadow == other.useAccentAsShadow && + useAccentAsOutline == other.useAccentAsOutline && + name == other.name; + } } ColorScheme getColorScheme(ColorSchemeData colorSchemeData) { diff --git a/lib/theme/types/style_theme.dart b/lib/theme/types/style_theme.dart index c4775334..f0be1244 100644 --- a/lib/theme/types/style_theme.dart +++ b/lib/theme/types/style_theme.dart @@ -40,7 +40,7 @@ class StyleTheme extends ThemeItem { .setValueWithoutNotify(borderWidth); } - StyleTheme.from(StyleTheme colorSchemeData) : super.from(colorSchemeData); + StyleTheme.from(StyleTheme super.colorSchemeData) : super.from(); @override String get name => settings.getSetting("Name").value; @@ -65,4 +65,14 @@ class StyleTheme extends ThemeItem { StyleTheme.fromJson(Json json) : super.fromJson(json, styleThemeSettingsSchema.copy()); + + bool isEqualTo(StyleTheme other) { + return name == other.name && + shadowElevation == other.shadowElevation && + shadowOpacity == other.shadowOpacity && + shadowBlurRadius == other.shadowBlurRadius && + shadowSpreadRadius == other.shadowSpreadRadius && + borderRadius == other.borderRadius && + borderWidth == other.borderWidth; + } } diff --git a/lib/timer/types/time_duration.dart b/lib/timer/types/time_duration.dart index fd7bc61f..7deeecf1 100644 --- a/lib/timer/types/time_duration.dart +++ b/lib/timer/types/time_duration.dart @@ -112,4 +112,18 @@ class TimeDuration extends JsonSerializable { minutes = json != null ? json['minutes'] ?? 0 : 0, seconds = json != null ? json['seconds'] ?? 0 : 0, milliseconds = json != null ? json['milliseconds'] ?? 0 : 0; + + @override + bool operator ==(Object other) { + if (other is TimeDuration) { + return hours == other.hours && + minutes == other.minutes && + seconds == other.seconds && + milliseconds == other.milliseconds; + } + return false; + } + + @override + int get hashCode => Object.hash(hours, minutes, seconds, milliseconds); } diff --git a/lib/timer/types/timer.dart b/lib/timer/types/timer.dart index 87f8ca2f..6cd2b9e7 100644 --- a/lib/timer/types/timer.dart +++ b/lib/timer/types/timer.dart @@ -153,7 +153,7 @@ class ClockTimer extends CustomizableListItem { } Future snooze() async { - TimeDuration addedDuration = TimeDuration(minutes: addLength.floor()); + TimeDuration addedDuration = TimeDuration(minutes: addLength.floor()); _currentDuration = addedDuration; _milliSecondsRemainingOnPause = addedDuration.inSeconds * 1000; await start(); @@ -262,4 +262,8 @@ class ClockTimer extends CustomizableListItem { copy() { return ClockTimer.from(this); } + + bool isEqualTo(ClockTimer other) { + return _duration == other._duration && _settings.isEqualTo(other._settings); + } } diff --git a/lib/timer/types/timer_preset.dart b/lib/timer/types/timer_preset.dart index 4a15b3a6..aef325c5 100644 --- a/lib/timer/types/timer_preset.dart +++ b/lib/timer/types/timer_preset.dart @@ -1,6 +1,7 @@ import 'package:clock_app/common/types/json.dart'; import 'package:clock_app/common/types/list_item.dart'; import 'package:clock_app/common/utils/id.dart'; +import 'package:clock_app/developer/logic/logger.dart'; import 'package:clock_app/timer/types/time_duration.dart'; class TimerPreset extends ListItem { @@ -45,6 +46,10 @@ class TimerPreset extends ListItem { } } + bool isEqualTo(TimerPreset other) { + return name == other.name && duration == other.duration; + } + @override copy() { return TimerPreset(name, duration);