diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 3a53d4b7f3..2943ed544f 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -100,4 +100,4 @@ jobs:
sed -i -e '/START Non-FOSS component/,/END Non-FOSS component/d' app/build.gradle.kts
- name: Build F-Droid variant
- run: ./gradlew :app:assembleRelease :app:check :app:lint --stacktrace
+ run: ./gradlew :app:assembleRelease :app:check :app:lint --stacktrace
\ No newline at end of file
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt b/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt
index edd42460e4..38db297c90 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt
@@ -5,13 +5,17 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.core.content.IntentCompat
import dev.dimension.flare.ui.FlareApp
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
class ShortcutComposeActivity : ComponentActivity() {
+ @OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
+ ComposeUiFlags.isMediaQueryIntegrationEnabled = true
super.onCreate(savedInstanceState)
val initialText =
when {
diff --git a/app/src/main/res/values-af-rZA/changelog.xml b/app/src/main/res/values-af-rZA/changelog.xml
index 3ea04e700d..88fec61a9b 100644
--- a/app/src/main/res/values-af-rZA/changelog.xml
+++ b/app/src/main/res/values-af-rZA/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ Weergawe %1$s:
+
+ - Deck Mode bygevoeg vir ’n meerkolom-tydlynervaring.
+ - Ryker tydlynoortjie-aanpassing bygevoeg, insluitend ikone, filters, groepe en voorkoms per oortjie.
+ - Regex-ondersteuning vir plaaslike sleutelwoordfilters bygevoeg.
+ - Werkverrigtingverbeterings en foutoplossings.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-ar-rSA/changelog.xml b/app/src/main/res/values-ar-rSA/changelog.xml
index dcfc43a6a4..6c55c68c7c 100644
--- a/app/src/main/res/values-ar-rSA/changelog.xml
+++ b/app/src/main/res/values-ar-rSA/changelog.xml
@@ -2,12 +2,15 @@
مرحبا بعودتك!
لقد قمنا ببعض التغييرات منذ آخر تسجيل دخولك. وفيما يلي التفاصيل:
- الإصدار %1$s:
+
+ الإصدار %1$s:
- - إضافة أوضاع جديدة لعرض الخط الزمني، بما في ذلك تصميم المعرض.
- - تحسين قراءة RSS ، المشاركة والترجمة والموجزات.
- - إعادة تنظيم إعدادات الظهور لتخصيص أسهل.
- - إصلاحات الشوائب وتحسينات الأداء.
+ - تمت إضافة وضع Deck لتجربة خط زمني متعددة الأعمدة.
+ - تمت إضافة تخصيص أوسع لتبويبات الخط الزمني، بما في ذلك الأيقونات والمرشحات والمجموعات والمظهر لكل تبويب.
+ - تمت إضافة دعم regex لمرشحات الكلمات المفتاحية المحلية.
+ - تحسينات في الأداء وإصلاحات للأخطاء.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-bg-rBG/changelog.xml b/app/src/main/res/values-bg-rBG/changelog.xml
index e23d00af21..9d0d8a0a53 100644
--- a/app/src/main/res/values-bg-rBG/changelog.xml
+++ b/app/src/main/res/values-bg-rBG/changelog.xml
@@ -1,4 +1,15 @@
Добре дошли отново!
+
+ Версия %1$s:
+
+ - Добавен е Deck Mode за времева линия с няколко колони.
+ - Добавено е по-богато персонализиране на разделите във времевата линия, включително икони, филтри, групи и външен вид за всеки раздел.
+ - Добавена е поддръжка на regex за локални филтри по ключови думи.
+ - Подобрения в производителността и корекции на грешки.
+
+ ]]>
+
diff --git a/app/src/main/res/values-ca-rES/changelog.xml b/app/src/main/res/values-ca-rES/changelog.xml
index 3ea04e700d..4a7045641b 100644
--- a/app/src/main/res/values-ca-rES/changelog.xml
+++ b/app/src/main/res/values-ca-rES/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ Versió %1$s:
+
+ - S’ha afegit el Deck Mode per a una experiència de línia de temps amb diverses columnes.
+ - S’ha afegit una personalització més completa de les pestanyes de la línia de temps, incloent-hi icones, filtres, grups i aparença per pestanya.
+ - S’ha afegit compatibilitat amb regex per als filtres locals de paraules clau.
+ - Millores de rendiment i correccions d’errors.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-cs-rCZ/changelog.xml b/app/src/main/res/values-cs-rCZ/changelog.xml
index 77278104a3..c2e925e369 100644
--- a/app/src/main/res/values-cs-rCZ/changelog.xml
+++ b/app/src/main/res/values-cs-rCZ/changelog.xml
@@ -2,4 +2,15 @@
Vítejte zpět!
Od posledního přihlášení jsme provedli nějaké změny. Zde jsou detaily:
+
+ Verze %1$s:
+
+ - Přidán Deck Mode pro vícesloupcové zobrazení časové osy.
+ - Přidáno bohatší přizpůsobení karet časové osy včetně ikon, filtrů, skupin a vzhledu jednotlivých karet.
+ - Přidána podpora regex pro místní filtry klíčových slov.
+ - Vylepšení výkonu a opravy chyb.
+
+ ]]>
+
diff --git a/app/src/main/res/values-da-rDK/changelog.xml b/app/src/main/res/values-da-rDK/changelog.xml
index e697a7192c..e989fd9dc2 100644
--- a/app/src/main/res/values-da-rDK/changelog.xml
+++ b/app/src/main/res/values-da-rDK/changelog.xml
@@ -2,12 +2,15 @@
Velkommen tilbage!
Vi har foretaget nogle ændringer siden du sidst er logget ind. Her er detaljerne:
- Version %1$s:
+
+ Version %1$s:
- - Tilføjet nye tidslinje visningstilstande, herunder et Galleri layout.
- - Forbedret RSS-læsning, deling, oversættelse og resuméer.
- - Omorganiseret udseende indstillinger for lettere tilpasning.
- - Fejlrettelser og forbedringer af ydeevnen.
+ - Tilføjet Deck Mode til en tidslinjeoplevelse med flere kolonner.
+ - Tilføjet mere avanceret tilpasning af tidslinjefaner, herunder ikoner, filtre, grupper og udseende pr. fane.
+ - Tilføjet regex-understøttelse for lokale nøgleordsfiltre.
+ - Ydelsesforbedringer og fejlrettelser.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-de-rDE/changelog.xml b/app/src/main/res/values-de-rDE/changelog.xml
index b2c27c89ee..588238d2ec 100644
--- a/app/src/main/res/values-de-rDE/changelog.xml
+++ b/app/src/main/res/values-de-rDE/changelog.xml
@@ -2,13 +2,15 @@
Willkommen zurück!
Seit der letzten Anmeldung haben wir einige Änderungen vorgenommen. Hier sind die Details:
- Version %1$s:
+
+ Version %1$s:
- - Neue Timeline-Anzeigemodi hinzugefügt inklusive Galerie-Layout.
-
- - Verbessertes RSS-Lesen teilen, übersetzen und zusammenfassen.
- - Umorganisierte Darstellungseinstellungen für einfachere Anpassung.
- - Fehlerbehebungen und Leistungsverbesserungen.
+ - Deck Mode für eine mehrspaltige Timeline-Ansicht hinzugefügt.
+ - Erweiterte Anpassung von Timeline-Tabs hinzugefügt, einschließlich Symbolen, Filtern, Gruppen und Erscheinungsbild pro Tab.
+ - Regex-Unterstützung für lokale Schlüsselwortfilter hinzugefügt.
+ - Leistungsverbesserungen und Fehlerbehebungen.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-el-rGR/changelog.xml b/app/src/main/res/values-el-rGR/changelog.xml
index a056ab27b4..b8a7b54734 100644
--- a/app/src/main/res/values-el-rGR/changelog.xml
+++ b/app/src/main/res/values-el-rGR/changelog.xml
@@ -2,12 +2,15 @@
Καλώς ήρθατε!
Έχουμε κάνει κάποιες αλλαγές από τότε που συνδεθήκατε την τελευταία φορά. Εδώ είναι οι λεπτομέρειες:
- Έκδοση %1$s:
+
+ Έκδοση %1$s:
- - Προστέθηκε νέα λειτουργία εμφάνισης χρονοδιαγράμματος, συμπεριλαμβανομένης μιας διάταξης συλλογής.
- - Βελτιωμένη ανάγνωση RSS, από κοινού, μετάφραση και περιλήψεις.
- - Επανοργανωμένες ρυθμίσεις εμφάνισης για ευκολότερη προσαρμογή.
- - Διορθώσεις σφαλμάτων και βελτιώσεις απόδοσης.
+ - Προστέθηκε το Deck Mode για εμπειρία χρονολογίου πολλών στηλών.
+ - Προστέθηκε πιο πλούσια προσαρμογή καρτελών χρονολογίου, με εικονίδια, φίλτρα, ομάδες και εμφάνιση ανά καρτέλα.
+ - Προστέθηκε υποστήριξη regex για τοπικά φίλτρα λέξεων-κλειδιών.
+ - Βελτιώσεις απόδοσης και διορθώσεις σφαλμάτων.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-es-rES/changelog.xml b/app/src/main/res/values-es-rES/changelog.xml
index 5b084cf4bf..759917e2a5 100644
--- a/app/src/main/res/values-es-rES/changelog.xml
+++ b/app/src/main/res/values-es-rES/changelog.xml
@@ -2,12 +2,15 @@
¡Bienvenido de nuevo!
Hemos realizado algunos cambios desde la última vez que iniciaste sesión. Estos son los detalles:
- Versión %1$s:
+
+ Versión %1$s:
- - Se añadieron nuevos modos de visualización de timeline, incluyendo un diseño de galería.
- - Lectura RSS mejorada, compartir, traducir y resumir.
- - Ajustes de apariencia reorganizados para una personalización más fácil.
- - correcciones de errores y mejoras de rendimiento.
+ - Se añadió Deck Mode para una experiencia de línea de tiempo con varias columnas.
+ - Se añadió una personalización más completa de las pestañas de la línea de tiempo, incluyendo iconos, filtros, grupos y apariencia por pestaña.
+ - Se añadió compatibilidad con regex para los filtros locales de palabras clave.
+ - Mejoras de rendimiento y correcciones de errores.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-fi-rFI/changelog.xml b/app/src/main/res/values-fi-rFI/changelog.xml
index 13a7360670..c7962a1880 100644
--- a/app/src/main/res/values-fi-rFI/changelog.xml
+++ b/app/src/main/res/values-fi-rFI/changelog.xml
@@ -2,12 +2,15 @@
Tervetuloa takaisin!
Olemme tehneet joitakin muutoksia, koska olet viimeksi kirjautunut sisään. Tässä on yksityiskohdat:
- Versio %1$s:
+
+ Versio %1$s:
- - Lisätty uusia aikajanan näyttötiloja, mukaan lukien gallerian asettelu.
- - Parannettu RSS-lukema, jakaminen, kääntäminen ja yhteenvedot.
- - Järjestä uudelleen ulkoasun asetukset helpommin muokattavaksi.
- - Virheenkorjauksia ja suorituskyvyn parannuksia.
+ - Lisätty Deck Mode usean sarakkeen aikajanakokemusta varten.
+ - Lisätty monipuolisempi aikajanavälilehtien mukautus, mukaan lukien kuvakkeet, suodattimet, ryhmät ja välilehtikohtainen ulkoasu.
+ - Lisätty regex-tuki paikallisille avainsanasuodattimille.
+ - Suorituskykyparannuksia ja virheenkorjauksia.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-hu-rHU/changelog.xml b/app/src/main/res/values-hu-rHU/changelog.xml
index 3ea04e700d..1a5e6fa23e 100644
--- a/app/src/main/res/values-hu-rHU/changelog.xml
+++ b/app/src/main/res/values-hu-rHU/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ %1$s verzió:
+
+ - Új Deck Mode a többoszlopos idővonalnézethez.
+ - Részletesebb idővonallap-testreszabás ikonokkal, szűrőkkel, csoportokkal és laponkénti megjelenéssel.
+ - Regex-támogatás a helyi kulcsszószűrőkhöz.
+ - Teljesítményjavítások és hibajavítások.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-iw-rIL/changelog.xml b/app/src/main/res/values-iw-rIL/changelog.xml
index 3ea04e700d..718e5891d8 100644
--- a/app/src/main/res/values-iw-rIL/changelog.xml
+++ b/app/src/main/res/values-iw-rIL/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ גרסה %1$s:
+
+ - נוסף Deck Mode לחוויית ציר זמן מרובת עמודות.
+ - נוספה התאמה אישית עשירה יותר ללשוניות ציר הזמן, כולל סמלים, מסננים, קבוצות ומראה לכל לשונית.
+ - נוספה תמיכה ב-regex למסנני מילות מפתח מקומיים.
+ - שיפורי ביצועים ותיקוני באגים.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-ja-rJP/changelog.xml b/app/src/main/res/values-ja-rJP/changelog.xml
index 4f3110cb19..420762016b 100644
--- a/app/src/main/res/values-ja-rJP/changelog.xml
+++ b/app/src/main/res/values-ja-rJP/changelog.xml
@@ -2,4 +2,15 @@
おかえりなさい!
最後にログインしてからいくつかの変更がありました。詳細は次のとおりです:
+
+ バージョン %1$s:
+
+ - 複数列のタイムラインを利用できる Deck Mode を追加しました。
+ - アイコン、フィルター、グループ、タブごとの外観など、タイムラインタブのカスタマイズを強化しました。
+ - ローカルキーワードフィルターで正規表現をサポートしました。
+ - パフォーマンス改善と不具合修正を行いました。
+
+ ]]>
+
diff --git a/app/src/main/res/values-ko-rKR/changelog.xml b/app/src/main/res/values-ko-rKR/changelog.xml
index 3ea04e700d..f2cd40c5a1 100644
--- a/app/src/main/res/values-ko-rKR/changelog.xml
+++ b/app/src/main/res/values-ko-rKR/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ 버전 %1$s:
+
+ - 여러 열 타임라인을 위한 Deck Mode를 추가했습니다.
+ - 아이콘, 필터, 그룹, 탭별 모양을 포함한 타임라인 탭 사용자 지정을 강화했습니다.
+ - 로컬 키워드 필터에 regex 지원을 추가했습니다.
+ - 성능 개선 및 버그 수정.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-nl-rNL/changelog.xml b/app/src/main/res/values-nl-rNL/changelog.xml
index cf3f69074d..e5336b15ec 100644
--- a/app/src/main/res/values-nl-rNL/changelog.xml
+++ b/app/src/main/res/values-nl-rNL/changelog.xml
@@ -2,12 +2,15 @@
Welkom terug!
We hebben enkele wijzigingen aangebracht sinds de laatste keer dat u bent ingelogd. Hier zijn de details:
- Versie %1$s:
+
+ Versie %1$s:
- - heeft nieuwe tijdlijnweergave modi toegevoegd, inclusief een galerij lay-out.
- - verbeterd RSS-lezen. delen, vertalen en samenvattingen.
- - Gereorganiseerd uiterlijk-instellingen voor makkelijker aanpassingen.
- - bugfixes en prestatieverbeteringen.
+ - Deck Mode toegevoegd voor een tijdlijnervaring met meerdere kolommen.
+ - Uitgebreidere aanpassing van tijdlijntabbladen toegevoegd, inclusief pictogrammen, filters, groepen en uiterlijk per tabblad.
+ - Regex-ondersteuning toegevoegd voor lokale trefwoordfilters.
+ - Prestatieverbeteringen en bugfixes.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-no-rNO/changelog.xml b/app/src/main/res/values-no-rNO/changelog.xml
index ac64d1c716..794653cedd 100644
--- a/app/src/main/res/values-no-rNO/changelog.xml
+++ b/app/src/main/res/values-no-rNO/changelog.xml
@@ -2,12 +2,15 @@
Velkommen tilbake!
Vi har gjort noen endringer siden sist du logget inn. Her er detaljer:
- Versjon %1$s:
+
+ Versjon %1$s:
- - La til nye tidslinjevisningsmodus, inkludert et galleri layout.
- - Forbedret RSS-avlesning, deling, oversettelse, og oppsummeringer.
- - Reorganisert Innstilling for enklere tilpasning.
- - Feilrettinger og ytelsesforbedringer.
+ - Lagt til Deck Mode for en tidslinjeopplevelse med flere kolonner.
+ - Lagt til rikere tilpasning av tidslinjefaner, inkludert ikoner, filtre, grupper og utseende per fane.
+ - Lagt til regex-støtte for lokale nøkkelordfiltre.
+ - Ytelsesforbedringer og feilrettinger.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-pl-rPL/changelog.xml b/app/src/main/res/values-pl-rPL/changelog.xml
index e93bad2b81..638c1e5cfe 100644
--- a/app/src/main/res/values-pl-rPL/changelog.xml
+++ b/app/src/main/res/values-pl-rPL/changelog.xml
@@ -2,12 +2,15 @@
Witaj ponownie!
Wprowadziliśmy pewne zmiany od ostatniego logowania. Oto szczegóły:
- Wersja %1$s:
+
+ Wersja %1$s:
- - Dodano nowe tryby wyświetlania osi czasu, łącznie z układem galerii.
- - Ulepszone czytanie RSS, udostępnianie, tłumaczenie i streszczenia.
- - Przeorganizowane ustawienia wyglądu dla łatwiejszego dostosowywania.
- - Poprawki błędów i ulepszenia wydajności.
+ - Dodano Deck Mode dla wielokolumnowego widoku osi czasu.
+ - Dodano bogatsze dostosowywanie kart osi czasu, w tym ikony, filtry, grupy i wygląd każdej karty.
+ - Dodano obsługę regex dla lokalnych filtrów słów kluczowych.
+ - Ulepszenia wydajności i poprawki błędów.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-pt-rBR/changelog.xml b/app/src/main/res/values-pt-rBR/changelog.xml
index 777fac9ca7..d135806a24 100644
--- a/app/src/main/res/values-pt-rBR/changelog.xml
+++ b/app/src/main/res/values-pt-rBR/changelog.xml
@@ -2,4 +2,15 @@
Bem-vindo de volta!
Fizemos algumas alterações desde a última vez que você entrou. Aqui estão os detalhes:
+
+ Versão %1$s:
+
+ - Adicionado Deck Mode para uma experiência de linha do tempo em várias colunas.
+ - Adicionada personalização mais completa das abas da linha do tempo, incluindo ícones, filtros, grupos e aparência por aba.
+ - Adicionado suporte a regex para filtros locais de palavras-chave.
+ - Melhorias de desempenho e correções de bugs.
+
+ ]]>
+
diff --git a/app/src/main/res/values-pt-rPT/changelog.xml b/app/src/main/res/values-pt-rPT/changelog.xml
index 777fac9ca7..c91c49cdd9 100644
--- a/app/src/main/res/values-pt-rPT/changelog.xml
+++ b/app/src/main/res/values-pt-rPT/changelog.xml
@@ -2,4 +2,15 @@
Bem-vindo de volta!
Fizemos algumas alterações desde a última vez que você entrou. Aqui estão os detalhes:
+
+ Versão %1$s:
+
+ - Adicionado Deck Mode para uma experiência de linha temporal em várias colunas.
+ - Adicionada personalização mais completa dos separadores da linha temporal, incluindo ícones, filtros, grupos e aparência por separador.
+ - Adicionado suporte a regex para filtros locais de palavras-chave.
+ - Melhorias de desempenho e correções de erros.
+
+ ]]>
+
diff --git a/app/src/main/res/values-ro-rRO/changelog.xml b/app/src/main/res/values-ro-rRO/changelog.xml
index 69b6f3b27c..27d05996c5 100644
--- a/app/src/main/res/values-ro-rRO/changelog.xml
+++ b/app/src/main/res/values-ro-rRO/changelog.xml
@@ -2,12 +2,15 @@
Bine ai revenit!
Am făcut unele modificări de când v-ați autentificat ultima dată. Iată detaliile:
- Versiunea %1$s:
+
+ Versiunea %1$s:
- - a adăugat noi moduri de afişare a cronologiei, incluzând un aspect de galerie.
- - S-a îmbunătățit citirea RSS, partajarea, traducerea și rezumatele.
- - Setări reorganizate de Appearance pentru o personalizare mai ușoară.
- - Remedierea erorilor şi îmbunătăţirea performanţei.
+ - A fost adăugat Deck Mode pentru o cronologie pe mai multe coloane.
+ - A fost adăugată personalizare mai bogată pentru filele cronologiei, inclusiv pictograme, filtre, grupuri și aspect per filă.
+ - A fost adăugat suport regex pentru filtrele locale de cuvinte-cheie.
+ - Îmbunătățiri de performanță și remedieri de erori.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-ru-rRU/changelog.xml b/app/src/main/res/values-ru-rRU/changelog.xml
index ffecb164ab..56cbe3ec12 100644
--- a/app/src/main/res/values-ru-rRU/changelog.xml
+++ b/app/src/main/res/values-ru-rRU/changelog.xml
@@ -2,12 +2,15 @@
С возвращением!
Мы внесли некоторые изменения с момента последнего входа. Ниже приведены подробности:
- Версия %1$s:
+
+ Версия %1$s:
- - Добавлены новые режимы отображения ленты времени, включая макет Галереи.
- - Улучшенное чтение RSS разделение, перевод и резюме.
- - Настройки внешнего вида для облегчения настройки.
- - Исправления ошибок и улучшения производительности.
+ - Добавлен Deck Mode для ленты в несколько колонок.
+ - Добавлена расширенная настройка вкладок ленты: значки, фильтры, группы и внешний вид для каждой вкладки.
+ - Добавлена поддержка regex для локальных фильтров ключевых слов.
+ - Улучшения производительности и исправления ошибок.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-sr-rSP/changelog.xml b/app/src/main/res/values-sr-rSP/changelog.xml
index 3ea04e700d..b6b3e17a58 100644
--- a/app/src/main/res/values-sr-rSP/changelog.xml
+++ b/app/src/main/res/values-sr-rSP/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ Верзија %1$s:
+
+ - Додат је Deck Mode за временску линију са више колона.
+ - Додато је богатије прилагођавање картица временске линије, укључујући иконе, филтере, групе и изглед по картици.
+ - Додата је regex подршка за локалне филтере кључних речи.
+ - Побољшања перформанси и исправке грешака.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-sv-rSE/changelog.xml b/app/src/main/res/values-sv-rSE/changelog.xml
index b614fcf04c..7852f4919e 100644
--- a/app/src/main/res/values-sv-rSE/changelog.xml
+++ b/app/src/main/res/values-sv-rSE/changelog.xml
@@ -2,12 +2,15 @@
Välkommen tillbaka!
Vi har gjort några ändringar sedan du senast loggade in. Här är detaljerna:
- Version %1$s:
+
+ Version %1$s:
- - Lade till nya tidslinjevisningslägen, inklusive en Galleri layout.
- - Förbättrad RSS-läsning, dela, översättning och sammanfattningar.
- - Omorganiserade Utseendeinställningar för enklare anpassning.
- - Felrättelser och prestandaförbättringar.
+ - Lade till Deck Mode för en tidslinjeupplevelse med flera kolumner.
+ - Lade till rikare anpassning av tidslinjeflikar, inklusive ikoner, filter, grupper och utseende per flik.
+ - Lade till regex-stöd för lokala nyckelordsfilter.
+ - Prestandaförbättringar och buggfixar.
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-tr-rTR/changelog.xml b/app/src/main/res/values-tr-rTR/changelog.xml
index 3ea04e700d..ab24ed1cac 100644
--- a/app/src/main/res/values-tr-rTR/changelog.xml
+++ b/app/src/main/res/values-tr-rTR/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ Sürüm %1$s:
+
+ - Çok sütunlu zaman akışı deneyimi için Deck Mode eklendi.
+ - Simgeler, filtreler, gruplar ve sekmeye özel görünüm dahil olmak üzere zaman akışı sekmeleri için daha zengin özelleştirme eklendi.
+ - Yerel anahtar kelime filtreleri için regex desteği eklendi.
+ - Performans iyileştirmeleri ve hata düzeltmeleri.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-uk-rUA/changelog.xml b/app/src/main/res/values-uk-rUA/changelog.xml
index 82f9391a6a..91abb65ae3 100644
--- a/app/src/main/res/values-uk-rUA/changelog.xml
+++ b/app/src/main/res/values-uk-rUA/changelog.xml
@@ -2,4 +2,15 @@
З поверненням!
Ми внесли деякі зміни після моменту, коли ви увійшли в систему. Детальніше:
+
+ Версія %1$s:
+
+ - Додано Deck Mode для багатоколонкової стрічки.
+ - Додано розширене налаштування вкладок стрічки, зокрема іконки, фільтри, групи та вигляд для кожної вкладки.
+ - Додано підтримку regex для локальних фільтрів ключових слів.
+ - Покращення продуктивності та виправлення помилок.
+
+ ]]>
+
diff --git a/app/src/main/res/values-vi-rVN/changelog.xml b/app/src/main/res/values-vi-rVN/changelog.xml
index 3ea04e700d..d5216c81d2 100644
--- a/app/src/main/res/values-vi-rVN/changelog.xml
+++ b/app/src/main/res/values-vi-rVN/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ Phiên bản %1$s:
+
+ - Đã thêm Deck Mode cho trải nghiệm dòng thời gian nhiều cột.
+ - Đã thêm tùy chỉnh tab dòng thời gian phong phú hơn, bao gồm biểu tượng, bộ lọc, nhóm và giao diện theo từng tab.
+ - Đã thêm hỗ trợ regex cho bộ lọc từ khóa cục bộ.
+ - Cải thiện hiệu năng và sửa lỗi.
+
+ ]]>
+
+
diff --git a/app/src/main/res/values-zh-rCN/changelog.xml b/app/src/main/res/values-zh-rCN/changelog.xml
index d5562a6a22..fbe06f7cda 100644
--- a/app/src/main/res/values-zh-rCN/changelog.xml
+++ b/app/src/main/res/values-zh-rCN/changelog.xml
@@ -2,12 +2,15 @@
欢迎回来!
自您上次登录以来,我们已经做了一些更改。以下是详细信息:
- %1$s版本:
+
+ 版本 %1$s:
- - 添加了新的时间线显示模式, 包括图库布局。
- - 改进RSS阅读, 分享、翻译和摘要。
- - 更容易自定义的重整外观设置。
- - Bug 修复和性能改进。
+ - 新增 Deck Mode,带来多列时间线体验。
+ - 新增更丰富的时间线标签自定义,包括图标、过滤器、分组和单标签外观。
+ - 本地关键词过滤新增正则表达式支持。
+ - 性能改进和问题修复。
- ]]>
+ ]]>
+
diff --git a/app/src/main/res/values-zh-rTW/changelog.xml b/app/src/main/res/values-zh-rTW/changelog.xml
index 3ea04e700d..7dadd57f4a 100644
--- a/app/src/main/res/values-zh-rTW/changelog.xml
+++ b/app/src/main/res/values-zh-rTW/changelog.xml
@@ -1,2 +1,14 @@
-
+
+
+ 版本 %1$s:
+
+ - 新增 Deck Mode,帶來多欄時間軸體驗。
+ - 新增更豐富的時間軸分頁自訂,包括圖示、篩選器、群組和單一分頁外觀。
+ - 本機關鍵字篩選新增正規表示式支援。
+ - 效能改善與錯誤修正。
+
+ ]]>
+
+
diff --git a/app/src/main/res/values/changelog.xml b/app/src/main/res/values/changelog.xml
index e508dd3797..6f2fe581bc 100644
--- a/app/src/main/res/values/changelog.xml
+++ b/app/src/main/res/values/changelog.xml
@@ -6,10 +6,10 @@
Version %1$s:
- - Added new timeline display modes, including a Gallery layout.
- - Improved RSS reading, sharing, translation, and summaries.
- - Reorganized Appearance settings for easier customization.
- - Bug fixes and performance improvements.
+ - Added Deck Mode for a multi-column timeline experience.
+ - Added richer timeline tab customization, including icons, filters, groups, and per-tab appearance.
+ - Added regex support for local keyword filters.
+ - Performance improvements and bug fixes.
]]>
diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts
index b34ef4eb53..e7b06b28a6 100644
--- a/desktopApp/build.gradle.kts
+++ b/desktopApp/build.gradle.kts
@@ -63,7 +63,7 @@ val fdroidProp = Properties().apply {
val desktopVersionName =
System.getenv("BUILD_VERSION")?.takeIf {
// match semantic versioning
- Regex("""\d+\.\d+\.\d+(-\S+)?""").matches(it)
+ Regex("""\d+\.\d+\.\d+""").matches(it)
} ?: fdroidProp.getProperty("versionName") ?: "1.0.0"
val desktopVersionCode =
System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode")?.toIntOrNull() ?: 1
@@ -158,7 +158,7 @@ nucleus.application {
}
?.sorted()
?: emptyList()
-
+
val localizationsXml = (listOf("en") + locales).distinct()
.joinToString("\n") { " $it" }
diff --git a/desktopApp/src/main/composeResources/values-af-rZA/strings.xml b/desktopApp/src/main/composeResources/values-af-rZA/strings.xml
index aa396d8584..87d09c1057 100644
--- a/desktopApp/src/main/composeResources/values-af-rZA/strings.xml
+++ b/desktopApp/src/main/composeResources/values-af-rZA/strings.xml
@@ -187,7 +187,7 @@
KI-tipe
Kies watter KI-agterkant Flare gebruik
KI op toestel
- OpenAI-versoenbare API
+ KI-versoenbare API
API-sleutel
Voer API-sleutel in
Model
diff --git a/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml b/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml
index 2175474e2f..d0668f11a4 100644
--- a/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml
@@ -191,7 +191,7 @@
نوع الذكاء الاصطناعي
اختر محرك الذكاء الاصطناعي الذي يستخدمه Flare
ذكاء اصطناعي على الجهاز
- واجهة برمجة تطبيقات متوافقة مع OpenAI
+ واجهة API متوافقة مع الذكاء الاصطناعي
مفتاح واجهة برمجة التطبيقات (API Key)
أدخل مفتاح واجهة برمجة التطبيقات
النموذج
diff --git a/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml b/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml
index c4d57aca8e..429909928a 100644
--- a/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml
+++ b/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml
@@ -187,7 +187,7 @@
Тип ИИ
Изберете кой ИИ бекенд да използва Flare
ИИ на устройството
- OpenAI-съвместим API
+ API, съвместим с ИИ
API ключ
Въведете API ключ
Модел
diff --git a/desktopApp/src/main/composeResources/values-ca-rES/strings.xml b/desktopApp/src/main/composeResources/values-ca-rES/strings.xml
index 08daf5e203..3f05801084 100644
--- a/desktopApp/src/main/composeResources/values-ca-rES/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ca-rES/strings.xml
@@ -187,7 +187,7 @@
Tipus d\'IA
Tria quin motor d\'IA utilitza Flare
IA al dispositiu
- API compatible amb OpenAI
+ API compatible amb IA
Clau API
Introdueix la clau API
Model
diff --git a/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml b/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml
index 8de5978fa9..e14dfd54a2 100644
--- a/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml
+++ b/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml
@@ -189,7 +189,7 @@
Typ AI
Vyberte, které AI rozhraní Flare používá
AI v zařízení
- API kompatibilní s OpenAI
+ API kompatibilní s AI
API klíč
Zadejte API klíč
Model
diff --git a/desktopApp/src/main/composeResources/values-da-rDK/strings.xml b/desktopApp/src/main/composeResources/values-da-rDK/strings.xml
index aefd571567..91b2f1868e 100644
--- a/desktopApp/src/main/composeResources/values-da-rDK/strings.xml
+++ b/desktopApp/src/main/composeResources/values-da-rDK/strings.xml
@@ -187,7 +187,7 @@
AI-type
Vælg hvilken AI-backend Flare bruger
AI på enheden
- OpenAI-kompatibel API
+ AI-kompatibel API
API-nøgle
Indtast API-nøgle
Model
diff --git a/desktopApp/src/main/composeResources/values-de-rDE/strings.xml b/desktopApp/src/main/composeResources/values-de-rDE/strings.xml
index 8fb9274a1c..d3b7f693a8 100644
--- a/desktopApp/src/main/composeResources/values-de-rDE/strings.xml
+++ b/desktopApp/src/main/composeResources/values-de-rDE/strings.xml
@@ -187,7 +187,7 @@
KI-Typ
Wählen Sie das KI-Backend für Flare
On-Device KI
- OpenAI-kompatible API
+ KI-kompatible API
API-Schlüssel
API-Schlüssel eingeben
Modell
diff --git a/desktopApp/src/main/composeResources/values-el-rGR/strings.xml b/desktopApp/src/main/composeResources/values-el-rGR/strings.xml
index 4780234cc5..c4e4c0f33b 100644
--- a/desktopApp/src/main/composeResources/values-el-rGR/strings.xml
+++ b/desktopApp/src/main/composeResources/values-el-rGR/strings.xml
@@ -187,7 +187,7 @@
Τύπος AI
Επιλέξτε ποιο σύστημα AI χρησιμοποιεί το Flare
AI στη συσκευή
- API συμβατό με OpenAI
+ API συμβατό με AI
Κλειδί API
Εισαγάγετε το κλειδί API
Μοντέλο
diff --git a/desktopApp/src/main/composeResources/values-es-rES/strings.xml b/desktopApp/src/main/composeResources/values-es-rES/strings.xml
index 0c81996500..e5d646d48c 100644
--- a/desktopApp/src/main/composeResources/values-es-rES/strings.xml
+++ b/desktopApp/src/main/composeResources/values-es-rES/strings.xml
@@ -187,7 +187,7 @@
Tipo de IA
Elige qué motor de IA utiliza Flare
IA en el dispositivo
- API compatible con OpenAI
+ API compatible con IA
Clave API
Introduce la clave API
Modelo
diff --git a/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml b/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml
index 466702eb07..ad95650fc6 100644
--- a/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml
+++ b/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml
@@ -187,7 +187,7 @@
Tekoälyn tyyppi
Valitse, mitä tekoälyalustaa Flare käyttää
Laitteen oma tekoäly
- OpenAI-yhteensopiva API
+ AI-yhteensopiva API
API-avain
Anna API-avain
Malli
diff --git a/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml b/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml
index 0dcb5955a9..7cd67c9186 100644
--- a/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml
+++ b/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml
@@ -187,7 +187,7 @@
Type d\'IA
Choisissez quel moteur d\'IA Flare utilise
IA sur l\'appareil
- API compatible OpenAI
+ API compatible IA
Clé API
Entrez la clé API
Modèle
diff --git a/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml b/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml
index 7faf6b516e..e4ae189e01 100644
--- a/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml
+++ b/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml
@@ -187,7 +187,7 @@
MI típusa
Válaszd ki, melyik MI hátteret használja a Flare
Eszközön lévő MI
- OpenAI-kompatibilis API
+ MI-kompatibilis API
API kulcs
Add meg az API kulcsot
Modell
diff --git a/desktopApp/src/main/composeResources/values-it-rIT/strings.xml b/desktopApp/src/main/composeResources/values-it-rIT/strings.xml
index cc9f375b34..36f101caf6 100644
--- a/desktopApp/src/main/composeResources/values-it-rIT/strings.xml
+++ b/desktopApp/src/main/composeResources/values-it-rIT/strings.xml
@@ -187,7 +187,7 @@
Tipo di IA
Scegli quale backend di IA deve utilizzare Flare
IA sul dispositivo
- API compatibile con OpenAI
+ API compatibile con IA
Chiave API
Inserisci la chiave API
Modello
diff --git a/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml b/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml
index fec1ef0470..360d1e3946 100644
--- a/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml
+++ b/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml
@@ -187,7 +187,7 @@
סוג בינה מלאכותית
בחרו באיזה מנוע בינה מלאכותית Flare ישתמש
בינה מלאכותית על המכשיר
- API תואם OpenAI
+ API תואם בינה מלאכותית
מפתח API
הזינו מפתח API
מודל
diff --git a/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml b/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml
index 9f20d5236d..ce6b1ad5d8 100644
--- a/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml
@@ -187,7 +187,7 @@
AIタイプ
Flareで使用するAIバックエンドを選択
デバイス上のAI
- OpenAI互換API
+ AI互換API
APIキー
APIキーを入力
モデル
diff --git a/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml b/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml
index 5ff2ade272..9f77fb4fc8 100644
--- a/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml
@@ -168,7 +168,7 @@
AI 유형
Flare가 사용할 AI 백엔드 선택
온디바이스 AI
- OpenAI 호환 API
+ AI 호환 API
API 키
API 키 입력
모델
diff --git a/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml b/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml
index c204ed1e19..e9928ec3b4 100644
--- a/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml
+++ b/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml
@@ -168,7 +168,7 @@
AI-type
Kies welke AI-backend Flare gebruikt
On-device AI
- OpenAI-compatibele API
+ AI-compatibele API
API-sleutel
Voer API-sleutel in
Model
diff --git a/desktopApp/src/main/composeResources/values-no-rNO/strings.xml b/desktopApp/src/main/composeResources/values-no-rNO/strings.xml
index ef92b95af3..f89127d10f 100644
--- a/desktopApp/src/main/composeResources/values-no-rNO/strings.xml
+++ b/desktopApp/src/main/composeResources/values-no-rNO/strings.xml
@@ -168,7 +168,7 @@
AI-type
Velg hvilken AI-backend Flare skal bruke
AI på enheten
- OpenAI-kompatibel API
+ AI-kompatibel API
API-nøkkel
Skriv inn API-nøkkel
Modell
diff --git a/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml b/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml
index b46fb0cc5b..e852a4bc43 100644
--- a/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml
+++ b/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml
@@ -170,7 +170,7 @@
Typ AI
Wybierz, którego zaplecza AI używa Flare
AI na urządzeniu
- API kompatybilne z OpenAI
+ API zgodne z AI
Klucz API
Wprowadź klucz API
Model
diff --git a/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml b/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml
index 6b810cb462..b022495aaf 100644
--- a/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml
+++ b/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml
@@ -168,7 +168,7 @@
Tipo de IA
Escolha qual backend de IA o Flare usa
IA no dispositivo
- API compatível com OpenAI
+ API compatível com IA
Chave de API
Insira a chave da API
Modelo
diff --git a/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml b/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml
index 53eb0fc07d..c5f97f1597 100644
--- a/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml
+++ b/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml
@@ -168,7 +168,7 @@
Tipo de IA
Escolha qual backend de IA o Flare usa
IA no dispositivo
- API compatível com OpenAI
+ API compatível com IA
Chave de API
Insira a chave da API
Modelo
diff --git a/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml b/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml
index cdf8b30477..9c96883854 100644
--- a/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml
@@ -169,7 +169,7 @@
Tip IA
Alege ce backend de IA folosește Flare
IA pe dispozitiv
- API compatibil cu OpenAI
+ API compatibil cu AI
Cheie API
Introdu cheia API
Model
diff --git a/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml b/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml
index 649fa0d019..37f3de9cbd 100644
--- a/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml
+++ b/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml
@@ -171,7 +171,7 @@
AI Type
Нажмите, чтобы выбрать тип AI бэкэнда
На устройстве ИИ
- AI Compatible API
+ AI-совместимый API
Ключ API
Введите ключ API
Модель
diff --git a/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml b/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml
index 80a3db4dc0..74c66d40f4 100644
--- a/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml
+++ b/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml
@@ -169,7 +169,7 @@
Tip AI
Izaberite koji AI pozadinski sistem Flare koristi
AI na uređaju
- OpenAI-kompatibilan API
+ API компатибилан са ВИ
API ključ
Unesite API ključ
Model
diff --git a/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml b/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml
index 67e2576fcd..57196e0bb7 100644
--- a/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml
+++ b/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml
@@ -168,7 +168,7 @@
AI-typ
Välj vilken AI-backend Flare ska använda
AI på enheten
- OpenAI-kompatibelt API
+ AI-kompatibelt API
API-nyckel
Ange API-nyckel
Modell
diff --git a/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml b/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml
index d605b6209b..7c7229877a 100644
--- a/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml
+++ b/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml
@@ -168,7 +168,7 @@
AI Türü
Flare\'in hangi AI altyapısını kullanacağını seçin
Cihaz üzerindeki AI
- OpenAI uyumlu API
+ YZ uyumlu API
API Anahtarı
API anahtarını girin
Model
diff --git a/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml b/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml
index 2faf1def99..bdbb66d88b 100644
--- a/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml
+++ b/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml
@@ -170,7 +170,7 @@
Тип ШІ
Виберіть, який ШІ-бекенд використовує Flare
ШІ на пристрої
- OpenAI-сумісний API
+ AI-сумісний API
Ключ API
Введіть ключ API
Модель
diff --git a/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml b/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml
index 8a23a8451a..7d620c97b3 100644
--- a/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml
+++ b/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml
@@ -168,7 +168,7 @@
Loại AI
Chọn phần phụ trợ AI mà Flare sử dụng
AI trên thiết bị
- API tương thích với OpenAI
+ API tương thích AI
Khóa API
Nhập khóa API
Mô hình
diff --git a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml
index fe230a0624..39d8dca750 100644
--- a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml
+++ b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml
@@ -167,7 +167,7 @@
AI 类型
选择 Flare 使用的 AI 后端
设备端 AI
- 兼容 OpenAI 的 API
+ AI 兼容 API
API 密钥
输入 API 密钥
模型
diff --git a/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml b/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml
index 45dd23f0bc..cc7a58b8b8 100644
--- a/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml
+++ b/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml
@@ -167,7 +167,7 @@
AI 類型
選擇 Flare 使用的 AI 後端
裝置端 AI
- OpenAI 相容 API
+ AI 相容 API
API 密鑰
輸入 API 密鑰
模型
diff --git a/desktopApp/src/main/composeResources/values/strings.xml b/desktopApp/src/main/composeResources/values/strings.xml
index 5a537c79aa..47654cdbec 100644
--- a/desktopApp/src/main/composeResources/values/strings.xml
+++ b/desktopApp/src/main/composeResources/values/strings.xml
@@ -187,7 +187,7 @@
AI Type
Choose which AI backend Flare uses
On-device AI
- OpenAI-compatible API
+ AI-compatible API
API Key
Enter API key
Model
diff --git a/fdroid.properties b/fdroid.properties
index e3707884ea..abea0d7f26 100644
--- a/fdroid.properties
+++ b/fdroid.properties
@@ -1,2 +1,2 @@
-versionName=1.4.5
-versionCode=1450
+versionName=1.5.0
+versionCode=1500
diff --git a/iosApp/Flare.xcodeproj/project.pbxproj b/iosApp/Flare.xcodeproj/project.pbxproj
index de18cd6204..318bfc2c25 100644
--- a/iosApp/Flare.xcodeproj/project.pbxproj
+++ b/iosApp/Flare.xcodeproj/project.pbxproj
@@ -334,7 +334,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1450;
+ CURRENT_PROJECT_VERSION = 1501;
DEVELOPMENT_TEAM = 7LFDZ96332;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -354,7 +354,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.4.5;
+ MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.dimension.flare;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -374,7 +374,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1450;
+ CURRENT_PROJECT_VERSION = 1501;
DEVELOPMENT_TEAM = 7LFDZ96332;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -394,7 +394,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.4.5;
+ MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.dimension.flare;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/iosApp/flare/Localizable.xcstrings b/iosApp/flare/Localizable.xcstrings
index 778ada160a..63ff51219d 100644
--- a/iosApp/flare/Localizable.xcstrings
+++ b/iosApp/flare/Localizable.xcstrings
@@ -3197,186 +3197,186 @@
}
}
},
- "AI Compatible" : {
+ "AI-compatible API" : {
"localizations" : {
"af" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI-versoenbaar"
+ "value" : "KI-versoenbare API"
}
},
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "متوافق مع الذكاء الاصطناعي"
+ "value" : "واجهة API متوافقة مع الذكاء الاصطناعي"
}
},
"bg" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Съвместим с ИИ"
+ "value" : "API, съвместим с ИИ"
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatible amb IA"
+ "value" : "API compatible amb IA"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kompatibilní s AI"
+ "value" : "API kompatibilní s AI"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI-kompatibel"
+ "value" : "AI-kompatibel API"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "KI-kompatibel"
+ "value" : "KI-kompatible API"
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Συμβατό με ΤΝ"
+ "value" : "API συμβατό με AI"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatible con IA"
+ "value" : "API compatible con IA"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "tekoäly-yhteensopiva"
+ "value" : "AI-yhteensopiva API"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatible IA"
+ "value" : "API compatible IA"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
- "value" : "תואם בינה מלאכותית"
+ "value" : "API תואם בינה מלאכותית"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MI-kompatibilis"
+ "value" : "MI-kompatibilis API"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatibile con IA"
+ "value" : "API compatibile con IA"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI互換"
+ "value" : "AI互換API"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI 호환"
+ "value" : "AI 호환 API"
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI-kompatibel"
+ "value" : "AI-kompatibel API"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI-compatibel"
+ "value" : "AI-compatibele API"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zgodny z AI"
+ "value" : "API zgodne z AI"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatível com IA"
+ "value" : "API compatível com IA"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatível com IA"
+ "value" : "API compatível com IA"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Compatibil cu IA"
+ "value" : "API compatibil cu AI"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Совместимо с ИИ"
+ "value" : "AI-совместимый API"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Компатибилно са ВИ"
+ "value" : "API компатибилан са ВИ"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI-kompatibel"
+ "value" : "AI-kompatibel API"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "YZ Uyumlu"
+ "value" : "YZ uyumlu API"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Сумісний з ШІ"
+ "value" : "AI-сумісний API"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tương thích AI"
+ "value" : "API tương thích AI"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI 兼容"
+ "value" : "AI 兼容 API"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "AI 相容"
+ "value" : "AI 相容 API"
}
}
}
diff --git a/iosApp/flare/UI/Component/CollectionViewTimeline.swift b/iosApp/flare/UI/Component/CollectionViewTimeline.swift
index d0212683fb..42ea5d49f9 100644
--- a/iosApp/flare/UI/Component/CollectionViewTimeline.swift
+++ b/iosApp/flare/UI/Component/CollectionViewTimeline.swift
@@ -207,6 +207,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
private var scrollingState = IsScrollingState()
private var lastAppliedSignature: SnapshotSignature?
private var lastRenderHashMap: [String: Int32] = [:]
+ private var lastLoadedTimelineItemIDs: Set = []
private let autoplayPlayerView = VideoPlayerView()
private var autoplaySelectionTask: Task?
private var autoplayCountdownTask: Task?
@@ -224,6 +225,8 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
// Maps item identifier → index for timeline cells
private var itemIndexMap: [String: Int] = [:]
private var stableTimelineItemIDs: Set = []
+ private var lastKnownTimelineItemIDByIndex: [Int: String] = [:]
+ private var lastKnownTimelineRenderHashByItemID: [String: Int32] = [:]
private struct SnapshotSignature: Equatable, Sendable {
let accessoryIDs: [String]
@@ -239,6 +242,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
let indexMap: [String: Int]
let renderHashMap: [String: Int32]
let stableTimelineItemIDs: Set
+ let loadedTimelineItemIDs: Set
let isRefreshing: Bool
}
@@ -400,6 +404,13 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
"\(itemID):\(renderHash):\(heightCacheWidthKey(for: width))"
}
+ private func cachedStabilizedHeight(for itemID: String, width: CGFloat) -> CGFloat? {
+ guard let renderHash = lastKnownTimelineRenderHashByItemID[itemID] else {
+ return nil
+ }
+ return heightCache[timelineHeightCacheKey(itemID: itemID, renderHash: renderHash, width: width)]
+ }
+
private func measuredCompressedCardHeight(_ card: UIView, width: CGFloat) -> CGFloat {
card.bounds = CGRect(x: 0, y: 0, width: width, height: UIView.layoutFittingCompressedSize.height)
card.setNeedsLayout()
@@ -444,6 +455,15 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
}
}
+ private func pruneStabilizedPagingCache(keepingItemIDs: Set, itemCount: Int) {
+ lastKnownTimelineItemIDByIndex = lastKnownTimelineItemIDByIndex.filter { index, itemID in
+ index >= 0 && index < itemCount && keepingItemIDs.contains(itemID)
+ }
+ lastKnownTimelineRenderHashByItemID = lastKnownTimelineRenderHashByItemID.filter { itemID, _ in
+ keepingItemIDs.contains(itemID)
+ }
+ }
+
private func applyMeasuredHeightCorrection(
itemID: String,
renderHash: Int32,
@@ -631,7 +651,12 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
} else {
cell.cachedPreferredHeight = nil
cell.onPreferredHeightChanged = nil
- cell.setHostedView(nil)
+ cell.configurePlaceholder(
+ index: index,
+ totalCount: totalCount,
+ appearance: appearance,
+ isMultipleColumn: columnCount > 1
+ )
}
}
@@ -915,6 +940,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
var newIndexMap: [String: Int] = [:]
var newRenderHashMap: [String: Int32] = [:]
var newStableTimelineItemIDs = Set()
+ var newLoadedTimelineItemIDs = Set()
let accessoryIDs = accessoryItems.map { "\(Self.accessoryPrefix)\($0.id)" }
var itemIDs: [String] = []
var footerIDs: [String] = []
@@ -933,25 +959,55 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
case .success(let success):
let itemCount = Int(success.itemCount)
+ var loadedIDsByIndex: [Int: String] = [:]
+ var loadedRenderHashByItemID: [String: Int32] = [:]
+ var loadedTimelineItemIDs = Set()
+
+ for i in 0..()
items.reserveCapacity(itemCount)
for i in 0..
+ ) -> [String] {
+ itemIDs.filter {
+ itemNeedsReconfigure(
+ $0,
+ newRenderHashMap: newRenderHashMap,
+ newLoadedItemIDs: newLoadedItemIDs
+ )
+ }
+ }
+
+ private func itemNeedsReconfigure(
+ _ itemID: String,
+ newRenderHashMap: [String: Int32],
+ newLoadedItemIDs: Set
+ ) -> Bool {
+ lastRenderHashMap[itemID] != newRenderHashMap[itemID] ||
+ lastLoadedTimelineItemIDs.contains(itemID) != newLoadedItemIDs.contains(itemID)
}
private func restorePendingScrollAnchorIfNeeded() {
@@ -1478,6 +1573,11 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView
return CGSize(width: width, height: height)
}
+ if itemID.hasPrefix(Self.timelinePrefix),
+ let cachedHeight = cachedStabilizedHeight(for: itemID, width: width) {
+ return CGSize(width: width, height: cachedHeight)
+ }
+
return CGSize(width: width, height: 200)
}
@@ -1568,6 +1668,8 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell {
private var hostedBottomConstraint: NSLayoutConstraint?
private var timelineViewStorage: TimelineUIView?
private var timelineCardStorage: AdaptiveTimelineCardUIView?
+ private var placeholderViewStorage: TimelinePlaceholderUIView?
+ private var placeholderCardStorage: AdaptiveTimelineCardUIView?
// Rebuild-skip signature. When the incoming data + appearance + detail-key are
// identical to the previous configure we short-circuit the expensive
@@ -1604,7 +1706,10 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell {
}
func autoplayCandidates(prefix: String) -> [TimelineVideoAutoplayCandidate] {
- timelineViewStorage?.autoplayCandidates(prefix: prefix) ?? []
+ guard hostedView === timelineCardStorage else {
+ return []
+ }
+ return timelineViewStorage?.autoplayCandidates(prefix: prefix) ?? []
}
func performDeferredPoolCleanup() {
@@ -1683,6 +1788,19 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell {
setHostedView(timelineCard, usesWaterfallLayout: isMultipleColumn)
}
+ func configurePlaceholder(
+ index: Int,
+ totalCount: Int,
+ appearance: TimelineUIKitAppearance,
+ isMultipleColumn: Bool
+ ) {
+ let placeholderCard = resolvedPlaceholderCard()
+ placeholderCard.isPlainTimelineDisplayMode = appearance.isPlainTimelineDisplayMode
+ placeholderCard.isMultipleColumn = isMultipleColumn
+ placeholderCard.configure(index: index, totalCount: totalCount)
+ setHostedView(placeholderCard, usesWaterfallLayout: isMultipleColumn)
+ }
+
override func layoutSubviews() {
super.layoutSubviews()
hostedView?.frame = contentView.bounds
@@ -1818,6 +1936,25 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell {
timelineCardStorage = card
return card
}
+
+ private func resolvedPlaceholderView() -> TimelinePlaceholderUIView {
+ if let placeholderViewStorage {
+ return placeholderViewStorage
+ }
+ let view = TimelinePlaceholderUIView()
+ placeholderViewStorage = view
+ return view
+ }
+
+ private func resolvedPlaceholderCard() -> AdaptiveTimelineCardUIView {
+ if let placeholderCardStorage {
+ return placeholderCardStorage
+ }
+ let card = AdaptiveTimelineCardUIView()
+ card.setContent(UIView.padding(resolvedPlaceholderView(), insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)))
+ placeholderCardStorage = card
+ return card
+ }
}
private final class TimelinePlaceholderCollectionViewCell: UICollectionViewCell {
diff --git a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift b/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift
index e121e9caa6..b9e726ff93 100644
--- a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift
+++ b/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift
@@ -57,8 +57,12 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
private var currentData: PagingState?
private var currentSuccess: PagingStateSuccess?
private var itemIndexMap: [String: Int] = [:]
+ private var stableGalleryItemIDs: Set = []
+ private var lastKnownItemIDByIndex: [Int: String] = [:]
+ private var lastKnownRenderHashByItemID: [String: Int32] = [:]
private var lastAppliedSignature: SnapshotSignature?
private var lastRenderHashMap: [String: Int32] = [:]
+ private var lastLoadedItemIDs: Set = []
private var lastProcessedDataRef: AnyObject?
private var lastProcessedUpdateSignature: UpdateSignature?
@@ -84,17 +88,39 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
private var heightCache: [String: CGFloat] = [:]
private var heightCacheKeysByItemID: [String: Set] = [:]
private var isUserRefreshing = false
+ private var pendingScrollAnchor: ScrollAnchor?
+ private var isRestoringScrollAnchor = false
+ private var scrollingState = IsScrollingState()
private struct SnapshotSignature: Equatable {
let itemIDs: [String]
let footerIDs: [String]
}
+ private struct ItemSnapshotState {
+ let itemIDs: [String]
+ let indexMap: [String: Int]
+ let renderHashMap: [String: Int32]
+ let stableItemIDs: Set
+ let loadedItemIDs: Set
+ }
+
+ private struct ScrollAnchor {
+ let itemID: String
+ let distanceFromViewportTop: CGFloat
+ }
+
private enum UpdateSignature: Equatable {
case loading
case error
case empty
- case success(itemCount: Int, isRefreshing: Bool, footerIDs: [String])
+ case success(
+ itemIDs: [String],
+ renderHashMap: [String: Int32],
+ loadedItemIDs: Set,
+ isRefreshing: Bool,
+ footerIDs: [String]
+ )
}
override func viewDidLoad() {
@@ -173,13 +199,16 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
// MARK: - Cell configuration
private func configureCell(_ cell: GalleryTimelineCollectionViewCell, itemID: String) {
- if itemID.hasPrefix(Self.itemPrefix),
- let index = itemIndexMap[itemID],
- let success = currentSuccess,
- index >= 0,
- index < Int(success.itemCount),
- let item = success.peek(index: Int32(index)) {
- cell.configureTile(item: item, appearance: appearance, openURL: openURL)
+ if itemID.hasPrefix(Self.itemPrefix) {
+ if let index = itemIndexMap[itemID],
+ let success = currentSuccess,
+ index >= 0,
+ index < Int(success.itemCount),
+ let item = success.peek(index: Int32(index)) {
+ cell.configureTile(item: item, appearance: appearance, openURL: openURL)
+ } else {
+ cell.configurePlaceholder()
+ }
} else if itemID.hasPrefix(Self.placeholderPrefix) {
cell.configurePlaceholder()
} else if itemID == Self.emptyID {
@@ -279,18 +308,202 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
case .empty:
return .empty
case .success(let success):
+ let itemState = makeItemSnapshotState(for: success, updatesLastKnownItems: false)
return .success(
- itemCount: Int(success.itemCount),
+ itemIDs: itemState.itemIDs,
+ renderHashMap: itemState.renderHashMap,
+ loadedItemIDs: itemState.loadedItemIDs,
isRefreshing: success.isRefreshing,
footerIDs: footerItemIDs(for: success)
)
}
}
+ private func makeItemSnapshotState(
+ for success: PagingStateSuccess,
+ updatesLastKnownItems: Bool
+ ) -> ItemSnapshotState {
+ let itemCount = Int(success.itemCount)
+ var loadedIDsByIndex: [Int: String] = [:]
+ var loadedRenderHashByItemID: [String: Int32] = [:]
+ var loadedItemIDs = Set()
+
+ for i in 0..()
+ var usedItemIDs = Set()
+ itemIDs.reserveCapacity(itemCount)
+
+ for i in 0.. CGFloat {
+ let minY = -collectionView.adjustedContentInset.top
+ let maxY = max(
+ minY,
+ collectionView.contentSize.height - collectionView.bounds.height + collectionView.adjustedContentInset.bottom
+ )
+ return min(max(offsetY, minY), maxY)
+ }
+
+ private func isStableGalleryItemID(_ itemID: String) -> Bool {
+ itemID.hasPrefix(Self.itemPrefix) && stableGalleryItemIDs.contains(itemID)
+ }
+
+ private func pruneStabilizedPagingCache(keepingItemIDs: Set, itemCount: Int) {
+ lastKnownItemIDByIndex = lastKnownItemIDByIndex.filter { index, itemID in
+ index >= 0 && index < itemCount && keepingItemIDs.contains(itemID)
+ }
+ lastKnownRenderHashByItemID = lastKnownRenderHashByItemID.filter { itemID, _ in
+ keepingItemIDs.contains(itemID)
+ }
+ }
+
+ private func captureScrollAnchor() -> ScrollAnchor? {
+ guard isViewLoaded,
+ currentSuccess != nil,
+ allowsScrollAnchorRestoration,
+ collectionView.bounds.height > 1 else {
+ return nil
+ }
+
+ let viewportTop = effectiveViewportTop
+ let viewportBottom = collectionView.contentOffset.y + collectionView.bounds.height - collectionView.adjustedContentInset.bottom
+ return collectionView.indexPathsForVisibleItems
+ .compactMap { indexPath -> (itemID: String, frame: CGRect)? in
+ guard let itemID = dataSource.itemIdentifier(for: indexPath),
+ isStableGalleryItemID(itemID) else {
+ return nil
+ }
+ let frame = collectionView.layoutAttributesForItem(at: indexPath)?.frame
+ ?? collectionView.cellForItem(at: indexPath)?.frame
+ ?? .null
+ guard !frame.isNull,
+ frame.maxY > viewportTop,
+ frame.minY < viewportBottom else {
+ return nil
+ }
+ return (itemID, frame)
+ }
+ .min { lhs, rhs in
+ if abs(lhs.frame.minY - rhs.frame.minY) > 0.5 {
+ return lhs.frame.minY < rhs.frame.minY
+ }
+ return lhs.frame.minX < rhs.frame.minX
+ }
+ .map {
+ ScrollAnchor(
+ itemID: $0.itemID,
+ distanceFromViewportTop: $0.frame.minY - viewportTop
+ )
+ }
+ }
+
+ @discardableResult
+ private func restoreScrollAnchorIfNeeded(_ anchor: ScrollAnchor?) -> Bool {
+ guard let anchor,
+ isViewLoaded,
+ allowsScrollAnchorRestoration,
+ let indexPath = dataSource.indexPath(for: anchor.itemID) else {
+ return false
+ }
+
+ view.layoutIfNeeded()
+ collectionView.layoutIfNeeded()
+
+ guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else {
+ return false
+ }
+
+ let targetOffsetY = attributes.frame.minY - anchor.distanceFromViewportTop - collectionView.adjustedContentInset.top
+ let targetOffset = CGPoint(x: collectionView.contentOffset.x, y: clampedContentOffsetY(targetOffsetY))
+ if abs(collectionView.contentOffset.y - targetOffset.y) > 0.5 {
+ isRestoringScrollAnchor = true
+ collectionView.setContentOffset(targetOffset, animated: false)
+ isRestoringScrollAnchor = false
+ }
+ return true
+ }
+
+ private func restorePendingScrollAnchorIfNeeded() {
+ guard !isRestoringScrollAnchor,
+ allowsScrollAnchorRestoration,
+ let pendingScrollAnchor else {
+ return
+ }
+ if restoreScrollAnchorIfNeeded(pendingScrollAnchor) {
+ collectionView.layer.removeAllAnimations()
+ }
+ }
+
private func applySnapshot(data: PagingState) {
var snapshot = NSDiffableDataSourceSnapshot()
var newIndexMap: [String: Int] = [:]
var newRenderHashMap: [String: Int32] = [:]
+ var newStableItemIDs = Set()
+ var newLoadedItemIDs = Set()
var itemIDs: [String] = []
var footerIDs: [String] = []
@@ -310,19 +523,12 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
snapshot.appendItems(itemIDs, toSection: Self.sectionMain)
case .success(let success):
snapshot.appendSections([Self.sectionMain])
- let itemCount = Int(success.itemCount)
- var items: [String] = []
- items.reserveCapacity(itemCount)
- for i in 0..
+ ) -> [String] {
+ itemIDs.filter {
+ itemNeedsReconfigure(
+ $0,
+ newRenderHashMap: newRenderHashMap,
+ newLoadedItemIDs: newLoadedItemIDs
+ )
+ }
+ }
+
+ private func itemNeedsReconfigure(
+ _ itemID: String,
+ newRenderHashMap: [String: Int32],
+ newLoadedItemIDs: Set
+ ) -> Bool {
+ lastRenderHashMap[itemID] != newRenderHashMap[itemID] ||
+ lastLoadedItemIDs.contains(itemID) != newLoadedItemIDs.contains(itemID)
}
private func reconfigureItems(_ itemIDs: [String]) {
@@ -460,6 +728,11 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
return CGSize(width: width, height: estimatedHeight(for: item, itemID: itemID, width: width))
}
+ if itemID.hasPrefix(Self.itemPrefix),
+ let cachedHeight = cachedStabilizedHeight(for: itemID, width: width) {
+ return CGSize(width: width, height: cachedHeight)
+ }
+
return CGSize(width: width, height: 200)
}
@@ -495,10 +768,14 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
}
private func heightCacheKey(for item: UiTimelineV2, itemID: String, width: CGFloat) -> String {
+ heightCacheKey(itemID: itemID, renderHash: item.renderHash, width: width)
+ }
+
+ private func heightCacheKey(itemID: String, renderHash: Int32, width: CGFloat) -> String {
let scaledWidth = Int((width * UIScreen.main.scale).rounded(.toNearestOrAwayFromZero))
return [
itemID,
- String(item.renderHash),
+ String(renderHash),
String(scaledWidth),
appearance.showMedia ? "media" : "text",
appearance.avatarShapeID,
@@ -506,6 +783,13 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
].joined(separator: "|")
}
+ private func cachedStabilizedHeight(for itemID: String, width: CGFloat) -> CGFloat? {
+ guard let renderHash = lastKnownRenderHashByItemID[itemID] else {
+ return nil
+ }
+ return heightCache[heightCacheKey(itemID: itemID, renderHash: renderHash, width: width)]
+ }
+
private func setCachedHeight(_ height: CGFloat, for key: String, itemID: String) {
heightCache[key] = height
heightCacheKeysByItemID[itemID, default: []].insert(key)
@@ -549,6 +833,31 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat
index < Int(success.itemCount) else { return }
_ = success.get(index: Int32(index))
}
+
+ // MARK: - UIScrollViewDelegate
+
+ func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
+ scrollingState.isScrolling = true
+ pendingScrollAnchor = nil
+ }
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ restorePendingScrollAnchorIfNeeded()
+ }
+
+ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
+ if !decelerate {
+ scrollingState.isScrolling = false
+ }
+ }
+
+ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
+ scrollingState.isScrolling = false
+ }
+
+ func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
+ scrollingState.isScrolling = false
+ }
}
// MARK: - UIKit tile views
diff --git a/iosApp/flare/UI/Component/Status/RichTextUIView.swift b/iosApp/flare/UI/Component/Status/RichTextUIView.swift
index e2acfae401..ad1fd9e4e2 100644
--- a/iosApp/flare/UI/Component/Status/RichTextUIView.swift
+++ b/iosApp/flare/UI/Component/Status/RichTextUIView.swift
@@ -127,6 +127,8 @@ final class RichTextUIView: UIView, TimelineHeightProviding {
traitRegistration = registerForTraitChanges([
UITraitUserInterfaceStyle.self,
UITraitPreferredContentSizeCategory.self,
+ UITraitLegibilityWeight.self,
+ UITraitAccessibilityContrast.self,
]) { (view: RichTextUIView, _) in
view.update(force: true)
}
@@ -650,10 +652,41 @@ final class RichTextUIView: UIView, TimelineHeightProviding {
private func preferredFont(forTextStyle textStyle: UIFont.TextStyle) -> UIFont {
UIFont.preferredFont(
forTextStyle: textStyle,
- compatibleWith: UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory)
+ compatibleWith: effectiveFontTraitCollection()
)
}
+ private func effectiveFontTraitCollection() -> UITraitCollection {
+ UITraitCollection(traitsFrom: [
+ traitCollection,
+ UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory),
+ ])
+ }
+
+ private func systemFont(size: CGFloat, weight: UIFont.Weight) -> UIFont {
+ let resolvedWeight: UIFont.Weight = traitCollection.legibilityWeight == .bold && weight == .regular
+ ? .semibold
+ : weight
+ return UIFont.systemFont(ofSize: size, weight: resolvedWeight)
+ }
+
+ private func monospacedFont(size: CGFloat, weight: UIFont.Weight) -> UIFont {
+ let resolvedWeight: UIFont.Weight = traitCollection.legibilityWeight == .bold && weight == .regular
+ ? .semibold
+ : weight
+ return UIFont.monospacedSystemFont(ofSize: size, weight: resolvedWeight)
+ }
+
+ private func addingItalicIfNeeded(_ font: UIFont, enabled: Bool) -> UIFont {
+ guard enabled,
+ let descriptor = font.fontDescriptor.withSymbolicTraits(
+ font.fontDescriptor.symbolicTraits.union(.traitItalic)
+ ) else {
+ return font
+ }
+ return UIFont(descriptor: descriptor, size: font.pointSize)
+ }
+
private func color(for style: RenderTextStyle, block: RenderBlockStyle) -> UIColor {
if block.isBlockQuote || style.small {
return baseTextColor.withAlphaComponent(0.7)
@@ -690,28 +723,25 @@ final class RichTextUIView: UIView, TimelineHeightProviding {
let baseSize = preferredFont(forTextStyle: textStyle).pointSize
let baseFont: UIFont
if style.code || style.monospace {
- baseFont = UIFont.monospacedSystemFont(
- ofSize: style.small ? baseSize * 0.8 : baseSize,
- weight: style.bold ? .bold : .regular
+ return addingItalicIfNeeded(
+ monospacedFont(
+ size: style.small ? baseSize * 0.8 : baseSize,
+ weight: style.bold ? .bold : .regular
+ ),
+ enabled: style.italic || block.isBlockQuote || block.isFigCaption
)
} else if style.small {
- baseFont = UIFont.systemFont(ofSize: baseSize * 0.8)
+ baseFont = systemFont(size: baseSize * 0.8, weight: style.bold ? .bold : .regular)
+ } else if style.bold {
+ baseFont = systemFont(size: baseSize, weight: .bold)
} else {
baseFont = preferredFont(forTextStyle: textStyle)
}
- var traits: UIFontDescriptor.SymbolicTraits = []
- if style.bold {
- traits.insert(.traitBold)
- }
- if style.italic || block.isBlockQuote || block.isFigCaption {
- traits.insert(.traitItalic)
- }
- guard !traits.isEmpty,
- let descriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) else {
- return baseFont
- }
- return UIFont(descriptor: descriptor, size: baseFont.pointSize)
+ return addingItalicIfNeeded(
+ baseFont,
+ enabled: style.italic || block.isBlockQuote || block.isFigCaption
+ )
}
private func font(for descriptor: PlatformTextStyleDescriptor) -> UIFont {
@@ -735,28 +765,25 @@ final class RichTextUIView: UIView, TimelineHeightProviding {
let baseSize = preferredFont(forTextStyle: baseTextStyle).pointSize
let baseFont: UIFont
if descriptor.code || descriptor.monospace {
- baseFont = UIFont.monospacedSystemFont(
- ofSize: descriptor.small ? baseSize * 0.8 : baseSize,
- weight: descriptor.bold ? .bold : .regular
+ return addingItalicIfNeeded(
+ monospacedFont(
+ size: descriptor.small ? baseSize * 0.8 : baseSize,
+ weight: descriptor.bold ? .bold : .regular
+ ),
+ enabled: descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption
)
} else if descriptor.small {
- baseFont = UIFont.systemFont(ofSize: baseSize * 0.8)
+ baseFont = systemFont(size: baseSize * 0.8, weight: descriptor.bold ? .bold : .regular)
+ } else if descriptor.bold {
+ baseFont = systemFont(size: baseSize, weight: .bold)
} else {
baseFont = preferredFont(forTextStyle: baseTextStyle)
}
- var traits: UIFontDescriptor.SymbolicTraits = []
- if descriptor.bold {
- traits.insert(.traitBold)
- }
- if descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption {
- traits.insert(.traitItalic)
- }
- guard !traits.isEmpty,
- let fontDescriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) else {
- return baseFont
- }
- return UIFont(descriptor: fontDescriptor, size: baseFont.pointSize)
+ return addingItalicIfNeeded(
+ baseFont,
+ enabled: descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption
+ )
}
private func linkColor() -> UIColor {
diff --git a/iosApp/flare/UI/Route/Route.swift b/iosApp/flare/UI/Route/Route.swift
index f95a74542a..444b6adebe 100644
--- a/iosApp/flare/UI/Route/Route.swift
+++ b/iosApp/flare/UI/Route/Route.swift
@@ -10,8 +10,6 @@ enum Route: Hashable, Identifiable {
switch (lhs, rhs) {
case (.timeline(let lhs), .timeline(let rhs)):
return lhs.id == rhs.id
- case (.tabGroupConfig(let lhs), .tabGroupConfig(let rhs)):
- return lhs?.id == rhs?.id
default:
return lhs.hashValue == rhs.hashValue
}
@@ -22,9 +20,6 @@ enum Route: Hashable, Identifiable {
case .timeline(let item):
hasher.combine("timeline")
hasher.combine(item.id)
- case .tabGroupConfig(let item):
- hasher.combine("tabGroupConfig")
- hasher.combine(item?.id)
default:
hasher.combine(String(describing: self))
}
@@ -99,8 +94,6 @@ enum Route: Hashable, Identifiable {
TranslationConfigScreen()
case .tabSettings:
TabSettingsScreen()
- case .tabGroupConfig(let item):
- GroupConfigScreen(item: item)
case .rssDetail(let url, let descriptionHtml, let title):
RssDetailScreen(url: url, descriptionHtml: descriptionHtml, descriptionTitle: title)
case .twitterArticle(let accountType, let tweetId, let articleId):
@@ -219,7 +212,6 @@ enum Route: Hashable, Identifiable {
case allAntennas(AccountType)
case allChannels(AccountType)
case allDirectMessages(AccountType)
- case tabGroupConfig(GroupTimelineTabItemV2?)
case rssManagement
case draftBox
case secondaryMenu
diff --git a/iosApp/flare/UI/Screen/AiConfigScreen.swift b/iosApp/flare/UI/Screen/AiConfigScreen.swift
index c1fb7829f8..14898df780 100644
--- a/iosApp/flare/UI/Screen/AiConfigScreen.swift
+++ b/iosApp/flare/UI/Screen/AiConfigScreen.swift
@@ -262,7 +262,7 @@ struct AiConfigScreen: View {
case .onDevice:
return "On Device"
case .openAi:
- return "AI Compatible"
+ return "AI-compatible API"
}
}
diff --git a/iosApp/flare/UI/Screen/GroupConfigScreen.swift b/iosApp/flare/UI/Screen/GroupConfigScreen.swift
index f76c5348cc..d7177eddab 100644
--- a/iosApp/flare/UI/Screen/GroupConfigScreen.swift
+++ b/iosApp/flare/UI/Screen/GroupConfigScreen.swift
@@ -6,6 +6,7 @@ struct GroupConfigScreen: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.timelineAppearance) private var baseTimelineAppearance
let item: GroupTimelineTabItemV2?
+ let onConfirm: (GroupTimelineTabItemV2?) -> Void
@State private var name: String
@State private var icon: IconType
@State private var enabled: Bool
@@ -18,8 +19,12 @@ struct GroupConfigScreen: View {
@State private var editItem: TimelineTabItemV2? = nil
@StateObject private var presenter: KotlinPresenter
- init(item: GroupTimelineTabItemV2? = nil) {
+ init(
+ item: GroupTimelineTabItemV2? = nil,
+ onConfirm: @escaping (GroupTimelineTabItemV2?) -> Void
+ ) {
self.item = item
+ self.onConfirm = onConfirm
_name = State(initialValue: item?.title.text ?? "")
_icon = State(initialValue: item?.icon ?? IconType.Material(icon: .rss))
_enabled = State(initialValue: item?.enabled ?? true)
@@ -167,16 +172,19 @@ struct GroupConfigScreen: View {
}
ToolbarItem(placement: .confirmationAction) {
Button {
- presenter.state.commit(
- initialItem: item,
- name: name,
- icon: icon,
- appearancePatch: appearancePatch,
- enabled: enabled,
- tabs: tabs,
- mergePolicy: mergePolicy,
- filterConfig: filterConfig,
- defaultGroupName: NSLocalizedString("tab_settings_group_default_name", comment: "")
+ let defaultGroupName = NSLocalizedString("tab_settings_group_default_name", comment: "")
+ onConfirm(
+ presenter.state.buildGroupItem(
+ initialItem: item,
+ name: name,
+ icon: icon,
+ appearancePatch: appearancePatch,
+ enabled: enabled,
+ tabs: tabs,
+ mergePolicy: mergePolicy,
+ filterConfig: filterConfig,
+ defaultGroupName: defaultGroupName
+ )
)
dismiss()
} label: {
diff --git a/iosApp/flare/UI/Screen/TabSettingsScreen.swift b/iosApp/flare/UI/Screen/TabSettingsScreen.swift
index a5ac5a51c4..cf0af3f4bc 100644
--- a/iosApp/flare/UI/Screen/TabSettingsScreen.swift
+++ b/iosApp/flare/UI/Screen/TabSettingsScreen.swift
@@ -165,13 +165,17 @@ struct TabSettingsScreen: View {
})) {
NavigationStack {
if let item = editGroup {
- GroupConfigScreen(item: item)
+ GroupConfigScreen(item: item) { updated in
+ upsertGroup(initialItem: item, updatedItem: updated)
+ }
}
}
}
.sheet(isPresented: $showCreateGroup) {
NavigationStack {
- GroupConfigScreen(item: nil)
+ GroupConfigScreen(item: nil) { updated in
+ upsertGroup(initialItem: nil, updatedItem: updated)
+ }
}
}
.sheet(isPresented: Binding(get: {
@@ -225,6 +229,23 @@ struct TabSettingsScreen: View {
.first(where: { isSystemHomeMixedTimeline($0) })?
.mergePolicy ?? .timePerPage
}
+
+ private func upsertGroup(
+ initialItem: GroupTimelineTabItemV2?,
+ updatedItem: GroupTimelineTabItemV2?
+ ) {
+ let targetIndex = initialItem
+ .flatMap { item in tabItems.firstIndex(where: { $0.id == item.id }) }
+ ?? tabItems.count
+
+ tabItems.removeAll { item in
+ item.id == initialItem?.id || item.id == updatedItem?.id
+ }
+
+ if let updatedItem {
+ tabItems.insert(updatedItem, at: min(targetIndex, tabItems.count))
+ }
+ }
}
struct EditTabSheet: View {
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt
index 9d376b88f9..01244ec8b9 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt
@@ -103,7 +103,7 @@ private fun List.renderThread(accountKey: MicroBlogKe
}
}.forEach { (depth, post) ->
while (stack.lastOrNull()?.first?.let { it >= depth } == true) {
- stack.removeLast()
+ stack.removeAt(stack.lastIndex)
}
val parents =
when {
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt
index c3687eb7d8..66e0823fbe 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt
@@ -175,13 +175,13 @@ internal object TimelinePagingMapper {
val currentKey = current.statusKey
resolvedPosts[currentKey]?.let {
resolvingKeys -= currentKey
- stack.removeLast()
+ stack.removeAt(stack.lastIndex)
continue
}
if (!frame.expanded) {
if (!resolvingKeys.add(currentKey)) {
- stack.removeLast()
+ stack.removeAt(stack.lastIndex)
continue
}
frame.expanded = true
@@ -205,7 +205,7 @@ internal object TimelinePagingMapper {
)
resolvedPosts[currentKey] = resolved
resolvingKeys -= currentKey
- stack.removeLast()
+ stack.removeAt(stack.lastIndex)
}
return resolvedPosts[post.statusKey] ?: post
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt
index 9d1e25ef2c..230bf0a0cb 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt
@@ -259,7 +259,12 @@ public sealed class UiTimelineV2 {
append(" ")
}
}
+ internalRepost?.content?.raw?.let { repostContent ->
+ append(repostContent)
+ append(" ")
+ }
}
+
val onClicked: ClickContext.() -> Unit by lazy {
clickEvent.onClicked
}
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt
index 1a288bc47f..3f7ade291b 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt
@@ -43,6 +43,38 @@ public class GroupConfigPresenter :
return object : State {
override val availableIcons: ImmutableList = availableIcons
+ override fun buildGroupItem(
+ initialItem: GroupTimelineTabItemV2?,
+ name: String,
+ icon: IconType,
+ appearancePatch: AppearancePatch?,
+ enabled: Boolean,
+ tabs: List,
+ mergePolicy: TimelineMergePolicy,
+ filterConfig: TimelineFilterConfig,
+ defaultGroupName: String,
+ ): GroupTimelineTabItemV2? {
+ val childSlots =
+ tabs
+ .distinctBy { it.id }
+ .map { timelineResolver.toSlot(it) }
+ if (childSlots.isEmpty()) {
+ return null
+ }
+ return timelineResolver.toTabItem(
+ buildGroupSlot(
+ name = name,
+ icon = icon,
+ appearancePatch = appearancePatch,
+ enabled = enabled,
+ mergePolicy = mergePolicy,
+ filterConfig = filterConfig,
+ defaultGroupName = defaultGroupName,
+ childSlots = childSlots,
+ ),
+ ) as GroupTimelineTabItemV2
+ }
+
override fun commit(
initialItem: GroupTimelineTabItemV2?,
name: String,
@@ -78,6 +110,18 @@ public class GroupConfigPresenter :
public interface State {
public val availableIcons: ImmutableList
+ public fun buildGroupItem(
+ initialItem: GroupTimelineTabItemV2?,
+ name: String,
+ icon: IconType,
+ appearancePatch: AppearancePatch?,
+ enabled: Boolean,
+ tabs: List,
+ mergePolicy: TimelineMergePolicy = initialItem?.mergePolicy ?: TimelineMergePolicy.TimePerPage,
+ filterConfig: TimelineFilterConfig = initialItem?.filterConfig ?: TimelineFilterConfig(),
+ defaultGroupName: String,
+ ): GroupTimelineTabItemV2?
+
public fun commit(
initialItem: GroupTimelineTabItemV2?,
name: String,
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt
index 00d51731b5..1a3ff4699e 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt
@@ -242,7 +242,13 @@ internal data class TimelinePostTraits(
internal fun UiTimelineV2.Post.traits(): TimelinePostTraits {
val kinds =
buildSet {
- if (replyToHandle != null) {
+ val currentUserKey = user?.key
+ val hasParentFromOtherUser =
+ currentUserKey != null &&
+ parents.any { parent ->
+ parent.user?.key?.let { it != currentUserKey } == true
+ }
+ if (hasParentFromOtherUser) {
add(TimelinePostKind.Reply)
}
if (internalRepost != null) {
diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt
index 8ff4099f69..5547193779 100644
--- a/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt
+++ b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt
@@ -5,6 +5,7 @@ import dev.dimension.flare.data.model.tab.TimelineFilterConfig
import dev.dimension.flare.data.model.tab.TimelinePostContent
import dev.dimension.flare.data.model.tab.TimelinePostKind
import dev.dimension.flare.data.repository.KeywordFilterPattern
+import dev.dimension.flare.model.MicroBlogKey
import dev.dimension.flare.ui.humanizer.PlatformFormatter
import dev.dimension.flare.ui.model.UiMedia
import dev.dimension.flare.ui.model.createSampleStatus
@@ -40,9 +41,12 @@ class TimelinePresenterFilterTest {
@Test
fun postTraitsCaptureKindsAndContents() {
- val base = createSampleStatus(createSampleUser())
+ val currentUser = createSampleUser()
+ val parentUser = currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost"))
+ val base = createSampleStatus(currentUser)
val repost = base.copy(statusKey = base.statusKey.copy(id = "repost"))
val quote = base.copy(statusKey = base.statusKey.copy(id = "quote"))
+ val parent = createSampleStatus(parentUser)
val filtered =
base.copy(
content = "".toUiPlainText(),
@@ -64,7 +68,7 @@ class TimelinePresenterFilterTest {
width = 100f,
),
),
- replyToHandle = "@flare",
+ parents = persistentListOf(parent),
quote = persistentListOf(quote),
internalRepost = repost,
)
@@ -83,7 +87,9 @@ class TimelinePresenterFilterTest {
@Test
fun matchesTimelineFilterExcludesConfiguredKindsAndContents() {
- val originalTextOnly = createSampleStatus(createSampleUser())
+ val currentUser = createSampleUser()
+ val parentUser = currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost"))
+ val originalTextOnly = createSampleStatus(currentUser)
val replyWithImage =
originalTextOnly.copy(
statusKey = originalTextOnly.statusKey.copy(id = "reply"),
@@ -98,7 +104,7 @@ class TimelinePresenterFilterTest {
sensitive = false,
),
),
- replyToHandle = "@flare",
+ parents = persistentListOf(createSampleStatus(parentUser)),
)
val filter =
TimelineFilterConfig(
@@ -110,6 +116,30 @@ class TimelinePresenterFilterTest {
assertFalse(replyWithImage.matchesTimelineFilter(filter))
}
+ @Test
+ fun postTraitsOnlyMarksReplyWhenParentUserDiffersFromCurrentUser() {
+ val currentUser = createSampleUser()
+ val original = createSampleStatus(currentUser)
+ val selfThread =
+ original.copy(
+ statusKey = original.statusKey.copy(id = "self-thread"),
+ parents = persistentListOf(createSampleStatus(currentUser)),
+ )
+ val replyToOtherUser =
+ original.copy(
+ statusKey = original.statusKey.copy(id = "reply-to-other-user"),
+ parents =
+ persistentListOf(
+ createSampleStatus(
+ currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost")),
+ ),
+ ),
+ )
+
+ assertFalse(TimelinePostKind.Reply in selfThread.traits().kinds)
+ assertTrue(TimelinePostKind.Reply in replyToOtherUser.traits().kinds)
+ }
+
@Test
fun matchesKeywordFiltersUsesRegexForRegexRules() {
val status =
@@ -141,6 +171,66 @@ class TimelinePresenterFilterTest {
)
}
+ @Test
+ fun matchesKeywordFiltersChecksInternalRepostContent() {
+ val base = createSampleStatus(createSampleUser())
+ val original =
+ base.copy(
+ statusKey = base.statusKey.copy(id = "original"),
+ content = "Visible original #blocked".toUiPlainText(),
+ )
+ val repostWrapper =
+ base.copy(
+ statusKey = original.statusKey.copy(id = "repost"),
+ content = "".toUiPlainText(),
+ internalRepost = original,
+ )
+
+ assertFalse(
+ repostWrapper.matchesKeywordFilters(
+ listOf(
+ KeywordFilterPattern(
+ keyword = "#blocked",
+ isRegex = false,
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun matchesKeywordFiltersDoesNotSearchNestedInternalRepostContent() {
+ val base = createSampleStatus(createSampleUser())
+ val nestedOriginal =
+ base.copy(
+ statusKey = base.statusKey.copy(id = "nested-original"),
+ content = "Nested original #deepblocked".toUiPlainText(),
+ )
+ val directRepost =
+ base.copy(
+ statusKey = base.statusKey.copy(id = "direct-repost"),
+ content = "".toUiPlainText(),
+ internalRepost = nestedOriginal,
+ )
+ val repostWrapper =
+ base.copy(
+ statusKey = base.statusKey.copy(id = "repost-wrapper"),
+ content = "".toUiPlainText(),
+ internalRepost = directRepost,
+ )
+
+ assertTrue(
+ repostWrapper.matchesKeywordFilters(
+ listOf(
+ KeywordFilterPattern(
+ keyword = "#deepblocked",
+ isRegex = false,
+ ),
+ ),
+ ),
+ )
+ }
+
@Test
fun matchesKeywordFiltersIgnoresInvalidRegexRules() {
val status =