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: + + ]]> + + 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: + + ]]> + 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: + + ]]> + + 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: + + ]]> + 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ó: + + ]]> + + 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: + + ]]> + + 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: + + ]]> + 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: + + ]]> + + 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: + + ]]> + 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: + + ]]> + 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: + + ]]> + + 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: + + ]]> + + 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: + + ]]> + 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: + + ]]> + + 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: + + ]]> + + 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: ]]> 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 =