diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2d4ce..f4c83a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,94 @@ Tento soubor vychází z [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), verzování z [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] - 2021/07/12 +### Nové +- První hybridní verze pro Android a iOS +- Tržiště #217 #201 +- Oprava anket a přidání hlasování #149 + +### Změněno +- Skrýt floating button když nemám práva čtení #218 +- Pod YT videem se zobrazují dva!! #180 +- Nahradit nekonečno číslem #198 +- Zredukovat zbytečný padding kolem příspěvku #215 +- Možnost posílat HTML a zobrazování invalidního HTML #229 +- LaunchIcons #178 +- Upgrade některých knihoven #207 + +### Opraveno +- Reload neoznačuje nepřečtené -> přečtené #186 +- Rozbitý syntax highlight #148 +- Přestaly fungovat spoilery #200 +- Chyba zobrazení příspěvku pokud v diskuzi žádné nejsou #212 +- Chyba zobrazení příspěvku #165 +- Chyba zobrazení příspěvku - jeden příspěvek zcela brání vyrenderovat diskuzi #214 +- Prazdne auditko #122 +- Zaporne hodnoty u neprectenych prispevku #194 +- Prokliky na jiné diskuze z odpovědí #206 +- Chybová hláška při vstupu do soukromé diskuze #208 +- Nezobrazují se badges #174 +- Pouze prispevky tohoto ID #211 +- Dvakrát zobrazený příspěvek #209 +- Cyklení zobrazení příspěvků #139 +- Nezobrazení diskuze s velmi dlouhým příspěvkem #224 +- Revize interních odkazů #226 +- Ztráta contextu při otevření notifikace #227 +- Chybý rebuild widget tree při pull 2 refresh #228 +- Zlobí zobrazování náhledu v kompakt modu #169 + +## [0.6.1] - 2021/04/23 + +### Nové +- Nefungují prokliky notifikací #154 + +### Opraveno +- Nenačítají se avataři #155 +- Pošta nezobrazuje odeslaný příspěvek #170 +- Odeslaná pošta má prohozené odesiltele #171 +- Obnovení home resetuje notifikace pošty #161 +- Vykopnutí z diskuze při novém příspěvku, který se mi notifikuje #141 +- Opravit odkaz na podmínky užívání #157 + +### Změněno +- Po probuzení bez připojení k internetu se zobrazí chyba místo přednačtených příspěvků #160 +- Po uspání a probuzení aplikace může diskuze odskrolovat jinam #159 +- Přehodit api z alpha na www #156 +- Vrátit zpět BACKERS.md #158 + +## [0.6.0] - 2021/04/02 + +### Nové +- Napojení na nové API +- Možnost nahrání více obrázků najednou + +### Opraveno +- Při kliku na zvoneček se vždy zobrazí chyba. #142 +- Nefungují obrázky v poště #147 + +## [0.5.0] - 2021/02/02 + +### Nové +- Push notifikace #14 +- Upozornění / notifikační centrum #6 +- Syntax highlight - tag #8 +- Zobrazování anket #38 +- Možnost ukládat obrázky #127 +- Nastavení > výchozí obrazovka > Ukládat poslední stav #91 +- Kontextová nabídka uživatele (filtrovat v diskuzi, poslat zprávu, ...) #113 +- Vylepšená práce s nahráváním fotek (fix performance, přidána kvalita, rozlišení, ...) #69 +- UI feedback palečkování, uložení do připomínek... #118 + +### Opraveno +- Reload zanořené diskuze #97 +- Přeskakující kursor #124 +- Po nahrání obrázku se i při blikajícím kurzoru schová klávesnice #119 +- Další drobné bugy + +### Změněno +- Nová kudlanka #112 +- Drobné UI fixy #116 #120 + ## [0.4.1] - 2020/11/09 ### Nové diff --git a/README.md b/README.md index ece3040..463f25a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Zde je přehled funkcí pro lepší představu. | Nástěnka / záhlaví klubu | ❌ | ✅ | | Ukládání do sledovaných | ❌ | ✅ | | Psaní příspěvků | ✅ | ✅ | -| Mazání příspěvků | ❌ | ✅ | +| Mazání příspěvků | ✅ | ✅ | | Kompaktní mód příspěvku | ✅ | ❌ | | Nahrávání obrázků | ✅ | ✅ | | Galerie více obrázků | ✅ | ❌ | @@ -44,10 +44,10 @@ Zde je přehled funkcí pro lepší představu. | Uložení do upomínek | ✅ | ✅ | | Videa v příspěvku | ✅ | ❌ | | Spoilery | ✅ | ❌ | -| Zobrazování anket | ✅ | ❌ | +| Zobrazování anket | ✅ | ✅ | | Zobrazování zdrojáků | ✅ | ❌ | | Zobrazování videí | ✅ | ❌ | -| Dark mode | ❌ | ✅ | +| Dark mode | ✅ | ✅ | | Pošta | ✅ | ✅ | | Hledání | ❌ | ✅ | | Tržiště | ❌ | ✅ | @@ -66,13 +66,14 @@ Pokud chcete vývoj Fyxu, který je nabízen zdarma, finančně podpořit, pak m - [Patreon](http://patreon.com/fyxapp) - [Bankovní spojení](https://www.nyx.cz/index.php?l=topic;l2=2;id=24237;n=6162) +- Bitcoin: bc1q6m0ptsg3z4u6296m9kqfl4adylt9kxkafw94ul ### Vývoj Jakákoli pomoc - od každého - vítána! Nejrychleji se zapojíte přes [klub na Nyxu](https://www.nyx.cz/index.php?l=topic;id=24237;n=23dd), který se o vývojem nového klienta zabývá. Také si můžete projít [Issues](https://github.com/lucien144/fyx/issues) případně [Projects](https://github.com/lucien144/fyx/projects) a poslat pull request. -Build produkce lze spustit zavoláním skpriput `$ ./ios/build.sh`, který zároveň zvýší build verzi o +1. +Build produkce lze spustit zavoláním skpriput `$ ./build.sh`, který zároveň zvýší build verzi o +1. Tento repozitář používá [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). Připravujte veškerou práci ve `feature` větvích nebo forku, pak pošlete PR do `develop` větve. PR jsou mergovány pouze adminy (a jako `squash commits`). @@ -81,48 +82,13 @@ Pokud jste našli chybu, pak ji nahlaste ideálně přes aplikaci. Pokud to nejd ## FAQ -- **Proč nelze k příspěvku nahrát víc obrázků najednou?** +- **Chybí mi možnost odskoku na nejbližší nepřečtený příspěvěk. Bude?** - To bohužel nepodporuje Nyx. - -- **Nikde nevidím možnost smazat příspěvek.** - - Zatím není podporováno, ale bude - viz. [roadmapa](https://github.com/lucien144/fyx/projects/2). + Ano, bude. - **Proč je tento repozitář v češtině?** Vzhledem k tomu, že [klub na Nyxu](https://www.nyx.cz/index.php?l=topic;id=24237;n=23dd) věnující se novému klientovi vznikl v češtině, rozhodl jsem se (Lucien) vést tento repozitář také v češtině. Naproti tomu kód a komentáře v kódu jsou v angličtině, protože to je pro mě přiřozené. Dále by měly [Issues](https://github.com/lucien144/fyx/issues) sloužit jako centrální hub pro vedení veškerých chyb a připomínek, což se mi zdá opět lepší vést v češtině pro běžné uživatele. Nicméně, změně na kompletně anglické repo se po diskuzi nebráním... -## Náhledy - - - - -
-

Výpis historie -

-

-

Výpis diskuzí -

-

-

Detail diskuze (v kompaktním módu) -

-

-

Galerie (náhled obrázku v příspěvku) -

-

-

Anketa -

-

-

Zvýraznění syntaxe -

-

-

Spoilery -

-

-

Psaní odpovědi -

-

-

Nastavení -

-

+## Náhled +![https://i.imgur.com/fY47YVH](https://i.imgur.com/fY47YVH.gif) \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 9b2d2dc..aa32256 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,8 +33,8 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 29 - + compileSdkVersion 31 + android.ndkVersion "20.1.5948944" sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -46,7 +46,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "net.lucien144.fyx" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8fc9a65..6681204 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ package="net.lucien144.fyx"> + ${bump}$NC \n" + version=$bump +fi + + +if [ $ios == true ]; then + # shellcheck disable=SC2059 + printf "$GREEN Building iOS: ${version}$NC\n" + flutter clean + flutter build ios -t lib/main_production.dart + open ios/Runner.xcworkspace + /usr/bin/osascript -e "display notification \"iOS built.\"" +fi + +if [ $android == true ]; then + # shellcheck disable=SC2059 + printf "$GREEN Building Android: ${version}$NC\n" + + if [ $ios == false ]; then + flutter clean + fi + + flutter build appbundle -t lib/main_production.dart + open build/app/outputs/bundle/release/ + /usr/bin/osascript -e "display notification \"Android built.\"" +fi \ No newline at end of file diff --git a/ios/Flutter/Flutter.podspec b/ios/Flutter/Flutter.podspec deleted file mode 100644 index 5ca3041..0000000 --- a/ios/Flutter/Flutter.podspec +++ /dev/null @@ -1,18 +0,0 @@ -# -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# - -Pod::Spec.new do |s| - s.name = 'Flutter' - s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.description = <<-DESC -Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. - DESC - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' - s.vendored_frameworks = 'Flutter.framework' -end diff --git a/ios/Podfile b/ios/Podfile index 4c0c07d..d379e82 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -42,6 +42,7 @@ end #end post_install do |installer| installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8bb32ad..6844218 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,69 +1,68 @@ PODS: - device_info (0.0.1): - Flutter - - Firebase/Analytics (6.26.0): + - Firebase/Analytics (8.9.0): - Firebase/Core - - Firebase/Core (6.26.0): + - Firebase/Core (8.9.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 6.6.0) - - Firebase/CoreOnly (6.26.0): - - FirebaseCore (= 6.7.2) - - Firebase/Messaging (6.26.0): + - FirebaseAnalytics (~> 8.9.0) + - Firebase/CoreOnly (8.9.0): + - FirebaseCore (= 8.9.0) + - Firebase/Messaging (8.9.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 4.4.1) - - firebase_analytics (6.0.0): - - Firebase/Analytics (~> 6.26.0) - - Firebase/CoreOnly (~> 6.26.0) + - FirebaseMessaging (~> 8.9.0) + - firebase_analytics (8.3.4): + - Firebase/Analytics (= 8.9.0) - firebase_core - Flutter - - firebase_core (0.5.0): - - Firebase/CoreOnly (~> 6.26.0) + - firebase_core (1.10.0): + - Firebase/CoreOnly (= 8.9.0) - Flutter - - firebase_messaging (7.0.2): - - Firebase/CoreOnly (~> 6.26.0) - - Firebase/Messaging (~> 6.26.0) + - firebase_messaging (11.2.4): + - Firebase/Messaging (= 8.9.0) - firebase_core - Flutter - - FirebaseAnalytics (6.6.0): - - FirebaseCore (~> 6.7) - - FirebaseInstallations (~> 1.3) - - GoogleAppMeasurement (= 6.6.0) - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (~> 1.30905.0) - - FirebaseAnalyticsInterop (1.5.0) - - FirebaseCore (6.7.2): - - FirebaseCoreDiagnostics (~> 1.3) - - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Logger (~> 6.5) - - FirebaseCoreDiagnostics (1.5.0): - - GoogleDataTransport (~> 7.0) - - GoogleUtilities/Environment (~> 6.7) - - GoogleUtilities/Logger (~> 6.7) - - nanopb (~> 1.30905.0) - - FirebaseCoreDiagnosticsInterop (1.2.0) - - FirebaseInstallations (1.3.0): - - FirebaseCore (~> 6.6) - - GoogleUtilities/Environment (~> 6.6) - - GoogleUtilities/UserDefaults (~> 6.6) - - PromisesObjC (~> 1.2) - - FirebaseInstanceID (4.3.4): - - FirebaseCore (~> 6.6) - - FirebaseInstallations (~> 1.0) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/UserDefaults (~> 6.5) - - FirebaseMessaging (4.4.1): - - FirebaseAnalyticsInterop (~> 1.5) - - FirebaseCore (~> 6.6) - - FirebaseInstanceID (~> 4.3) - - GoogleUtilities/AppDelegateSwizzler (~> 6.5) - - GoogleUtilities/Environment (~> 6.5) - - GoogleUtilities/Reachability (~> 6.5) - - GoogleUtilities/UserDefaults (~> 6.5) - - Protobuf (>= 3.9.2, ~> 3.9) + - FirebaseAnalytics (8.9.1): + - FirebaseAnalytics/AdIdSupport (= 8.9.1) + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/AdIdSupport (8.9.1): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleAppMeasurement (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" + - nanopb (~> 2.30908.0) + - FirebaseCore (8.9.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Logger (~> 7.6) + - FirebaseCoreDiagnostics (8.10.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Logger (~> 7.6) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.10.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/UserDefaults (~> 7.6) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseMessaging (8.9.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Reachability (~> 7.6) + - GoogleUtilities/UserDefaults (~> 7.6) + - nanopb (~> 2.30908.0) - Flutter (1.0.0) - fluttertoast (0.0.2): - Flutter @@ -71,57 +70,81 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - GoogleAppMeasurement (6.6.0): - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (~> 1.30905.0) - - GoogleDataTransport (7.2.0): - - nanopb (~> 1.30905.0) - - GoogleUtilities/AppDelegateSwizzler (6.7.2): + - GoogleAppMeasurement (8.9.1): + - GoogleAppMeasurement/AdIdSupport (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (8.9.1): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (8.9.1): + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.2): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.6.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (6.7.2): - - PromisesObjC (~> 1.2) - - GoogleUtilities/Logger (6.7.2): + - GoogleUtilities/Environment (7.6.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.6.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.7.2): + - GoogleUtilities/MethodSwizzler (7.6.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (6.7.2): + - GoogleUtilities/Network (7.6.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.7.2)" - - GoogleUtilities/Reachability (6.7.2): + - "GoogleUtilities/NSData+zlib (7.6.0)" + - GoogleUtilities/Reachability (7.6.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.7.2): + - GoogleUtilities/UserDefaults (7.6.0): - GoogleUtilities/Logger - image_gallery_saver (1.5.0): - Flutter - image_picker (0.0.1): - Flutter - - nanopb (1.30905.0): - - nanopb/decode (= 1.30905.0) - - nanopb/encode (= 1.30905.0) - - nanopb/decode (1.30905.0) - - nanopb/encode (1.30905.0) + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) - package_info (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider (0.0.1): - Flutter - - PromisesObjC (1.2.10) - - Protobuf (3.13.0) - - screen (0.0.1): + - "permission_handler (5.1.0+2)": + - Flutter + - PromisesObjC (2.0.0) + - Sentry (7.5.4): + - Sentry/Core (= 7.5.4) + - Sentry/Core (7.5.4) + - sentry_flutter (0.0.1): - Flutter + - FlutterMacOS + - Sentry (~> 7.5.1) - share (0.0.1): - Flutter - shared_preferences (0.0.1): - Flutter - - sqflite (0.0.1): + - sqflite (0.0.2): - Flutter - - FMDB (~> 2.7.2) + - FMDB (>= 2.7.5) - Toast (4.0.0) - url_launcher (0.0.1): - Flutter @@ -142,8 +165,10 @@ DEPENDENCIES: - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - - screen (from `.symlinks/plugins/screen/ios`) + - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) @@ -153,16 +178,12 @@ DEPENDENCIES: - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) SPEC REPOS: - https://cdn.cocoapods.org/: - - FirebaseCore - https://github.com/cocoapods/specs.git: + trunk: - Firebase - FirebaseAnalytics - - FirebaseAnalyticsInterop + - FirebaseCore - FirebaseCoreDiagnostics - - FirebaseCoreDiagnosticsInterop - FirebaseInstallations - - FirebaseInstanceID - FirebaseMessaging - FMDB - GoogleAppMeasurement @@ -170,7 +191,7 @@ SPEC REPOS: - GoogleUtilities - nanopb - PromisesObjC - - Protobuf + - Sentry - Toast EXTERNAL SOURCES: @@ -192,10 +213,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker/ios" package_info: :path: ".symlinks/plugins/package_info/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" - screen: - :path: ".symlinks/plugins/screen/ios" + permission_handler: + :path: ".symlinks/plugins/permission_handler/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" share: :path: ".symlinks/plugins/share/ios" shared_preferences: @@ -213,41 +238,40 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 - Firebase: 7cf5f9c67f03cb3b606d1d6535286e1080e57eb6 - firebase_analytics: 3ad65aa7be059752b2c1a6392bfafe030b4ab9fa - firebase_core: 3134fe79d257d430f163b558caf52a10a87efe8a - firebase_messaging: 2844c37f9ce87c0904b38fe435223161b1a71528 - FirebaseAnalytics: 96634d356482d4f3af8fe459a0ebf19a99c71b75 - FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae - FirebaseCore: f42e5e5f382cdcf6b617ed737bf6c871a6947b17 - FirebaseCoreDiagnostics: 7535fe695737f8c5b350584292a70b7f8ff0357b - FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 - FirebaseInstallations: 6f5f680e65dc374397a483c32d1799ba822a395b - FirebaseInstanceID: cef67c4967c7cecb56ea65d8acbb4834825c587b - FirebaseMessaging: 29543feb343b09546ab3aa04d008ee8595b43c44 - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Firebase: 13d8d96499e2635428d5bf0ec675df21f95d9a95 + firebase_analytics: 7a7528bb2abf4ca22cff7a57bbf909dcab73e13d + firebase_core: f770e033e790657b3505f04be4cb24c482912f11 + firebase_messaging: dff5cd08781ee1de988565a83c977e435405cd7e + FirebaseAnalytics: 4ab446ce08a3fe52e8a4303dd997cf26276bf968 + FirebaseCore: 599ee609343eaf4941bd188f85e3aa077ffe325b + FirebaseCoreDiagnostics: 56fb7216d87e0e6ec2feddefa9d8a392fe8b2c18 + FirebaseInstallations: 830327b45345ffc859eaa9c17bcd5ae893fd5425 + FirebaseMessaging: 82c4a48638f53f7b184f3cc9f6cd2cbe533ab316 + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleAppMeasurement: 67458367830514fb20fd9e233496f1eef9d90185 - GoogleDataTransport: 672fb0ce96fe7f7f31d43672fca62ad2c9c86f7b - GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 + GoogleAppMeasurement: 837649ad3987936c232f6717c5680216f6243d24 + GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 + GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 - image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 - nanopb: c43f40fadfe79e8b8db116583945847910cbabc9 + image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d - PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151 - Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 - screen: abd91ca7bf3426e1cc3646d27e9b2358d6bf07b0 + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e + path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de + permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + Sentry: 5c5dd4005f3b7b9765d5a8871232cddbd0d888b7 + sentry_flutter: 4cd99764f9fe01c9415790d1f3fb1c7fd3a5cbe9 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef - video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e - wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4 - webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 + video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + webview_flutter: 3603125dfd3bcbc9d8d418c3f80aeecf331c068b -PODFILE CHECKSUM: c25da410c4cc6fc1a1a1dcf45ba80e27b97bb9fc +PODFILE CHECKSUM: 54b56b37ef04a402b4e20983b360f8d98c1d2f96 -COCOAPODS: 1.7.5 +COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f083a27..1403839 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -256,10 +256,14 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", - "${PODS_ROOT}/../Flutter/Flutter.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreDiagnostics/FirebaseCoreDiagnostics.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", + "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", - "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework", "${BUILT_PRODUCTS_DIR}/Toast/Toast.framework", "${BUILT_PRODUCTS_DIR}/device_info/device_info.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", @@ -267,8 +271,9 @@ "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", + "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", - "${BUILT_PRODUCTS_DIR}/screen/screen.framework", + "${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework", "${BUILT_PRODUCTS_DIR}/share/share.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", @@ -280,10 +285,14 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreDiagnostics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", @@ -291,8 +300,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/screen.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", @@ -628,4 +638,4 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; -} \ No newline at end of file +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index c34310b..cb19635 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,21 +1,83 @@ { "images" : [ { + "filename" : "image@1x.png", "idiom" : "universal", - "scale" : "1x", - "filename" : "image@1x.png" + "scale" : "1x" }, { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], "idiom" : "universal", - "scale" : "2x", - "filename" : "image@2x.png" + "scale" : "1x" }, { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], "idiom" : "universal", - "scale" : "3x", - "filename" : "image@3x.png" + "scale" : "1x" + }, + { + "filename" : "image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "3x" } ], - "author" : "Iconizer", - "version" : "3.0.0" -} \ No newline at end of file + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index 89a52f2..29dd51d 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -23,7 +24,7 @@ - + @@ -37,5 +38,8 @@ + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f80f8e0..3425ddb 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -50,8 +50,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIUserInterfaceStyle - Light UIViewControllerBasedStatusBarAppearance diff --git a/ios/build.sh b/ios/build.sh deleted file mode 100755 index ea9a04b..0000000 --- a/ios/build.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e - -# Get to the project dir -cd "$(dirname "$0")" -cd .. - -# Find and increment the version number. -perl -i -pe 's/^(version:\s+\d+\.\d+\.\d+\+)(\d+)$/$1.($2+1)/e' pubspec.yaml - -version=`grep 'version: ' pubspec.yaml | sed 's/version: //'` -echo "Building version: $version" -flutter clean -flutter build ios -t lib/main_production.dart -open ios/Runner.xcworkspace \ No newline at end of file diff --git a/lib/FyxApp.dart b/lib/FyxApp.dart index f849be1..8bbb93b 100644 --- a/lib/FyxApp.dart +++ b/lib/FyxApp.dart @@ -1,17 +1,20 @@ import 'dart:async'; import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:fyx/SkinnedApp.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/controllers/SettingsProvider.dart'; import 'package:fyx/libs/DeviceInfo.dart'; import 'package:fyx/model/Credentials.dart'; import 'package:fyx/model/MainRepository.dart'; +import 'package:fyx/model/enums/ThemeEnum.dart'; import 'package:fyx/model/provider/NotificationsModel.dart'; +import 'package:fyx/model/provider/ThemeModel.dart'; import 'package:fyx/pages/DiscussionPage.dart'; import 'package:fyx/pages/GalleryPage.dart'; import 'package:fyx/pages/HomePage.dart'; @@ -22,15 +25,18 @@ import 'package:fyx/pages/NoticesPage.dart'; import 'package:fyx/pages/SettingsPage.dart'; import 'package:fyx/pages/TutorialPage.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/skins/FyxSkin.dart'; import 'package:package_info/package_info.dart'; import 'package:provider/provider.dart'; import 'package:sentry/sentry.dart'; + import 'controllers/NotificationsService.dart'; enum Environment { dev, staging, production } class FyxApp extends StatefulWidget { - static Environment _env; + static Environment _env = Environment.dev; static set env(val) => FyxApp._env = val; @@ -44,9 +50,9 @@ class FyxApp extends StatefulWidget { static FirebaseAnalytics analytics = FirebaseAnalytics(); - static RouteObserver _routeObserver; + static RouteObserver _routeObserver = RouteObserver(); - static NotificationService _notificationsService; + static late NotificationService _notificationsService; static GlobalKey navigatorKey = new GlobalKey(); @@ -55,18 +61,17 @@ class FyxApp extends StatefulWidget { } static get routeObserver { - if (_routeObserver == null) { - _routeObserver = RouteObserver(); - } return _routeObserver; } - static init(SentryClient sentry) async { + static init() async { + await Firebase.initializeApp(); + // This must be initialized after WidgetsFlutterBinding.ensureInitialized FlutterError.onError = (details, {bool forceReport = false}) { try { - sentry.captureException( - exception: details.exception, + Sentry.captureException( + details.exception, stackTrace: details.stack, ); } catch (e) { @@ -77,7 +82,7 @@ class FyxApp extends StatefulWidget { } }; - if (FyxApp.isDev) { + if (FyxApp.isProduction) { ErrorWidget.builder = (FlutterErrorDetails details) { String stack = '${DateTime.now()}: ${details.exceptionAsString()}'; return T.somethingsWrongButton(stack); @@ -89,44 +94,38 @@ class FyxApp extends StatefulWidget { // TODO: Move to build using FutureBuilder. var results = await Future.wait([ApiController().getCredentials(), PackageInfo.fromPlatform(), DeviceInfo.init(), SettingsProvider().init()]); - MainRepository().credentials = results[0]; - MainRepository().packageInfo = results[1]; - MainRepository().deviceInfo = results[2]; - MainRepository().settings = results[3]; - MainRepository().sentry = sentry; + MainRepository().credentials = results[0] == null ? null : results[0] as Credentials; + MainRepository().packageInfo = results[1] as PackageInfo; + MainRepository().deviceInfo = results[2] as DeviceInfo; + MainRepository().settings = results[3] as SettingsProvider; _notificationsService = NotificationService( onToken: (fcmToken) => ApiController().registerFcmToken(fcmToken), // TODO: Do not register if the token is already saved. onTokenRefresh: (fcmToken) => ApiController().refreshFcmToken(fcmToken), ); - _notificationsService.onNewMail = () => - FyxApp.navigatorKey.currentState.pushReplacementNamed('/home', - arguments: HomePageArguments(HomePage.PAGE_MAIL)); + _notificationsService.configure(); + _notificationsService.onNewMail = + () => FyxApp.navigatorKey.currentState!.pushReplacementNamed('/home', arguments: HomePageArguments(HomePage.PAGE_MAIL)); _notificationsService.onNewPost = ({discussionId, postId}) { - if (discussionId > 0 && postId > 0) { - FyxApp.navigatorKey.currentState.pushNamed('/discussion', arguments: DiscussionPageArguments(discussionId, postId: postId + 1)); + if (discussionId! > 0 && postId! > 0) { + FyxApp.navigatorKey.currentState!.pushNamed('/discussion', arguments: DiscussionPageArguments(discussionId, postId: postId + 1)); } else if (discussionId > 0) { - FyxApp.navigatorKey.currentState.pushNamed('/discussion', arguments: DiscussionPageArguments(discussionId)); + FyxApp.navigatorKey.currentState!.pushNamed('/discussion', arguments: DiscussionPageArguments(discussionId)); } else { - FyxApp.navigatorKey.currentState.pushReplacementNamed('/home', arguments: HomePageArguments(HomePage.PAGE_BOOKMARK)); + FyxApp.navigatorKey.currentState!.pushReplacementNamed('/home', arguments: HomePageArguments(HomePage.PAGE_BOOKMARK)); } }; _notificationsService.onError = (error) { print(error); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); }; MainRepository().notifications = _notificationsService; AnalyticsProvider.provider = analytics; } - @override - _FyxAppState createState() => _FyxAppState(); -} - -class _FyxAppState extends State { - Route routes(RouteSettings settings) { + static Route routes(RouteSettings settings) { switch (settings.name) { case '/token': print('[Router] Token'); @@ -146,7 +145,11 @@ class _FyxAppState extends State { case '/gallery': print('[Router] Gallery'); return PageRouteBuilder( - transitionDuration: const Duration(milliseconds: 0), opaque: false, pageBuilder: (_, __, ___) => GalleryPage(), settings: settings, fullscreenDialog: true); + transitionDuration: const Duration(milliseconds: 0), + opaque: false, + pageBuilder: (_, __, ___) => GalleryPage(), + settings: settings, + fullscreenDialog: true); case '/settings': print('[Router] Settings'); return CupertinoPageRoute(builder: (_) => SettingsPage(), settings: settings); @@ -162,6 +165,20 @@ class _FyxAppState extends State { } } + @override + _FyxAppState createState() => _FyxAppState(); +} + +class _FyxAppState extends State with WidgetsBindingObserver { + Brightness? _platformBrightness; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance?.addObserver(this); + _platformBrightness ??= WidgetsBinding.instance?.window.platformBrightness; + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -175,27 +192,19 @@ class _FyxAppState extends State { child: MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => NotificationsModel()), + ChangeNotifierProvider(create: (context) => ThemeModel(MainRepository().settings.theme)), ], - child: CupertinoApp( - title: 'Fyx', - theme: CupertinoThemeData( - primaryColor: T.COLOR_PRIMARY, - brightness: Brightness.light, - textTheme: CupertinoTextThemeData(primaryColor: Colors.white, textStyle: TextStyle(color: T.COLOR_BLACK, fontSize: 16))), - home: MainRepository().credentials is Credentials && MainRepository().credentials.isValid ? HomePage() : LoginPage(), - debugShowCheckedModeBanner: FyxApp.isDev, - onUnknownRoute: (RouteSettings settings) => CupertinoPageRoute(builder: (_) => DiscussionPage(), settings: settings), - onGenerateRoute: routes, - navigatorKey: FyxApp.navigatorKey, - navigatorObservers: [ - FyxApp.routeObserver, - FirebaseAnalyticsObserver( - analytics: FyxApp.analytics, - onError: (error) async => await MainRepository().sentry.captureException( - exception: error, - )) - ], - ), + builder: (ctx, widget) => Directionality( + textDirection: TextDirection.ltr, + child: Skin( + skin: FyxSkin.create(), + brightness: (() { + if (ctx.watch().theme == ThemeEnum.system && _platformBrightness != null) { + return _platformBrightness!; + } + return ctx.watch().theme == ThemeEnum.light ? Brightness.light : Brightness.dark; + })(), + child: SkinnedApp())), ), ); } @@ -204,5 +213,12 @@ class _FyxAppState extends State { void dispose() { super.dispose(); FyxApp._notificationsService.dispose(); + WidgetsBinding.instance?.removeObserver(this); + } + + @override + void didChangePlatformBrightness() { + setState(() => _platformBrightness = WidgetsBinding.instance?.window.platformBrightness); + super.didChangePlatformBrightness(); // make sure you call this } } diff --git a/lib/SkinnedApp.dart b/lib/SkinnedApp.dart new file mode 100644 index 0000000..b1d3605 --- /dev/null +++ b/lib/SkinnedApp.dart @@ -0,0 +1,35 @@ +import 'package:firebase_analytics/observer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:fyx/FyxApp.dart'; +import 'package:fyx/model/MainRepository.dart'; +import 'package:fyx/pages/DiscussionPage.dart'; +import 'package:fyx/pages/HomePage.dart'; +import 'package:fyx/pages/LoginPage.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class SkinnedApp extends StatelessWidget { + const SkinnedApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CupertinoApp( + title: 'Fyx', + theme: Skin.of(context).theme.data, + home: MainRepository().credentials != null && MainRepository().credentials!.isValid ? HomePage() : LoginPage(), + debugShowCheckedModeBanner: FyxApp.isDev, + onUnknownRoute: (RouteSettings settings) => CupertinoPageRoute(builder: (_) => DiscussionPage(), settings: settings), + onGenerateRoute: FyxApp.routes, + navigatorKey: FyxApp.navigatorKey, + navigatorObservers: [ + FyxApp.routeObserver, + FirebaseAnalyticsObserver( + analytics: FyxApp.analytics, + onError: (error) async => await Sentry.captureException( + error, + )) + ], + ); + } +} diff --git a/lib/components/CircleAvatar.dart b/lib/components/Avatar.dart similarity index 50% rename from lib/components/CircleAvatar.dart rename to lib/components/Avatar.dart index 3331c2d..56185fd 100644 --- a/lib/components/CircleAvatar.dart +++ b/lib/components/Avatar.dart @@ -2,37 +2,32 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; -class CircleAvatar extends StatelessWidget { +class Avatar extends StatelessWidget { final String url; - final bool _isHighlighted; - final double _size; + final bool isHighlighted; + final double size; - CircleAvatar(this.url, {bool isHighlighted = false, double size = 40.0}) - : _isHighlighted = isHighlighted, - _size = size; + Avatar(this.url, {this.size = 32.0, this.isHighlighted = false}); @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; return Container( - padding: EdgeInsets.all(1), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(22), color: _isHighlighted ? T.COLOR_PRIMARY : Colors.black), - child: Container( padding: EdgeInsets.all(2), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(22), color: _isHighlighted ? T.COLOR_PRIMARY : Colors.white), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(3), color: isHighlighted ? colors.highlight : Colors.transparent), child: ClipRRect( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(3), child: CachedNetworkImage( imageUrl: url, fit: BoxFit.cover, placeholder: (context, url) => CupertinoActivityIndicator(), errorWidget: (context, url, error) => Icon(Icons.error), - width: _size, - height: _size, + width: size, + height: size * (50 / 40), ), - ), - ), - ); + )); } } diff --git a/lib/components/ContentBoxLayout.dart b/lib/components/ContentBoxLayout.dart index 61b1178..4add767 100644 --- a/lib/components/ContentBoxLayout.dart +++ b/lib/components/ContentBoxLayout.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_html/shims/dart_ui.dart'; import 'package:fyx/components/post/Advertisement.dart'; import 'package:fyx/components/post/Poll.dart'; import 'package:fyx/components/post/PostFooterLink.dart'; @@ -9,24 +10,28 @@ import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/enums/PostTypeEnum.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/Image.dart' as model; +import 'package:fyx/model/post/content/Advertisement.dart'; +import 'package:fyx/model/post/content/Poll.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/UnreadBadgeDecoration.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:fyx/theme/skin/Skin.dart'; enum LAYOUT_TYPES { textOnly, oneImageOnly, attachmentsOnly, attachmentsAndText } -typedef Widget TLayout(); +typedef Widget? TLayout(); class ContentBoxLayout extends StatelessWidget { final Widget topLeftWidget; final Widget topRightWidget; - final Widget bottomWidget; + final Widget? bottomWidget; final Content content; final bool _isPreview; final bool _isHighlighted; final Map _layoutMap = {}; - final Function onTap; + final VoidCallback? onTap; - ContentBoxLayout({this.topLeftWidget, this.topRightWidget, this.bottomWidget, this.content, isPreview = false, isHighlighted = false, this.onTap}) + ContentBoxLayout({required this.topLeftWidget, required this.topRightWidget, this.bottomWidget, required this.content, isPreview = false, isHighlighted = false, this.onTap}) : _isPreview = isPreview, _isHighlighted = isHighlighted { // The order here is important! @@ -109,8 +114,10 @@ class ContentBoxLayout extends StatelessWidget { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Container( - decoration: _isPreview ? T.CARD_SHADOW_DECORATION : null, + decoration: _isPreview ? colors.shadow : null, child: Column( children: [ Visibility( @@ -121,8 +128,8 @@ class ContentBoxLayout extends StatelessWidget { ), ), Container( - color: _isHighlighted ? T.COLOR_SECONDARY.withOpacity(0.1) : null, - foregroundDecoration: _isHighlighted ? UnreadBadgeDecoration(badgeColor: T.COLOR_PRIMARY, badgeSize: 16) : null, + color: _isHighlighted ? colors.primary.withOpacity(0.1) : null, + foregroundDecoration: _isHighlighted ? UnreadBadgeDecoration(badgeColor: colors.primary, badgeSize: 16) : null, child: Column( children: [ SizedBox( @@ -132,7 +139,7 @@ class ContentBoxLayout extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [topLeftWidget ?? Container(), SizedBox(), _isPreview ? Container() : (topRightWidget ?? Container())], + children: [topLeftWidget, SizedBox(), _isPreview ? Container() : (topRightWidget)], ), ), if (this.onTap == null) @@ -161,7 +168,7 @@ class ContentBoxLayout extends StatelessWidget { SizedBox( height: 8, ), - this.bottomWidget != null ? Divider(color: Colors.black38) : Container(), + this.bottomWidget != null ? Divider(color: colors.grey) : Container(), this.bottomWidget != null ? Container(child: this.bottomWidget, padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16)) : Container(), SizedBox( height: 8, @@ -178,7 +185,7 @@ class ContentBoxLayout extends StatelessWidget { return MainRepository().settings.useCompactMode ? (() { for (final layout in LAYOUT_TYPES.values) { - var result = _layoutMap[layout](); + var result = _layoutMap[layout]!(); if (result != null) { return result; } @@ -191,11 +198,11 @@ class ContentBoxLayout extends StatelessWidget { Widget getWidgetByContentType(Content content) { switch (this.content.contentType) { case PostTypeEnum.poll: - return Poll(content); + return Poll(content as ContentPoll); case PostTypeEnum.text: return PostHtml(content); case PostTypeEnum.advertisement: - return Advertisement(content); + return Advertisement(content as ContentAdvertisement); default: return T.somethingsWrongButton(content.rawBody); } diff --git a/lib/components/DiscussionListItem.dart b/lib/components/DiscussionListItem.dart index 433c487..7b65b64 100644 --- a/lib/components/DiscussionListItem.dart +++ b/lib/components/DiscussionListItem.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fyx/model/BookmarkedDiscussion.dart'; import 'package:fyx/pages/DiscussionPage.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class DiscussionListItem extends StatelessWidget { final BookmarkedDiscussion discussion; @@ -12,10 +14,12 @@ class DiscussionListItem extends StatelessWidget { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return GestureDetector( onTap: () => Navigator.of(context, rootNavigator: true).pushNamed('/discussion', arguments: DiscussionPageArguments(discussion.idKlub)), child: Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.black12))), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.grey.withOpacity(.12)))), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( children: [ @@ -26,7 +30,7 @@ class DiscussionListItem extends StatelessWidget { : Container( width: 24, height: 24, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: discussion.replies > 0 ? Colors.red : CupertinoTheme.of(context).primaryColor), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: discussion.replies > 0 ? colors.danger : colors.primary), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Center( @@ -35,7 +39,7 @@ class DiscussionListItem extends StatelessWidget { maxFontSize: 12, maxLines: 1, minFontSize: 1, - style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600), + style: TextStyle(color: colors.background, fontSize: 12, fontWeight: FontWeight.w600), ), ), )), @@ -45,7 +49,7 @@ class DiscussionListItem extends StatelessWidget { Expanded( child: Text( discussion.name, - overflow: TextOverflow.ellipsis, + overflow: TextOverflow.ellipsis )), Visibility(visible: discussion.links > 0, child: Icon(Icons.link)), Visibility(visible: discussion.images > 0, child: Icon(Icons.image)), diff --git a/lib/components/FeedbackIndicator.dart b/lib/components/FeedbackIndicator.dart index 89acee4..df33e89 100644 --- a/lib/components/FeedbackIndicator.dart +++ b/lib/components/FeedbackIndicator.dart @@ -5,7 +5,7 @@ class FeedbackIndicator extends StatefulWidget { final Widget child; final bool isLoading; - FeedbackIndicator({Key key, this.child, this.isLoading}) : super(key: key); + FeedbackIndicator({Key? key, required this.child, required this.isLoading}) : super(key: key); @override _FeedbackIndicatorState createState() => _FeedbackIndicatorState(); diff --git a/lib/components/GestureFeedback.dart b/lib/components/GestureFeedback.dart index 4c815d0..1df1c55 100644 --- a/lib/components/GestureFeedback.dart +++ b/lib/components/GestureFeedback.dart @@ -4,7 +4,7 @@ class GestureFeedback extends StatefulWidget { final Widget child; final GestureTapCallback onTap; - GestureFeedback({Key key, @required this.child, this.onTap}) : super(key: key); + GestureFeedback({Key? key, required this.child, required this.onTap}) : super(key: key); @override _GestureFeedbackState createState() => _GestureFeedbackState(); diff --git a/lib/components/ListHeader.dart b/lib/components/ListHeader.dart index 9c54154..3b854a0 100644 --- a/lib/components/ListHeader.dart +++ b/lib/components/ListHeader.dart @@ -1,24 +1,28 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class ListHeader extends StatelessWidget { final String label; - final Function onTap; + final Function? onTap; ListHeader(this.label, {this.onTap}); @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return GestureDetector( - onTap: () => this.onTap is Function ? this.onTap() : null, + onTap: () => this.onTap!(), child: Container( - decoration: BoxDecoration(color: CupertinoTheme.of(context).primaryColor, border: Border(bottom: BorderSide(width: 1, color: Colors.white38))), + decoration: BoxDecoration(color: colors.primary, border: Border(bottom: BorderSide(width: 1, color: colors.background.withOpacity(0.38)))), padding: EdgeInsets.all(8), alignment: Alignment.centerLeft, child: Text( label, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: colors.background), ), ), ); diff --git a/lib/components/MailListItem.dart b/lib/components/MailListItem.dart index aa62e98..d10c140 100644 --- a/lib/components/MailListItem.dart +++ b/lib/components/MailListItem.dart @@ -10,14 +10,18 @@ import 'package:fyx/model/Mail.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/IconReply.dart'; +import 'package:fyx/theme/IconUnread.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class MailListItem extends StatefulWidget { final Mail mail; final bool isPreview; - final Function onUpdate; + final Function? onUpdate; - const MailListItem(this.mail, {this.isPreview, this.onUpdate}); + const MailListItem(this.mail, {this.isPreview = false, this.onUpdate}); @override _MailListItemState createState() => _MailListItemState(); @@ -26,33 +30,41 @@ class MailListItem extends StatefulWidget { class _MailListItemState extends State { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return ContentBoxLayout( isHighlighted: widget.mail.isNew, isPreview: widget.isPreview == true, content: widget.mail.content, topLeftWidget: PostAvatar( - widget.mail.direction == MailDirection.from ? widget.mail.participant : MainRepository().credentials.nickname, - description: '→ ${widget.mail.direction == MailDirection.to ? widget.mail.participant : MainRepository().credentials.nickname}, ~${Helpers.relativeTime(widget.mail.time)}' - ), + widget.mail.direction == MailDirection.from + ? widget.mail.participant + : MainRepository().credentials!.nickname, + description: + '→ ${widget.mail.direction == MailDirection.to ? widget.mail.participant : MainRepository().credentials!.nickname}, ~${Helpers.relativeTime(widget.mail.time)}'), topRightWidget: Row( children: [ Visibility( visible: widget.mail.isUnread, - child: T.ICO_UNREAD, + child: IconUnread(), ), SizedBox( width: 4, ), GestureDetector( - child: Icon(Icons.more_vert, color: Colors.black38), + child: Icon(Icons.more_vert, color: colors.text.withOpacity(0.38)), onTap: () => showCupertinoModalPopup( context: context, builder: (BuildContext context) => PostActionSheet( parentContext: context, user: widget.mail.participant, postId: widget.mail.id, - shareData: ShareData(subject: '@${widget.mail.participant}', body: widget.mail.content, link: widget.mail.link), - flagPostCallback: (mailId) => MainRepository().settings.blockMail(mailId), + shareData: ShareData( + subject: '@${widget.mail.participant}', + body: widget.mail.content, + link: widget.mail.link), + flagPostCallback: (mailId) => + MainRepository().settings.blockMail(mailId), )), ), ], @@ -63,21 +75,34 @@ class _MailListItemState extends State { GestureDetector( child: Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [T.ICO_REPLY, Text('Odpovědět', style: TextStyle(color: Colors.black38, fontSize: 14))], + children: [ + IconReply(), + Text('Odpovědět', + style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], ), - onTap: () => Navigator.of(context, rootNavigator: true).pushNamed('/new-message', - arguments: NewMessageSettings( - onSubmit: (String inputField, String message, List> attachments) async { - var response = await ApiController().sendMail(inputField, message, attachments: attachments); - return response.isOk; - }, - onClose: this.widget.onUpdate, - inputFieldPlaceholder: widget.mail.participant, - hasInputField: true, - replyWidget: MailListItem( - widget.mail, - isPreview: true, - ))), + onTap: () => Navigator.of(context, rootNavigator: true) + .pushNamed('/new-message', + arguments: NewMessageSettings( + onSubmit: (String? inputField, + String message, + List> + attachments) async { + if (inputField == null) { + return false; + } + var response = await ApiController().sendMail( + inputField, message, + attachments: attachments); + return response.isOk; + }, + onClose: this.widget.onUpdate!, + inputFieldPlaceholder: widget.mail.participant, + hasInputField: true, + replyWidget: MailListItem( + widget.mail, + isPreview: true, + ))), ) ]), ); diff --git a/lib/components/NavigationBarIcon.dart b/lib/components/NavigationBarIcon.dart deleted file mode 100644 index 3c30529..0000000 --- a/lib/components/NavigationBarIcon.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:fyx/theme/T.dart'; - -class NavigationBarIcon extends StatelessWidget { - final IconData icon; - final Color _color; - final double _size; - - NavigationBarIcon(this.icon, {Color color = T.COLOR_PRIMARY, double size = 28.0}) - : this._color = color, - this._size = size; - - @override - Widget build(BuildContext context) { - return Text.rich( - TextSpan( - text: String.fromCharCode(icon.codePoint), - style: TextStyle( - inherit: false, - color: _color, - fontSize: _size, - fontFamily: icon.fontFamily, - package: icon.fontPackage, - ), - ), - ); - } -} diff --git a/lib/components/NotificationBadge.dart b/lib/components/NotificationBadge.dart index 82b564f..bfae654 100644 --- a/lib/components/NotificationBadge.dart +++ b/lib/components/NotificationBadge.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class NotificationBadge extends StatelessWidget { final Widget widget; final bool isVisible; final int counter; - const NotificationBadge({Key key, @required this.widget, this.counter = 0, this.isVisible = false}) : super(key: key); + const NotificationBadge({Key? key, required this.widget, this.counter = 0, this.isVisible = false}) : super(key: key); @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Stack( alignment: Alignment.center, children: [ @@ -21,10 +25,10 @@ class NotificationBadge extends StatelessWidget { child: Container( padding: EdgeInsets.all(3), constraints: BoxConstraints(minWidth: 16), - decoration: BoxDecoration(color: Colors.redAccent, borderRadius: BorderRadius.circular(16)), + decoration: BoxDecoration(color: colors.danger, borderRadius: BorderRadius.circular(16)), child: Text( counter.toString(), - style: TextStyle(color: Colors.white, fontSize: 10), + style: TextStyle(color: colors.background, fontSize: 10), textAlign: TextAlign.center, ), ), diff --git a/lib/components/PullToRefreshList.dart b/lib/components/PullToRefreshList.dart index 0deb08e..44fff46 100644 --- a/lib/components/PullToRefreshList.dart +++ b/lib/components/PullToRefreshList.dart @@ -6,20 +6,23 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:sentry/sentry.dart'; // ignore: must_be_immutable class PullToRefreshList extends StatefulWidget { final TDataProvider dataProvider; - Function sliverListBuilder; + final Function? sliverListBuilder; bool _disabled; bool _isInfinite; int _rebuild; - final Widget pinnedWidget; + final Widget? pinnedWidget; - PullToRefreshList({@required this.dataProvider, isInfinite = false, int rebuild = 0, this.sliverListBuilder, bool disabled = false, this.pinnedWidget}) + PullToRefreshList( + {required this.dataProvider, isInfinite = false, int rebuild = 0, this.sliverListBuilder, bool disabled = false, this.pinnedWidget}) : _isInfinite = isInfinite, _rebuild = rebuild, _disabled = disabled, @@ -30,13 +33,13 @@ class PullToRefreshList extends StatefulWidget { } class _PullToRefreshListState extends State { - ScrollController _controller = ScrollController(); + ScrollController? _controller; bool _isLoading = true; bool _hasPulledDown = false; bool _hasError = false; - DataProviderResult _result; - int _lastId; - int _prevLastId; // ID of last item loaded previously. + DataProviderResult? _result; + int? _lastId; + int? _prevLastId; // ID of last item loaded previously. var _slivers = []; int _lastRebuild = 0; @@ -51,49 +54,43 @@ class _PullToRefreshListState extends State { void initState() { super.initState(); - if (widget._isInfinite) { - _controller.addListener(() { - // TODO: Refactor, use ScrollNotification ? - // Display loading and load next page if we are at the end of the list - if (_controller.position.userScrollDirection == ScrollDirection.reverse && _controller.position.outOfRange) { - if (_slivers.last is! SliverPadding) { - setState(() => _slivers.add(SliverPadding(padding: EdgeInsets.symmetric(vertical: 16), sliver: SliverToBoxAdapter(child: CupertinoActivityIndicator())))); - this.loadData(append: true); + () async { + await Future.delayed(Duration.zero); + _controller = PrimaryScrollController.of(context); + + // Add the refresh control on first position + _slivers.add(CupertinoSliverRefreshControl( + builder: Platform.isIOS ? CupertinoSliverRefreshControl.buildRefreshIndicator : buildAndroidRefreshIndicator, + onRefresh: () { + setState(() => _hasPulledDown = true); + if (!widget._disabled) { + return this.loadData(); } - } - }); - } - - // Add the refresh control on first position - _slivers.add(CupertinoSliverRefreshControl( - builder: Platform.isIOS ? CupertinoSliverRefreshControl.buildRefreshIndicator : buildAndroidRefreshIndicator, - onRefresh: () { - setState(() => _hasPulledDown = true); - if (!widget._disabled) { - return this.loadData(); - } - return Future.wait([]); - }, - )); + return Future.wait([]); + }, + )); - this.loadData(); + this.loadData(); + }(); } @override void dispose() { - _controller.dispose(); + _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + if (widget._rebuild > _lastRebuild && !_isLoading) { setState(() => _lastRebuild = widget._rebuild); this.loadData(); } if (_hasError) { - return T.feedbackScreen(isLoading: _isLoading, onPress: loadData, label: L.GENERAL_REFRESH); + return T.feedbackScreen(context, isLoading: _isLoading, onPress: loadData, label: L.GENERAL_REFRESH); } if (_slivers.length == 1 && !_isLoading) { @@ -114,38 +111,53 @@ class _PullToRefreshListState extends State { ); } - return CupertinoScrollbar( - child: Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: CustomScrollView( - physics: Platform.isIOS ? const AlwaysScrollableScrollPhysics() : const RefreshScrollPhysics(), - slivers: _slivers, + return Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (widget._isInfinite) { + if (_controller?.position.userScrollDirection == ScrollDirection.reverse && scrollInfo.metrics.outOfRange) { + if (_slivers.last is! SliverPadding) { + setState(() => _slivers.add(SliverPadding( + padding: EdgeInsets.symmetric(vertical: 16), sliver: SliverToBoxAdapter(child: CupertinoActivityIndicator())))); + this.loadData(append: true); + } + } + } + return false; + }, + child: CupertinoScrollbar( controller: _controller, + child: CustomScrollView( + physics: Platform.isIOS ? const AlwaysScrollableScrollPhysics() : const RefreshScrollPhysics(), + slivers: _slivers, + controller: _controller, + ), ), ), - ], - ), - Visibility( - visible: _isLoading && !_hasPulledDown, // Show only when not pulling down the list - child: Positioned( - top: 0, - left: 0, - child: Container( - width: MediaQuery.of(context).size.width, - height: 1, - child: LinearProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - backgroundColor: T.COLOR_PRIMARY, - ), + ), + ], + ), + Visibility( + visible: _isLoading && !_hasPulledDown, // Show only when not pulling down the list + child: Positioned( + top: 0, + left: 0, + child: Container( + width: MediaQuery.of(context).size.width, + height: 1, + child: LinearProgressIndicator( + valueColor: AlwaysStoppedAnimation(colors.light), + backgroundColor: colors.primary, ), ), - ) - ], - ), + ), + ) + ], ); } @@ -153,7 +165,7 @@ class _PullToRefreshListState extends State { // If the list contains widgets if (_data.first is Widget) { if (widget.sliverListBuilder is Function) { - return [widget.sliverListBuilder(_data)]; + return [widget.sliverListBuilder!(_data)]; } else { return [ SliverList( @@ -171,8 +183,8 @@ class _PullToRefreshListState extends State { List _list = []; _data.cast().forEach((block) { - _list.add(SliverStickyHeaderBuilder( - builder: (context, state) => block['header'], + _list.add(SliverStickyHeader( + header: block['header'], sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, i) => block['items'][i], @@ -216,7 +228,7 @@ class _PullToRefreshListState extends State { // If the ID of the last ID is same as the ID of currently loaded last ID // Make the list inactive (makeInactive = true) - if (_lastId != null && _result.lastId == _lastId) { + if (_lastId != null && _result!.lastId == _lastId) { makeInactive = true; if (append) { // ... and if also appending, remove the loading indicator @@ -225,15 +237,15 @@ class _PullToRefreshListState extends State { } // Load the data only if there are any data AND should not be inactive. - if (_result.data.length > 0 && !makeInactive) { + if (_result!.data.length > 0 && !makeInactive) { if (append) { _slivers.removeLast(); // Remove the loading indicator } else { _slivers.removeRange(1, _slivers.length); } - _slivers.addAll(this.buildTheList(_result.data)); + _slivers.addAll(this.buildTheList(_result!.data)); setState(() => _hasError = false); - setState(() => _lastId = _result.lastId); + setState(() => _lastId = _result!.lastId); } // Add the pinned widget only if the list is active @@ -245,7 +257,7 @@ class _PullToRefreshListState extends State { print('[PullToRefresh error]: $error'); print(StackTrace.current); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); } finally { setState(() { _hasPulledDown = false; @@ -261,6 +273,7 @@ class _PullToRefreshListState extends State { double refreshTriggerPullDistance, double refreshIndicatorExtent, ) { + SkinColors colors = Skin.of(context).theme.colors; const Curve opacityCurve = const Interval(0.4, 0.8, curve: Curves.easeInOut); return Align( alignment: Alignment.bottomCenter, @@ -269,15 +282,15 @@ class _PullToRefreshListState extends State { child: refreshState == RefreshIndicatorMode.drag ? Opacity( opacity: opacityCurve.transform(min(pulledExtent / refreshTriggerPullDistance, 1.0)), - child: const Icon( + child: Icon( Icons.arrow_downward, - color: CupertinoColors.inactiveGray, + color: colors.text.withOpacity(.35), size: 24.0, ), ) : Opacity( opacity: opacityCurve.transform(min(pulledExtent / refreshIndicatorExtent, 1.0)), - child: CircularProgressIndicator(strokeWidth: 2.0, valueColor: AlwaysStoppedAnimation(T.COLOR_PRIMARY)), + child: CircularProgressIndicator(strokeWidth: 2.0, valueColor: AlwaysStoppedAnimation(colors.primary)), ), ), ); @@ -285,10 +298,10 @@ class _PullToRefreshListState extends State { } class RefreshScrollPhysics extends BouncingScrollPhysics { - const RefreshScrollPhysics({ScrollPhysics parent}) : super(parent: parent); + const RefreshScrollPhysics({ScrollPhysics? parent}) : super(parent: parent); @override - RefreshScrollPhysics applyTo(ScrollPhysics ancestor) { + RefreshScrollPhysics applyTo(ScrollPhysics? ancestor) { return RefreshScrollPhysics(parent: buildParent(ancestor)); } @@ -305,4 +318,4 @@ class DataProviderResult { DataProviderResult(this.data, {this.lastId}); } -typedef Future TDataProvider(int id); +typedef Future TDataProvider(int? id); diff --git a/lib/components/TextIcon.dart b/lib/components/TextIcon.dart index 02c97eb..55cfe83 100644 --- a/lib/components/TextIcon.dart +++ b/lib/components/TextIcon.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; class TextIcon extends StatelessWidget { final String label; final IconData icon; - final Color iconColor; + final Color? iconColor; - const TextIcon(this.label, {Key key, this.icon, this.iconColor}) : super(key: key); + const TextIcon(this.label, {Key? key, required this.icon, this.iconColor}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/components/actionSheets/PostActionSheet.dart b/lib/components/actionSheets/PostActionSheet.dart index 4e7f554..0117bfe 100644 --- a/lib/components/actionSheets/PostActionSheet.dart +++ b/lib/components/actionSheets/PostActionSheet.dart @@ -7,6 +7,8 @@ import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:share/share.dart'; class ShareData { @@ -14,7 +16,7 @@ class ShareData { final Content body; final String link; - ShareData({this.subject, this.body, this.link}); + ShareData({required this.subject, required this.body, required this.link}); } class PostActionSheet extends StatefulWidget { @@ -24,7 +26,9 @@ class PostActionSheet extends StatefulWidget { final Function flagPostCallback; final ShareData shareData; - PostActionSheet({Key key, this.user, this.postId, this.flagPostCallback, this.parentContext, this.shareData}) : super(key: key); + PostActionSheet( + {Key? key, required this.user, required this.postId, required this.flagPostCallback, required this.parentContext, required this.shareData}) + : super(key: key); @override _PostActionSheetState createState() => _PostActionSheetState(); @@ -32,9 +36,12 @@ class PostActionSheet extends StatefulWidget { class _PostActionSheetState extends State { bool _reportIndicator = false; + int _deleteCounter = 0; @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return CupertinoActionSheet( actions: [ Visibility( @@ -44,7 +51,7 @@ class _PostActionSheetState extends State { onPressed: () { var data = ClipboardData(text: widget.shareData.link); Clipboard.setData(data).then((_) { - T.success(L.TOAST_COPIED); + T.success(L.TOAST_COPIED, bg: colors.success); Navigator.pop(context); }); AnalyticsProvider().logEvent('copyLink'); @@ -66,7 +73,7 @@ class _PostActionSheetState extends State { body = widget.shareData.body.videos.fold('', (previousValue, element) => '$previousValue ${element.link}').trim(); } - final RenderBox box = context.findRenderObject(); + final RenderBox box = context.findRenderObject() as RenderBox; Share.share(body, subject: widget.shareData.subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); Navigator.pop(context); AnalyticsProvider().logEvent('shareSheet'); @@ -76,12 +83,12 @@ class _PostActionSheetState extends State { child: TextIcon( L.POST_SHEET_HIDE, icon: Icons.visibility_off, - iconColor: Colors.redAccent, + iconColor: colors.danger, ), isDestructiveAction: true, onPressed: () { widget.flagPostCallback(widget.postId); - T.success(L.TOAST_POST_HIDDEN); + T.success(L.TOAST_POST_HIDDEN, bg: colors.success); Navigator.pop(context); AnalyticsProvider().logEvent('hidePost'); }), @@ -89,16 +96,16 @@ class _PostActionSheetState extends State { child: TextIcon( _reportIndicator ? L.POST_SHEET_FLAG_SAVING : L.POST_SHEET_FLAG, icon: Icons.warning, - iconColor: Colors.redAccent, + iconColor: colors.danger, ), isDestructiveAction: true, onPressed: () async { try { setState(() => _reportIndicator = true); - await ApiController().sendMail('FYXBOT', 'Inappropriate post/mail report: ID $widget.postId by user @$widget.user.'); - T.success(L.TOAST_POST_FLAGGED); + await ApiController().sendMail('FYXBOT', 'Inappropriate post/mail report: ID ${widget.postId} by user @${widget.user}.'); + T.success(L.TOAST_POST_FLAGGED, bg: colors.success); } catch (error) { - T.error(L.TOAST_POST_FLAG_ERROR); + T.error(L.TOAST_POST_FLAG_ERROR, bg: colors.danger); } finally { setState(() => _reportIndicator = false); Navigator.pop(context); diff --git a/lib/components/actionSheets/PostAvatarActionSheet.dart b/lib/components/actionSheets/PostAvatarActionSheet.dart index d0207e3..761b860 100644 --- a/lib/components/actionSheets/PostAvatarActionSheet.dart +++ b/lib/components/actionSheets/PostAvatarActionSheet.dart @@ -9,15 +9,19 @@ import 'package:fyx/pages/DiscussionPage.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class PostAvatarActionSheet extends StatelessWidget { final String user; final int idKlub; - const PostAvatarActionSheet({Key key, @required this.user, this.idKlub}) : super(key: key); + const PostAvatarActionSheet({Key? key, required this.user, required this.idKlub}) : super(key: key); @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return CupertinoActionSheet( title: Text(this.user), actions: [ @@ -29,8 +33,10 @@ class PostAvatarActionSheet extends StatelessWidget { arguments: NewMessageSettings( hasInputField: true, inputFieldPlaceholder: this.user, - onClose: () => T.success('👍 Zpráva poslána.'), - onSubmit: (String inputField, String message, List> attachments) async { + onClose: () => T.success('👍 Zpráva poslána.', bg: colors.success), + onSubmit: (String? inputField, String message, List> attachments) async { + if (inputField == null) return false; + var response = await ApiController().sendMail(inputField, message, attachments: attachments); return response.isOk; })); @@ -44,12 +50,12 @@ class PostAvatarActionSheet extends StatelessWidget { }, ), Visibility( - visible: user != MainRepository().credentials.nickname, + visible: user != MainRepository().credentials!.nickname, child: CupertinoActionSheetAction( child: TextIcon( '${L.POST_SHEET_BLOCK} @${user}', icon: Icons.block, - iconColor: Colors.redAccent, + iconColor: colors.danger, ), isDestructiveAction: true, onPressed: () { @@ -69,7 +75,7 @@ class PostAvatarActionSheet extends StatelessWidget { isDestructiveAction: true, onPressed: () { MainRepository().settings.blockUser(user); - T.success(L.TOAST_USER_BLOCKED); + T.success(L.TOAST_USER_BLOCKED, bg: colors.success); Navigator.of(context).pop(); AnalyticsProvider().logEvent('blockUser'); }, diff --git a/lib/components/post/Advertisement.dart b/lib/components/post/Advertisement.dart index 9fce1d7..39d2180 100644 --- a/lib/components/post/Advertisement.dart +++ b/lib/components/post/Advertisement.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/post/PostAvatar.dart'; import 'package:fyx/components/post/PostHeroAttachment.dart'; @@ -8,28 +9,32 @@ import 'package:fyx/model/post/Image.dart' as i; import 'package:fyx/model/post/content/Advertisement.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:fyx/theme/skin/Skin.dart'; class Advertisement extends StatelessWidget { final ContentAdvertisement content; - final String title; // Ad title can be overwritten. Helpful in discussion page where content.fullName is null. + final String? title; // Ad title can be overwritten. Helpful in discussion page where content.fullName is null. final String username; // If this widget needs to be displayed within PostListItem (in discussion) or as a standalone widget (pinned to pull-to-refresh) bool get isStandaloneWidget => this.username is String && this.username.isNotEmpty; - String get heading => this.title ?? (content.fullName ?? ''); + String get heading => this.title ?? (content.fullName); - const Advertisement(this.content, {this.title, this.username}); + const Advertisement(this.content, {this.title, this.username = ''}); Widget buildPriceWidget(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Container( padding: const EdgeInsets.all(6), child: Text('${content.price.toString()} ${content.currency}', style: DefaultTextStyle .of(context) .style - .copyWith(fontSize: 16, fontWeight: FontWeight.bold, color: T.COLOR_PRIMARY)), - decoration: BoxDecoration(color: Color(0xffa9ccd3), borderRadius: BorderRadius.circular(6))); + .copyWith(fontSize: 16, fontWeight: FontWeight.bold, color: colors.background)), + decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(6))); } Widget buildTitleWidget(BuildContext context) { @@ -40,13 +45,15 @@ class Advertisement extends StatelessWidget { } Widget buildRefrencesWidget(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + if (content.references is UserReferences) { return RichText( text: TextSpan(children: [ - TextSpan(text: 'Reference: ', style: TextStyle(color: Colors.black38, fontSize: 10)), - if (content.references.positive > 0) TextSpan(text: '+${content.references.positive}', style: TextStyle(color: T.COLOR_PRIMARY, fontSize: 10)), - if (content.references.positive > 0 && content.references.negative < 0) TextSpan(text: ' / ', style: TextStyle(color: Colors.black38, fontSize: 10)), - if (content.references.negative < 0) TextSpan(text: '-${content.references.negative}', style: TextStyle(color: T.COLOR_ACCENT, fontSize: 10)) + TextSpan(text: 'Reference: ', style: TextStyle(color: colors.grey, fontSize: 10)), + if (content.references != null && content.references!.positive > 0) TextSpan(text: '+${content.references!.positive}', style: TextStyle(color: colors.primary, fontSize: 10)), + if (content.references != null && content.references!.positive > 0 && content.references!.negative < 0) TextSpan(text: ' / ', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10)), + if (content.references != null && content.references!.negative < 0) TextSpan(text: '-${content.references!.negative}', style: TextStyle(color: colors.danger, fontSize: 10)) ]), ); } @@ -72,12 +79,14 @@ class Advertisement extends StatelessWidget { ), ], ), - //decoration: BoxDecoration(color: T.COLOR_LIGHT, borderRadius: BorderRadius.circular(6), border: Border.all(color: T.COLOR_PRIMARY)), + //decoration: BoxDecoration(color: T.COLOR_LIGHT, borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.primaryColor)), ); } @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -96,6 +105,7 @@ class Advertisement extends StatelessWidget { Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (heading.isNotEmpty) Flexible(child: buildTitleWidget(context)), @@ -114,9 +124,9 @@ class Advertisement extends StatelessWidget { style: DefaultTextStyle .of(context) .style - .copyWith(fontSize: 12, color: Colors.white), + .copyWith(fontSize: 12, color: content.type == AdTypeEnum.offer ? colors.background : colors.text), ), - decoration: BoxDecoration(color: content.type == AdTypeEnum.offer ? T.COLOR_SECONDARY : Color(0xff00B99D), borderRadius: BorderRadius.circular(6)), + decoration: BoxDecoration(color: content.type == AdTypeEnum.offer ? colors.primary : colors.highlight, borderRadius: BorderRadius.circular(6)), ), if (content.location.isNotEmpty) Padding( @@ -158,7 +168,7 @@ class Advertisement extends StatelessWidget { String small = 'https://nyx.cz/$thumb'; String large = small.replaceAllMapped(RegExp(r'(square)(\.[a-z]{3,4})$'), (match) => 'original${match[2]}'); - return i.Image(large, small); + return i.Image(large, thumb: small); }).toList(); return Wrap( children: images.map((image) => PostHeroAttachment(image, images: images)).toList(), diff --git a/lib/components/post/Poll.dart b/lib/components/post/Poll.dart index bcbd060..547b161 100644 --- a/lib/components/post/Poll.dart +++ b/lib/components/post/Poll.dart @@ -6,6 +6,8 @@ import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/model/post/content/Poll.dart'; import 'package:fyx/model/post/content/Regular.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:fyx/theme/skin/Skin.dart'; class Poll extends StatefulWidget { final ContentPoll content; @@ -19,7 +21,8 @@ class Poll extends StatefulWidget { class _PollState extends State { List _votes = []; bool _loading = false; - ContentPoll _poll; + ContentPoll? _poll; + ScrollController controller = ScrollController(); @override @@ -29,31 +32,33 @@ class _PollState extends State { } Widget buildAnswers(BuildContext context) { - var totalRespondents = _poll.pollComputedValues.totalRespondents; + SkinColors colors = Skin.of(context).theme.colors; + var totalRespondents = _poll!.pollComputedValues != null ? _poll!.pollComputedValues!.totalRespondents : 0; return ListView.builder( physics: NeverScrollableScrollPhysics(), + controller: controller, itemBuilder: (context, index) { - final answer = _poll.answers[index]; + final answer = _poll!.answers[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: GestureDetector( - onTap: !_poll.canVote ? null : () => setState(() { - if (_votes.contains(index)) { - _votes.remove(index); + onTap: !_poll!.canVote ? null : () => setState(() { + if (_votes.contains(answer.id)) { + _votes.remove(answer.id); } else { - if (_votes.length >= _poll.allowedVotes) { + if (_votes.length >= _poll!.allowedVotes) { _votes.removeLast(); - _votes.add(index); + _votes.add(answer.id); } else { - _votes.add(index); + _votes.add(answer.id); } } }), child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: _votes.contains(index) ? Color(0xff76b9b9) : Color(0xffa9ccd3), border: _poll.canVote ? Border.all(color: T.COLOR_PRIMARY) : null), + color: _votes.contains(answer.id) ? colors.pollAnswerSelected : colors.pollAnswer, border: _poll!.canVote ? Border.all(color: colors.primary) : null), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ PostHtml(ContentRegular(answer.answer)), if (answer.result != null) @@ -64,7 +69,7 @@ class _PollState extends State { child: FractionallySizedBox( widthFactor: totalRespondents > 0 ? (answer.result.respondentsCount / totalRespondents) + 0.005 : .005, child: Container( - color: answer.result.isMyVote ? T.COLOR_ACCENT : T.COLOR_PRIMARY, + color: answer.result.isMyVote ? colors.highlight : colors.primary, height: 10, ), ), @@ -79,49 +84,50 @@ class _PollState extends State { ), ); }, - itemCount: _poll.answers.length, + itemCount: _poll!.answers.length, shrinkWrap: true, padding: const EdgeInsets.all(0)); } @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Container( alignment: Alignment.centerLeft, child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text(_poll.question, style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20, fontWeight: FontWeight.bold)), - if (_poll.instructions != null) + Text(_poll!.question, style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20, fontWeight: FontWeight.bold)), + if (_poll!.instructions != null) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: PostHtml(ContentRegular(_poll.instructions)), + child: PostHtml(ContentRegular(_poll!.instructions)), ), - Text('Hlasů: ${_poll.pollComputedValues.totalVotes}\nHlasujících: ${_poll.pollComputedValues.totalRespondents}'), + if (_poll!.pollComputedValues != null) Text('Hlasů: ${_poll!.pollComputedValues!.totalVotes}\nHlasujících: ${_poll!.pollComputedValues!.totalRespondents}'), SizedBox(height: 8,), buildAnswers(context), - if (_poll.canVote) + if (_poll!.canVote) Padding( padding: const EdgeInsets.only(top: 16.0), child: CupertinoButton( onPressed: _votes.length == 0 || _loading ? null : () async { setState(() => _loading = true); try { - var votes = _votes.map((index) => index + 1).toList(); // Votes starting from 1 and not from 0. - var poll = await ApiController().votePoll(_poll.discussionId, _poll.postId, votes); + var poll = await ApiController().votePoll(_poll!.discussionId, _poll!.postId, _votes); setState(() => _poll = poll); } catch (error) { - T.error(error.toString()); + T.error(error.toString(), bg: colors.danger); } finally { setState(() => _loading = false); } }, - child: _loading ? CupertinoActivityIndicator() : Text('Hlasovat ${_votes.length}/${_poll.allowedVotes}'), - color: T.COLOR_PRIMARY, + child: _loading ? CupertinoActivityIndicator() : Text('Hlasovat ${_votes.length}/${_poll!.allowedVotes}'), + color: colors.primary, padding: EdgeInsets.all(0), - disabledColor: Colors.black26, + disabledColor: colors.disabled, ), ) ]), - color: Color(0xffcde5e9), + color: colors.pollBackground, padding: EdgeInsets.all(15)); } } diff --git a/lib/components/post/PostAvatar.dart b/lib/components/post/PostAvatar.dart index 755dc21..bd155fd 100644 --- a/lib/components/post/PostAvatar.dart +++ b/lib/components/post/PostAvatar.dart @@ -1,14 +1,17 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' as material; import 'package:flutter/widgets.dart'; -import 'package:fyx/components/CircleAvatar.dart'; +import 'package:fyx/components/Avatar.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class PostAvatar extends StatelessWidget { final String nick; final bool isHighlighted; - String description; - Widget descriptionWidget; + String? description; + Widget? descriptionWidget; String get image => Helpers.avatarUrl(nick); @@ -16,8 +19,10 @@ class PostAvatar extends StatelessWidget { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Row(children: [ - CircleAvatar(image, isHighlighted: isHighlighted), + Avatar(image), SizedBox( width: 4, ), @@ -28,7 +33,7 @@ class PostAvatar extends StatelessWidget { children: [ Text( nick, - style: TextStyle(color: isHighlighted ? T.COLOR_PRIMARY : material.Colors.black), + style: TextStyle(color: isHighlighted ? colors.primary : colors.text), ), Visibility( visible: isHighlighted, @@ -38,12 +43,15 @@ class PostAvatar extends StatelessWidget { ) ], ), - if (this.description is String) - Text( - description, - style: TextStyle(color: material.Colors.black38, fontSize: 10), + if (this.description != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + this.description!, + style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10), + ), ) - else if (this.descriptionWidget is Widget) this.descriptionWidget + else if (this.descriptionWidget != null) Padding(padding: const EdgeInsets.only(top: 4.0),child: this.descriptionWidget!) ], ) ]); diff --git a/lib/components/post/PostFooterLink.dart b/lib/components/post/PostFooterLink.dart index 67780d9..7f9169d 100644 --- a/lib/components/post/PostFooterLink.dart +++ b/lib/components/post/PostFooterLink.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fyx/model/post/Link.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class PostFooterLink extends StatelessWidget { final Link link; @@ -11,6 +13,8 @@ class PostFooterLink extends StatelessWidget { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return GestureDetector( onTap: () => T.openLink(link.url), child: Padding( @@ -19,10 +23,10 @@ class PostFooterLink extends StatelessWidget { children: [ Container( padding: EdgeInsets.all(4), - decoration: BoxDecoration(color: CupertinoTheme.of(context).primaryColor, borderRadius: BorderRadius.circular(4)), + decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(4)), child: Icon( Icons.link, - color: Colors.white, + color: colors.background, )), SizedBox( width: 4, diff --git a/lib/components/post/PostHeroAttachment.dart b/lib/components/post/PostHeroAttachment.dart index 903ad6e..8a23514 100644 --- a/lib/components/post/PostHeroAttachment.dart +++ b/lib/components/post/PostHeroAttachment.dart @@ -13,14 +13,14 @@ class GalleryArguments { final String imageUrl; final List images; - GalleryArguments(this.imageUrl, {this.images}); + GalleryArguments(this.imageUrl, {this.images = const []}); } class PostHeroAttachment extends StatelessWidget { final dynamic attachment; final List _images; final bool _crop; - final Function _onTap; + final Function? _onTap; final bool _openGallery; Size size; bool showStrip; @@ -36,8 +36,8 @@ class PostHeroAttachment extends StatelessWidget { if (attachment is model.Image) { return GestureDetector( onTap: () { - if (_onTap is Function) { - _onTap(); + if (_onTap != null) { + _onTap!(); } if (_openGallery) { Navigator.of(context, rootNavigator: true).pushNamed('/gallery', arguments: GalleryArguments((attachment as model.Image).image, images: _images)); @@ -69,13 +69,14 @@ class PostHeroAttachment extends StatelessWidget { } if (attachment is Video) { + var link = (attachment as Video).link; return PostHeroAttachmentBox( - title: (attachment as Video).link.title, + title: link == null ? '' : link.title, showStrip: this.showStrip, icon: Icons.play_circle_filled, image: (attachment as Video).thumb, size: size, - onTap: () => T.openLink((attachment as Video).link.url), + onTap: link == null ? null : () => T.openLink(link.url), ); } diff --git a/lib/components/post/PostHeroAttachmentBox.dart b/lib/components/post/PostHeroAttachmentBox.dart index 9a3159c..fbba9d3 100644 --- a/lib/components/post/PostHeroAttachmentBox.dart +++ b/lib/components/post/PostHeroAttachmentBox.dart @@ -1,27 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:fyx/theme/skin/Skin.dart'; class PostHeroAttachmentBox extends StatelessWidget { final String title; final IconData icon; - final String image; - final Function onTap; + final String? image; + final VoidCallback? onTap; Size size; bool showStrip; - PostHeroAttachmentBox({this.title, this.icon, this.image, this.onTap, this.showStrip = true, this.size = const Size(100, 100)}); + PostHeroAttachmentBox({required this.title, required this.icon, this.image, this.onTap, this.showStrip = true, this.size = const Size(100, 100)}); @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return GestureDetector( onTap: this.onTap, child: Container( width: this.size.width, height: this.size.height, decoration: BoxDecoration( - image: this.image == null ? null : DecorationImage(image: NetworkImage(this.image), fit: BoxFit.cover), - color: T.COLOR_PRIMARY, + image: this.image == null ? null : DecorationImage(image: NetworkImage(this.image!), fit: BoxFit.cover), + color: colors.primary, borderRadius: BorderRadius.circular(8), ), child: Column( @@ -29,19 +33,22 @@ class PostHeroAttachmentBox extends StatelessWidget { Expanded( child: Icon( icon, - color: Colors.white, + color: colors.light.withOpacity(.85), size: 40, ), ), if (this.showStrip) Container( - color: Color.fromRGBO(255, 255, 255, 0.6), - width: double.infinity, + decoration: BoxDecoration( + color: colors.light.withOpacity(.6), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)), + ), + width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Text( title, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 14), + style: TextStyle(fontSize: 14, color: colors.dark), ), ), ) diff --git a/lib/components/post/PostHtml.dart b/lib/components/post/PostHtml.dart index 18f796c..d4d1207 100644 --- a/lib/components/post/PostHtml.dart +++ b/lib/components/post/PostHtml.dart @@ -18,7 +18,7 @@ import 'package:html/dom.dart' as dom; import 'package:html_unescape/html_unescape.dart'; class PostHtml extends StatelessWidget { - final Content content; + final Content? content; bool _isImageTap = false; /// overloadRaw - if true, the content.rawBody is used to parse no matter what settings is on. @@ -27,37 +27,43 @@ class PostHtml extends StatelessWidget { @override Widget build(BuildContext context) { return Html( - data: MainRepository().settings.useCompactMode && content.consecutiveImages ? content.body : content.rawBody, + data: MainRepository().settings.useCompactMode && content!.consecutiveImages ? content!.body : content!.rawBody, style: { 'html': Style.fromTextStyle(CupertinoTheme.of(context).textTheme.textStyle), '.image-link': Style(textDecoration: TextDecoration.none), 'span.r': Style(fontWeight: FontWeight.bold), - 'body': Style(margin: EdgeInsets.all(0)) + 'body': Style(margin: EdgeInsets.all(0)), + 'pre': Style(color: Colors.transparent), }, customRender: { 'img': ( RenderContext renderContext, Widget parsedChild, - Map attributes, - dom.Element element, ) { - final String thumb = element.attributes['src']; + final element = renderContext.tree.element; + final String? thumb = element!.attributes['src']; + + if (thumb == null) { + return parsedChild; + } + String src = thumb; bool openGallery = true; - if (element.parent.localName == 'a') { + if (element.parent!.localName == 'a') { final RegExp r = RegExp(r'\.(jpg|jpeg|png|gif|webp)(\?.*)?$'); - if (r.hasMatch(element.parent.attributes['href'] ?? '')) { - src = element.parent.attributes['href']; + if (r.hasMatch(element.parent!.attributes['href'] ?? '')) { + src = element.parent!.attributes['href'] ?? ''; } else { openGallery = false; } } - post.Image img = post.Image(src, thumb); + + post.Image img = post.Image(src, thumb: thumb); return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: PostHeroAttachment( img, - images: content.images, + images: content!.images, openGallery: openGallery, onTap: () => openGallery ? _isImageTap = true : null, crop: false, @@ -67,14 +73,13 @@ class PostHtml extends StatelessWidget { 'video': ( RenderContext renderContext, Widget parsedChild, - Map attributes, - dom.Element element, ) { - var url = element.attributes['src']; + final element = renderContext.tree.element; + var url = element!.attributes['src']; var urls = element.querySelectorAll('source').map((element) => element.attributes['src']).toList(); if ([null, ''].contains(url) && urls.length > 0) { - url = urls.firstWhere((url) => url.endsWith('.mp4')); - if (url.isEmpty) { + url = urls.firstWhere((url) => url!.endsWith('.mp4')); + if (url!.isEmpty) { url = urls.first; } } @@ -82,16 +87,16 @@ class PostHtml extends StatelessWidget { return VideoPlayer(element); } - return T.somethingsWrongButton(content.rawBody); + return T.somethingsWrongButton(content!.rawBody); }, 'div': ( RenderContext renderContext, Widget parsedChild, - Map attributes, - dom.Element element, ) { + final element = renderContext.tree.element; + // Spoiler - if (element.classes.contains('spoiler')) { + if (element!.classes.contains('spoiler')) { return Spoiler(element.text); } @@ -106,10 +111,10 @@ class PostHtml extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8.0), child: PostHeroAttachment( Video( - id: element.attributes['data-embed-value'], - type: Video.findVideoType(element.attributes['data-embed-type']), - image: img.attributes['src'], - thumb: img.attributes['src']), + id: element.attributes['data-embed-value']!, + type: Video.findVideoType(element.attributes['data-embed-type']!), + image: img.attributes['src']!, + thumb: img.attributes['src']!), size: Size(double.infinity, MediaQuery.of(context).size.width * (0.5)), showStrip: false, ), @@ -121,11 +126,11 @@ class PostHtml extends StatelessWidget { 'span': ( RenderContext renderContext, Widget parsedChild, - Map attributes, - dom.Element element, ) { + final element = renderContext.tree.element; + // Spoiler - if (element.classes.contains('spoiler')) { + if (element!.classes.contains('spoiler')) { return Spoiler(element.text); } @@ -134,10 +139,10 @@ class PostHtml extends StatelessWidget { 'pre': ( RenderContext renderContext, Widget parsedChild, - Map attributes, - dom.Element element, ) { - if (attributes['style'] == 'background-color:#272822') { + final element = renderContext.tree.element; + + if (element!.attributes['style'] == 'background-color:#272822') { final source = HtmlUnescape().convert(element.text); return SyntaxHighlighter(source); } else { @@ -145,30 +150,35 @@ class PostHtml extends StatelessWidget { } } }, - onImageTap: (String src) { + onImageTap: (String? src, RenderContext context, Map attributes, dom.Element? element) { _isImageTap = true; - Navigator.of(context).pushNamed('/gallery', arguments: GalleryArguments(src, images: content.images)); + Navigator.of(context.buildContext).pushNamed('/gallery', arguments: GalleryArguments(src!, images: content!.images)); }, - onLinkTap: (String link) async { + onLinkTap: (String? link, RenderContext context, Map attributes, dom.Element? element) async { // 👇 https://github.com/Sub6Resources/flutter_html/issues/121#issuecomment-581593467 if (_isImageTap) { _isImageTap = false; return; } + if (link == null) { + return; + } + // Click through to another discussion var parserResult = Helpers.parseDiscussionUri(link); if (parserResult.isNotEmpty) { - var arguments = DiscussionPageArguments(parserResult[INTERNAL_URI_PARSER.discussionId]); - Navigator.of(context, rootNavigator: true).pushNamed('/discussion', arguments: arguments); + var arguments = DiscussionPageArguments(parserResult[INTERNAL_URI_PARSER.discussionId]!, search: parserResult[INTERNAL_URI_PARSER.search]); + Navigator.of(context.buildContext, rootNavigator: true).pushNamed('/discussion', arguments: arguments); return; } // Click through to another discussion with message deeplink parserResult = Helpers.parseDiscussionPostUri(link); if (parserResult.isNotEmpty) { - var arguments = DiscussionPageArguments(parserResult[INTERNAL_URI_PARSER.discussionId], postId: parserResult[INTERNAL_URI_PARSER.postId] + 1); - Navigator.of(context, rootNavigator: true).pushNamed('/discussion', arguments: arguments); + var arguments = + DiscussionPageArguments(parserResult[INTERNAL_URI_PARSER.discussionId]!, postId: parserResult[INTERNAL_URI_PARSER.postId]! + 1); + Navigator.of(context.buildContext, rootNavigator: true).pushNamed('/discussion', arguments: arguments); return; } diff --git a/lib/components/post/PostListItem.dart b/lib/components/post/PostListItem.dart index 6c98751..3cc54fb 100644 --- a/lib/components/post/PostListItem.dart +++ b/lib/components/post/PostListItem.dart @@ -15,14 +15,19 @@ import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/IconReply.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class PostDeleteFailNotification extends Notification {} class PostListItem extends StatefulWidget { final Post post; final bool _isPreview; final bool _isHighlighted; - final Function onUpdate; + final Function? onUpdate; PostListItem(this.post, {this.onUpdate, isPreview = false, isHighlighted = false}) : _isPreview = isPreview, @@ -33,7 +38,7 @@ class PostListItem extends StatefulWidget { } class _PostListItemState extends State { - Post _post; + Post? _post; bool _isSaving = false; @override @@ -44,87 +49,168 @@ class _PostListItemState extends State { @override Widget build(BuildContext context) { - return ContentBoxLayout( - isPreview: widget._isPreview, - isHighlighted: widget._isHighlighted, - topLeftWidget: GestureFeedback( - onTap: () => showCupertinoModalPopup(context: context, builder: (BuildContext context) => PostAvatarActionSheet(user: _post.nick, idKlub: _post.idKlub,)), - child: PostAvatar( - _post.nick, - description: Helpers.relativeTime(_post.time), + SkinColors colors = Skin.of(context).theme.colors; + + return Dismissible( + key: UniqueKey(), + direction: _post!.canBeDeleted ? DismissDirection.endToStart : DismissDirection.none, + onDismissed: (direction) { + ApiController().deleteDiscussionMessage(_post!.idKlub, _post!.id).then((response) { + T.success('👍 Smazáno', bg: colors.success); + }).onError((error, stackTrace) { + PostDeleteFailNotification().dispatch(context); + }); + }, + background: Container( + alignment: Alignment.centerRight, + color: colors.danger, + padding: const EdgeInsets.all(32), + child: Icon( + Icons.delete, + size: 32, + color: colors.background, ), ), - topRightWidget: GestureDetector( - child: Icon(Icons.more_vert, color: Colors.black38), - onTap: () => showCupertinoModalPopup( - context: context, - builder: (BuildContext context) => PostActionSheet( - parentContext: context, - user: _post.nick, - postId: _post.id, - shareData: ShareData(subject: '@${_post.nick}', body: _post.content, link: _post.link), - flagPostCallback: (postId) => MainRepository().settings.blockPost(postId)))), - bottomWidget: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PostRating(_post), - Row( - children: [ - Visibility( - visible: widget._isPreview != true && _post.canReply, - child: GestureDetector( - onTap: () => Navigator.of(context).pushNamed('/new-message', - arguments: NewMessageSettings( - replyWidget: PostListItem( - _post, - isPreview: true, - ), - onClose: this.widget.onUpdate, - onSubmit: (String inputField, String message, List> attachments) async { - var result = await ApiController().postDiscussionMessage(_post.idKlub, message, attachments: attachments, replyPost: _post); - return result.isOk; - })), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [T.ICO_REPLY, Text('Odpovědět', style: TextStyle(color: Colors.black38, fontSize: 14))], + child: GestureDetector( + onDoubleTap: () { + if (!_post!.canBeRated) { + return null; + } + + ApiController().giveRating(_post!.idKlub, _post!.id, remove: _post!.myRating != 'none').then((response) { + if (_post!.myRating != 'none') { + T.success('👎', bg: colors.success); + } else { + T.success('👍', bg: colors.success); + } + print(response.currentRating); + print(response.myRating); + print(response.isGiven); + setState(() { + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; + }); + }).catchError((error) { + print(error); + T.error(L.RATING_ERROR, bg: colors.danger); + }); + }, + behavior: HitTestBehavior.opaque, + child: ContentBoxLayout( + isPreview: widget._isPreview, + isHighlighted: widget._isHighlighted, + topLeftWidget: GestureFeedback( + onTap: () => showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => PostAvatarActionSheet( + user: _post!.nick, + idKlub: _post!.idKlub, )), + child: PostAvatar( + _post!.nick, + descriptionWidget: Row( + children: [ + if (_post!.rating != null) + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 1), + decoration: BoxDecoration( + color: _post!.rating! > 0 + ? colors.success.withOpacity(Helpers.ratingRange(_post!.rating!)) + : (_post!.rating! < 0 + ? colors.danger.withOpacity(Helpers.ratingRange(_post!.rating!.abs())) + : colors.text.withOpacity(0.2)), + borderRadius: BorderRadius.circular(2)), + child: Text(Post.formatRating(_post!.rating!), style: TextStyle(fontSize: 10)), + ), + if (_post!.rating != null) SizedBox(width: 8), + Text( + Helpers.absoluteTime(_post!.time), + style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10), + ), + SizedBox(width: 8), + Text( + '~${Helpers.relativeTime(_post!.time)}', + style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10), + ) + ], ), - Visibility( - visible: widget._isPreview != true, - child: SizedBox( - width: 16, - ), - ), - if (_post.canBeReminded) GestureDetector( - child: FeedbackIndicator( - isLoading: _isSaving, - child: Row( - children: [ - Icon( - _post.hasReminder ? Icons.bookmark : Icons.bookmark_border, - color: Colors.black38, - ), - Text('Uložit', style: TextStyle(color: Colors.black38, fontSize: 14)) - ], + ), + ), + topRightWidget: GestureDetector( + child: Icon(Icons.more_vert, color: colors.text.withOpacity(0.38)), + onTap: () => showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => PostActionSheet( + parentContext: context, + user: _post!.nick, + postId: _post!.id, + shareData: ShareData(subject: '@${_post!.nick}', body: _post!.content, link: _post!.link), + flagPostCallback: (postId) => MainRepository().settings.blockPost(postId)))), + bottomWidget: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), + Row( + children: [ + Visibility( + visible: widget._isPreview != true && _post!.canReply, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed('/new-message', + arguments: NewMessageSettings( + replyWidget: PostListItem( + _post!, + isPreview: true, + ), + onClose: this.widget.onUpdate, + onSubmit: (String? inputField, String message, List> attachments) async { + var result = + await ApiController().postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); + return result.isOk; + })), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [IconReply(), Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14))], + )), + ), + Visibility( + visible: widget._isPreview != true, + child: SizedBox( + width: 16, + ), ), - ), - onTap: () { - setState(() { - _post.hasReminder = !_post.hasReminder; - _isSaving = true; - }); - ApiController().setPostReminder(_post.idKlub, _post.id, _post.hasReminder).catchError((error) { - T.error(L.REMINDER_ERROR); - setState(() => _post.hasReminder = !_post.hasReminder); - }).whenComplete(() => setState(() => _isSaving = false)); - AnalyticsProvider().logEvent('reminder'); - }, + if (_post!.canBeReminded) + GestureDetector( + child: FeedbackIndicator( + isLoading: _isSaving, + child: Row( + children: [ + Icon( + _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, + color: colors.text.withOpacity(0.38), + ), + Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], + ), + ), + onTap: () { + setState(() { + _post!.hasReminder = !_post!.hasReminder; + _isSaving = true; + }); + ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { + T.error(L.REMINDER_ERROR, bg: colors.danger); + setState(() => _post!.hasReminder = !_post!.hasReminder); + }).whenComplete(() => setState(() => _isSaving = false)); + AnalyticsProvider().logEvent('reminder'); + }, + ) + ], ) ], - ) - ], + ), + content: _post!.content, + ), ), - content: _post.content, ); } diff --git a/lib/components/post/PostRating.dart b/lib/components/post/PostRating.dart index 13d0199..6aa4a9e 100644 --- a/lib/components/post/PostRating.dart +++ b/lib/components/post/PostRating.dart @@ -2,22 +2,24 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/FeedbackIndicator.dart'; import 'package:fyx/controllers/ApiController.dart'; -import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class PostRating extends StatefulWidget { final Post post; + Function? onRatingChange; - PostRating(this.post, {Key key}) : super(key: key); + PostRating(this.post, {Key? key, this.onRatingChange}) : super(key: key); @override _PostRatingState createState() => _PostRatingState(); } class _PostRatingState extends State { - Post _post; + Post? _post; bool _givingRating = false; @override @@ -36,29 +38,34 @@ class _PostRatingState extends State { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return FeedbackIndicator( isLoading: _givingRating, child: Row( children: [ Visibility( - visible: _post.canBeRated, + visible: _post!.canBeRated, child: GestureDetector( child: Icon( Icons.thumb_up, - color: _post.myRating == 'positive' ? Colors.green : Colors.black38, + color: _post!.myRating == 'positive' ? colors.success : colors.text.withOpacity(0.38), ), onTap: _givingRating ? null : () { setState(() => _givingRating = true); - ApiController().giveRating(_post.idKlub, _post.id, remove: _post.myRating != 'none').then((response) { + ApiController().giveRating(_post!.idKlub, _post!.id, remove: _post!.myRating != 'none').then((response) { setState(() { - _post.rating = response.currentRating; - _post.myRating = response.myRating; + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; }); + if (widget.onRatingChange != null) { + widget.onRatingChange!(_post); + } }).catchError((error) { print(error); - T.error(L.RATING_ERROR); + T.error(L.RATING_ERROR, bg: colors.danger); }).whenComplete(() => setState(() => _givingRating = false)); }, ), @@ -66,31 +73,30 @@ class _PostRatingState extends State { SizedBox( width: 4, ), - Visibility( - visible: _post.rating != 0 || MainRepository().credentials.nickname != _post.nick, - child: Opacity( + if (_post!.rating != null) + Opacity( opacity: _givingRating ? 0 : 1, child: Text( - _post.rating > 0 ? '+${_post.rating}' : _post.rating.toString(), - style: TextStyle(fontSize: 14, color: _post.rating > 0 ? Colors.green : (_post.rating < 0 ? Colors.redAccent : Colors.black38)), + Post.formatRating(_post!.rating!), + style: TextStyle( + fontSize: 14, color: _post!.rating! > 0 ? colors.success : (_post!.rating! < 0 ? colors.danger : colors.text.withOpacity(0.38))), ), ), - ), SizedBox( width: 4, ), Visibility( - visible: _post.canBeRated, + visible: _post!.canBeRated, child: GestureDetector( child: Icon( Icons.thumb_down, - color: ['negative', 'negative_visible'].contains(_post.myRating) ? Colors.redAccent : Colors.black38, + color: ['negative', 'negative_visible'].contains(_post!.myRating) ? colors.danger : colors.text.withOpacity(0.38), ), onTap: _givingRating ? null : () { setState(() => _givingRating = true); - ApiController().giveRating(_post.idKlub, _post.id, positive: false, remove: _post.myRating != 'none').then((response) { + ApiController().giveRating(_post!.idKlub, _post!.id, positive: false, remove: _post!.myRating != 'none').then((response) { if (response.needsConfirmation) { showCupertinoDialog( context: context, @@ -110,14 +116,19 @@ class _PostRatingState extends State { isDestructiveAction: true, child: new Text('Hodnotit'), onPressed: () { - ApiController().giveRating(_post.idKlub, _post.id, positive: false, confirm: true, remove: _post.myRating != 'none').then((response) { + ApiController() + .giveRating(_post!.idKlub, _post!.id, positive: false, confirm: true, remove: _post!.myRating != 'none') + .then((response) { setState(() { - _post.rating = response.currentRating; - _post.myRating = response.myRating; + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; }); + if (widget.onRatingChange != null) { + widget.onRatingChange!(_post); + } }).catchError((error) { print(error); - T.error(L.RATING_ERROR); + T.error(L.RATING_ERROR, bg: colors.danger); }).whenComplete(() { setState(() => _givingRating = false); Navigator.of(context, rootNavigator: true).pop(); @@ -128,14 +139,17 @@ class _PostRatingState extends State { ); } else { setState(() { - _post.rating = response.currentRating; - _post.myRating = response.myRating; + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; _givingRating = false; }); + if (widget.onRatingChange != null) { + widget.onRatingChange!(_post); + } } }).catchError((error) { setState(() => _givingRating = false); - T.error(L.RATING_ERROR); + T.error(L.RATING_ERROR, bg: colors.danger); }); }, ), diff --git a/lib/components/post/Spoiler.dart b/lib/components/post/Spoiler.dart index 3d9add4..453be1a 100644 --- a/lib/components/post/Spoiler.dart +++ b/lib/components/post/Spoiler.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class Spoiler extends StatefulWidget { final String text; - Spoiler(this.text, {Key key}) : super(key: key); + Spoiler(this.text, {Key? key}) : super(key: key); @override _SpoilerState createState() => _SpoilerState(); } class _SpoilerState extends State { - String _text; + late final String _text; bool _toggle = false; @override @@ -22,6 +24,8 @@ class _SpoilerState extends State { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return GestureDetector( onTap: () => setState(() => _toggle = !_toggle), child: RichText( @@ -29,7 +33,7 @@ class _SpoilerState extends State { TextSpan(text: 'Spoiler ⮕ ', style: DefaultTextStyle.of(context).style), TextSpan( text: '$_text', - style: DefaultTextStyle.of(context).style.apply(backgroundColor: _toggle ? Colors.transparent : T.COLOR_BLACK), + style: DefaultTextStyle.of(context).style.apply(backgroundColor: _toggle ? Colors.transparent : colors.text), ), ]), ), diff --git a/lib/components/post/SyntaxHighlighter.dart b/lib/components/post/SyntaxHighlighter.dart index d4f5383..4657098 100644 --- a/lib/components/post/SyntaxHighlighter.dart +++ b/lib/components/post/SyntaxHighlighter.dart @@ -9,7 +9,7 @@ class SyntaxHighlighter extends StatelessWidget { // TODO: Get rid of static. static String languageContext = ''; - const SyntaxHighlighter(this.source, {Key key}) : super(key: key); + const SyntaxHighlighter(this.source, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -19,11 +19,7 @@ class SyntaxHighlighter extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ HighlightView(source, - language: lang, - theme: draculaTheme, - padding: EdgeInsets.all(12), - tabSize: 2, - textStyle: TextStyle(fontFamily: 'JetBrainsMono')), + language: lang, theme: draculaTheme, padding: EdgeInsets.all(12), tabSize: 2, textStyle: TextStyle(fontFamily: 'JetBrainsMono')), Container( alignment: Alignment.centerRight, child: Text( @@ -44,8 +40,7 @@ class SyntaxHighlighter extends StatelessWidget { Map langs = { 'php': r"php|wordpress", - 'javascript': - r"javascript|typescript|angular|\.js|ajax|angular|react|vue", + 'javascript': r"javascript|typescript|angular|\.js|ajax|angular|react|vue", 'java': r"java|android", 'sql': r"sql", 'css': r"css|scss|sass|less", @@ -63,6 +58,7 @@ class SyntaxHighlighter extends StatelessWidget { 'cs': r"csharp|c#|\.net|asp", 'lisp': r"lisp", 'ruby': r"ruby", + 'rust': r"rust", 'scala': r"scala|clojure", 'bash': r"bash|shell|unix|linux", 'swift': r"swift", @@ -72,7 +68,7 @@ class SyntaxHighlighter extends StatelessWidget { String foundLang = 'plaintext'; for (String key in langs.keys) { - final RegExp regexp = new RegExp(langs[key], caseSensitive: false); + final RegExp regexp = new RegExp(langs[key]!, caseSensitive: false); if (regexp.hasMatch(SyntaxHighlighter.languageContext)) { foundLang = key; break; diff --git a/lib/components/post/VideoPlayer.dart b/lib/components/post/VideoPlayer.dart index db8d729..627fdd0 100644 --- a/lib/components/post/VideoPlayer.dart +++ b/lib/components/post/VideoPlayer.dart @@ -1,21 +1,24 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:fyx/theme/skin/Skin.dart'; import 'package:html/dom.dart' as dom; import 'package:video_player/video_player.dart'; class VideoPlayer extends StatefulWidget { final dom.Element element; - String videoUrl; + late final String? videoUrl; VideoPlayer(this.element) { videoUrl = element.attributes['src']; var urls = element.querySelectorAll('source').map((element) => element.attributes['src']).toList(); if ([null, ''].contains(videoUrl) && urls.length > 0) { - videoUrl = urls.firstWhere((url) => url.endsWith('.mp4')); - if (videoUrl.isEmpty) { + videoUrl = urls.firstWhere((url) => url is String && url.endsWith('.mp4')); + if ((videoUrl as String).isEmpty) { videoUrl = urls.first; } } @@ -26,35 +29,40 @@ class VideoPlayer extends StatefulWidget { } class _VideoPlayerState extends State { - VideoPlayerController videoPlayerController; - ChewieController chewieController; + VideoPlayerController? videoPlayerController; + ChewieController? chewieController; + late SkinColors colors; @override void initState() { super.initState(); - if (widget.videoUrl?.isEmpty ?? true) { - return; + if (widget.videoUrl != null && widget.videoUrl!.isNotEmpty) { + videoPlayerController = VideoPlayerController.network(widget.videoUrl!); } - - videoPlayerController = VideoPlayerController.network(widget.videoUrl); } Future initVideo(BuildContext context) async { - await videoPlayerController.initialize(); + if (videoPlayerController == null) { + return false; + } + + SkinColors colors = Skin.of(context).theme.colors; + await videoPlayerController!.initialize(); final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; - final aspectRatio = videoPlayerController.value.initialized ? videoPlayerController.value.aspectRatio : (width > height ? width / height : height / width); + final aspectRatio = + videoPlayerController!.value.isInitialized ? videoPlayerController!.value.aspectRatio : (width > height ? width / height : height / width); chewieController = ChewieController( - videoPlayerController: videoPlayerController, + videoPlayerController: videoPlayerController!, aspectRatio: aspectRatio, placeholder: Container( - color: T.COLOR_PRIMARY, + color: colors.primary, child: Icon( Icons.camera_roll, - color: Colors.white.withAlpha(75), + color: colors.background.withAlpha(75), size: 32, ), alignment: Alignment.center, @@ -65,55 +73,76 @@ class _VideoPlayerState extends State { @override void dispose() { if (chewieController != null) { - chewieController.dispose(); + chewieController!.dispose(); } if (videoPlayerController != null) { - videoPlayerController.dispose(); + videoPlayerController!.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { + colors = Skin.of(context).theme.colors; if (widget.videoUrl?.isEmpty ?? true) { return T.somethingsWrongButton(widget.element.outerHtml); } return Card( elevation: 0, + color: colors.background, child: FutureBuilder( future: initVideo(context), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData && snapshot.data == true) { return Column( children: [ - AspectRatio(aspectRatio: videoPlayerController.value.aspectRatio, child: Chewie(controller: chewieController)), + AspectRatio(aspectRatio: videoPlayerController!.value.aspectRatio, child: Chewie(controller: chewieController!)), SizedBox( height: 8, ), - GestureDetector( - onTap: () => T.openLink(widget.videoUrl), - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan(children: [ - TextSpan(text: 'Zdroj: ', style: DefaultTextStyle.of(context).style.merge(TextStyle(fontSize: 12))), - TextSpan( - text: widget.videoUrl.replaceAll('', '\u{200B}'), - style: TextStyle(fontSize: 12, color: T.COLOR_PRIMARY, decoration: TextDecoration.underline), - ) - ]), - ), - ), + _sourceButton(), SizedBox( height: 8, ) ], ); } else if (snapshot.hasError) { - return T.somethingsWrongButton(widget.element.outerHtml); + if (snapshot.error is PlatformException) { + final error = (snapshot.error as PlatformException); + return Column(children: [ + T.somethingsWrongButton(widget.element.outerHtml, + icon: Icons.play_disabled, title: 'Video se nepodařilo nahrát.\n${error.message}', stack: error.stacktrace ?? ''), + _sourceButton() + ]); + } + return Column(children: [ + T.somethingsWrongButton(widget.element.outerHtml, + icon: Icons.play_disabled, title: 'Video se nepodařilo nahrát.', stack: snapshot.error.toString()), + _sourceButton() + ]); } return Center(child: CupertinoActivityIndicator()); }), ); } + + Widget _sourceButton() { + return GestureDetector( + onTap: () => T.openLink(widget.videoUrl!), + child: Padding( + padding: const EdgeInsets.all(4), + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan(children: [ + TextSpan(text: 'Zdroj: ', style: DefaultTextStyle.of(context).style.merge(TextStyle(fontSize: 12, color: colors.text))), + TextSpan( + text: widget.videoUrl!.replaceAll('', '\u{200B}'), + style: TextStyle(fontSize: 12, color: colors.primary, decoration: TextDecoration.underline), + ) + ]), + ), + ), + ); + } } diff --git a/lib/controllers/AnalyticsProvider.dart b/lib/controllers/AnalyticsProvider.dart index 8191164..6644883 100644 --- a/lib/controllers/AnalyticsProvider.dart +++ b/lib/controllers/AnalyticsProvider.dart @@ -2,7 +2,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; class AnalyticsProvider { static final AnalyticsProvider _instance = AnalyticsProvider._init(); - static FirebaseAnalytics provider; + static FirebaseAnalytics? provider; factory AnalyticsProvider() { if (provider == null) { @@ -14,30 +14,30 @@ class AnalyticsProvider { AnalyticsProvider._init(); Future setUser(String userId) async { - await provider.setUserId(userId); + await provider!.setUserId(userId); } Future setScreen(String screenName, String screenClassOverride) async { - await provider.setCurrentScreen( + await provider!.setCurrentScreen( screenName: screenName, screenClassOverride: screenClassOverride, ); } Future setUserProperty(String name, String value) async { - await provider.setUserProperty(name: name, value: value); + await provider!.setUserProperty(name: name, value: value); } Future logTutorialBegin() async { - await provider.logTutorialBegin(); + await provider!.logTutorialBegin(); } Future logTutorialComplete() async { - await provider.logTutorialComplete(); + await provider!.logTutorialComplete(); } - Future logEvent(String name, {Map parameters}) async { - await provider.logEvent( + Future logEvent(String name, {Map? parameters}) async { + await provider!.logEvent( name: name, parameters: parameters ); diff --git a/lib/controllers/ApiController.dart b/lib/controllers/ApiController.dart index e29ad1e..7879440 100644 --- a/lib/controllers/ApiController.dart +++ b/lib/controllers/ApiController.dart @@ -7,7 +7,6 @@ import 'package:fyx/controllers/ApiProvider.dart'; import 'package:fyx/controllers/IApiProvider.dart'; import 'package:fyx/exceptions/AuthException.dart'; import 'package:fyx/model/Credentials.dart'; -import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; import 'package:fyx/model/ResponseContext.dart'; import 'package:fyx/model/post/content/Poll.dart'; @@ -26,23 +25,22 @@ import 'package:fyx/model/reponses/WaitingFilesResponse.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:provider/provider.dart'; +import 'package:sentry/sentry.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum AUTH_STATES { AUTH_INVALID_USERNAME, AUTH_NEW, AUTH_EXISTING } class ApiController { static ApiController _instance = ApiController._init(); - IApiProvider provider; + IApiProvider provider = ApiProvider(); bool isLoggingIn = false; - BuildContext buildContext; + BuildContext? buildContext; factory ApiController() { return _instance; } ApiController._init() { - provider = ApiProvider(); - provider.onAuthError = (String message) { // API returns the same error on authorization as well as on normal data request. Therefore this "workaround". if (isLoggingIn) { @@ -51,7 +49,9 @@ class ApiController { this.logout(removeAuthrorization: false); T.error(message == '' ? L.AUTH_ERROR : message); - FyxApp.navigatorKey.currentState.pushNamed('/login'); + if (FyxApp.navigatorKey.currentState != null) { + FyxApp.navigatorKey.currentState!.pushNamed('/login'); + } }; provider.onError = (message) { @@ -64,8 +64,8 @@ class ApiController { } ResponseContext responseContext = ResponseContext.fromJson(data); - Provider.of(buildContext, listen: false).setNewMails(responseContext.user.mailUnread); - Provider.of(buildContext, listen: false).setNewNotices(responseContext.user.notificationsUnread); + Provider.of(buildContext!, listen: false).setNewMails(responseContext.user.mailUnread); + Provider.of(buildContext!, listen: false).setNewNotices(responseContext.user.notificationsUnread); }; } @@ -82,7 +82,7 @@ class ApiController { return loginResponse; } - Future setCredentials(Credentials credentials) async { + Future setCredentials(Credentials credentials) async { if (credentials.isValid) { provider.setCredentials(credentials); var storage = await SharedPreferences.getInstance(); @@ -93,21 +93,21 @@ class ApiController { return Future(() => null); } - Future getCredentials() async { - Credentials creds = provider.getCredentials(); + Future getCredentials() async { + Credentials? creds = provider.getCredentials(); - if (creds is Credentials) { + if (creds != null) { return creds; } var prefs = await SharedPreferences.getInstance(); - String identity = prefs.getString('identity'); + String? identity = prefs.getString('identity'); // Breaking change fix -> old identity storage // TODO: Delete in 3/2021 ? if (identity == null) { // Load identity from old storage - creds = Credentials(prefs.getString('nickname'), prefs.getString('token')); + creds = Credentials(prefs.getString('nickname') ?? '', prefs.getString('token') ?? ''); // Save the identity into the new storage this.setCredentials(creds); // Remove the old fragments @@ -134,7 +134,7 @@ class ApiController { this.setCredentials(creds.copyWith(fcmToken: token)); } catch (error) { debugPrint(error.toString()); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); } } }); @@ -152,7 +152,7 @@ class ApiController { this.setCredentials(creds.copyWith(fcmToken: token)); } catch (error) { debugPrint(error.toString()); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); } }); } @@ -167,12 +167,21 @@ class ApiController { return BookmarksAllResponse.fromJson(response.data); } - Future loadDiscussion(int id, {int lastId, String user}) async { - var response = await provider.fetchDiscussion(id, lastId: lastId == null ? null : lastId, user: user); - if (response.statusCode == 400) { - return DiscussionResponse.accessDenied(); + Future loadDiscussion(int id, {int? lastId, String? user, String? search}) async { + try { + var response = await provider.fetchDiscussion(id, lastId: lastId, user: user, search: search); + if (response.statusCode == 400) { + return DiscussionResponse.accessDenied(); + } + return DiscussionResponse.fromJson(response.data); + } catch (error) { + if (error is DioError) { + if (error.response?.statusCode == 400) { + return DiscussionResponse.accessDenied(); + } + } } - return DiscussionResponse.fromJson(response.data); + return DiscussionResponse.error(); } Future getDiscussionHome(int id) async { @@ -185,21 +194,23 @@ class ApiController { return FeedNoticesResponse.fromJson(response.data); } - Future postDiscussionMessage(int id, String message, {List> attachments, Post replyPost}) async { - if (attachments is List) { + Future postDiscussionMessage(int id, String message, {List>? attachments, Post? replyPost}) async { + if (attachments != null) { try { WaitingFilesResponse waitingFilesResponse = await this.fetchDiscussionWaitingFiles(id); await this.deleteAllWaitingFiles(waitingFilesResponse.files); } catch (error) { debugPrint(error.toString()); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); // TODO: Notify user? } try { await provider.uploadFile(attachments, id: id); } catch (error) { - provider.onError('👎 Nějakteré z obrázků se nepodařilo nahrát.'); + if (provider.onError != null) { + provider.onError!('👎 Nějakteré z obrázků se nepodařilo nahrát.'); + } } } @@ -211,6 +222,10 @@ class ApiController { return OkResponse.fromJson(result.data); } + Future deleteDiscussionMessage(int discussionId, int postId) { + return provider.deleteDiscussionMessage(discussionId, postId); + } + Future setPostReminder(int discussionId, int postId, bool setReminder) { return provider.setPostReminder(discussionId, postId, setReminder); } @@ -219,7 +234,10 @@ class ApiController { Response response = await provider.giveRating(discussionId, postId, positive, confirm, remove); var data = response.data; return RatingResponse( - isGiven: data['error'] ?? true, needsConfirmation: data['code'] == 'NeedsConfirmation', currentRating: data['rating'] ?? 0, myRating: data['my_rating'] ?? 'none'); + isGiven: data['error'] ?? true, + needsConfirmation: data['code'] == 'NeedsConfirmation', + currentRating: data['rating'], + myRating: data['my_rating'] ?? 'none'); } void logout({bool removeAuthrorization = true}) { @@ -229,7 +247,7 @@ class ApiController { } } - Future loadMail({int lastId}) async { + Future loadMail({int? lastId}) async { var response = await provider.fetchMail(lastId: lastId); return MailResponse.fromJson(response.data); } @@ -242,21 +260,23 @@ class ApiController { return await Future.wait(deletes); } - Future sendMail(String recipient, String message, {List> attachments}) async { + Future sendMail(String recipient, String message, {List>? attachments}) async { // Upload image - if (attachments is List) { + if (attachments != null) { try { WaitingFilesResponse waitingFilesResponse = await this.fetchMailWaitingFiles(); await this.deleteAllWaitingFiles(waitingFilesResponse.files); } catch (error) { debugPrint(error.toString()); - MainRepository().sentry.captureException(exception: error); + Sentry.captureException(error); // TODO: Notify user? } try { await provider.uploadFile(attachments); } catch (error) { - provider.onError('👎 Nějakteré z obrázků se nepodařilo nahrát.'); + if (provider.onError != null) { + provider.onError!('👎 Nějakteré z obrázků se nepodařilo nahrát.'); + } } } @@ -277,7 +297,7 @@ class ApiController { Future votePoll(discussionId, postId, votes) async { Response response = await provider.votePoll(discussionId, postId, votes); Map json = response.data; - return ContentPoll.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'], postId: json['post_id']); + return ContentPoll.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'] ?? 0, postId: json['post_id'] ?? 0); } throwAuthException(LoginResponse loginResponse, {String message: ''}) { diff --git a/lib/controllers/ApiProvider.dart b/lib/controllers/ApiProvider.dart index c4f4586..426f49a 100644 --- a/lib/controllers/ApiProvider.dart +++ b/lib/controllers/ApiProvider.dart @@ -10,20 +10,20 @@ class ApiProvider implements IApiProvider { // ignore: non_constant_identifier_names final URL = 'https://nyx.cz/api'; - Credentials _credentials; + Credentials? _credentials; - TOnError onError; - TOnAuthError onAuthError; - TOnContextData onContextData; + TOnError? onError; + TOnAuthError? onAuthError; + TOnContextData? onContextData; - Credentials getCredentials() { - if (_credentials != null && _credentials.isValid) { + getCredentials() { + if (_credentials != null && _credentials!.isValid) { return _credentials; } return null; } - Credentials setCredentials(Credentials creds) { + setCredentials(Credentials? creds) { if (creds != null && creds.isValid) { _credentials = creds; } @@ -31,7 +31,7 @@ class ApiProvider implements IApiProvider { } ApiProvider() { - dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) async { + dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options, RequestInterceptorHandler handler) async { try { // TODO: Perhaps, solve Czech characters too... // TODO: Get rid of MainRepository() @@ -46,43 +46,53 @@ class ApiProvider implements IApiProvider { print('[API] UA: ${options.headers['user-agent']}'); print('[API] ${options.method.toUpperCase()}: ${options.uri}'); - if (_credentials != null && _credentials.isValid) { - print('[API] -> Bearer: ${_credentials.token}'); - options.headers['Authorization'] = 'Bearer ${_credentials.token}'; + if (_credentials != null && _credentials!.isValid) { + print('[API] -> Bearer: ${_credentials!.token}'); + options.headers['Authorization'] = 'Bearer ${_credentials!.token}'; } - return options; - }, onResponse: (Response response) async { + return handler.next(options); + }, onResponse: (Response response, ResponseInterceptorHandler handler) async { if (response.data.containsKey('context')) { - onContextData(response.data['context']); + if (onContextData != null) { + onContextData!(response.data['context']); + } } // All seems ok. if (response.statusCode == 200) { - return response; + return handler.next(response); + } + + // Negative rating confirmation + if (response.statusCode == 403 && ['NeedsConfirmation'].contains(response.data?['code'])) { + return handler.next(response); } // Malformed response - onError(L.API_ERROR); - return response; - }, onError: (DioError e) async { + if (onError != null) { + onError!(L.API_ERROR); + } + return handler.next(response); + }, onError: (DioError e, ErrorInterceptorHandler handler) async { // Not Authorized if (e.response?.statusCode == 401) { - onAuthError(e.response.data['message']); - return e.response; + if (onAuthError != null) { + onAuthError!(e.response!.data['message']); + } + return handler.next(e); } // Other problem if (e.response?.statusCode == 400) { - onError(e.response.data['message']); - return e.response; + if (onError != null) { + onError!(e.response!.data['message']); + } + return handler.next(e); } - // Negative rating confirmation - if (e.response?.statusCode == 403) { - return e.response; + if (onError != null) { + onError!(e.message); } - - onError(e.message); })); } @@ -92,7 +102,7 @@ class ApiProvider implements IApiProvider { Future registerFcmToken(String token) async { String client = 'fyx'; - return await dio.post('$URL/register_for_notifications/${_credentials.token}/$client/$token'); + return await dio.post('$URL/register_for_notifications/${_credentials?.token}/$client/$token'); } Future fetchBookmarks() async { @@ -100,16 +110,17 @@ class ApiProvider implements IApiProvider { } Future fetchHistory() async { - return await dio.get('$URL/bookmarks/history/more'); + return await dio.get('$URL/bookmarks/history', queryParameters: {'more_results': true, 'show_read': true}); } - Future fetchDiscussion(int discussionId, {int lastId, String user}) async { - Map params = {'order': lastId == null ? 'newest' : 'older_than', 'from_id': lastId, 'user': user}; + Future fetchDiscussion(int discussionId, {int? lastId, String? user, String? search}) async { + Map params = {'order': lastId == null ? 'newest' : 'older_than', 'from_id': lastId, 'user': user, 'text': search}; return await dio.get('$URL/discussion/$discussionId', queryParameters: params); } Future fetchDiscussionHome(int id) async { - FormData formData = new FormData.fromMap({'auth_nick': _credentials.nickname, 'auth_token': _credentials.token, 'l': 'discussion', 'l2': 'home', 'id_klub': id}); + FormData formData = new FormData.fromMap( + {'auth_nick': _credentials?.nickname, 'auth_token': _credentials?.token, 'l': 'discussion', 'l2': 'home', 'id_klub': id}); return await dio.post(URL, data: formData); } @@ -117,8 +128,13 @@ class ApiProvider implements IApiProvider { return await dio.get('$URL/notifications'); } - Future postDiscussionMessage(int postId, String message) async { - return await dio.post('$URL/discussion/$postId/send/text', data: {'content': message, 'format': 'html'}, options: Options(contentType: Headers.formUrlEncodedContentType)); + Future postDiscussionMessage(int discussionId, String message) async { + return await dio.post('$URL/discussion/$discussionId/send/text', + data: {'content': message, 'format': 'html'}, options: Options(contentType: Headers.formUrlEncodedContentType)); + } + + Future deleteDiscussionMessage(int discussionId, int postId) async { + return await dio.delete('$URL/discussion/$discussionId/delete/$postId'); } Future setPostReminder(int discussionId, int postId, bool setReminder) async { @@ -129,20 +145,21 @@ class ApiProvider implements IApiProvider { String action = positive ? 'positive' : 'negative'; action = remove ? 'remove' : action; action = confirm ? 'negative_visible' : action; - return await dio.post('$URL/discussion/$discussionId/rating/$postId/$action'); + return await dio.post('$URL/discussion/$discussionId/rating/$postId/$action', options: Options(validateStatus: (status) => status! < 500)); } Future logout() async { - return await dio.delete('$URL/profile/delete_token/${_credentials.token}'); + return await dio.delete('$URL/profile/delete_token/${_credentials?.token}'); } - Future fetchMail({int lastId, String username}) async { + Future fetchMail({int? lastId, String? username}) async { Map params = {'order': lastId == null ? 'newest' : 'older_than', 'from_id': lastId, 'user': username}; return await dio.get('$URL/mail', queryParameters: params); } Future sendMail(String recipient, String message) async { - return await dio.post('$URL/mail/send', data: {'recipient': recipient, 'message': message, 'format': 'html'}, options: Options(contentType: Headers.formUrlEncodedContentType)); + return await dio.post('$URL/mail/send', + data: {'recipient': recipient, 'message': message, 'format': 'html'}, options: Options(contentType: Headers.formUrlEncodedContentType)); } Future deleteFile(int id) async { @@ -161,7 +178,8 @@ class ApiProvider implements IApiProvider { List uploads = []; for (Map attachment in attachments) { FormData fileData = new FormData.fromMap({ - 'file': MultipartFile.fromBytes(attachment[ATTACHMENT.bytes], filename: attachment[ATTACHMENT.filename], contentType: attachment[ATTACHMENT.mediatype]), + 'file': MultipartFile.fromBytes(attachment[ATTACHMENT.bytes], + filename: attachment[ATTACHMENT.filename], contentType: attachment[ATTACHMENT.mediatype]), 'file_type': id == 0 ? 'mail_attachment' : 'discussion_attachment', 'id_specific': id }); diff --git a/lib/controllers/IApiProvider.dart b/lib/controllers/IApiProvider.dart index 503925c..7e78053 100644 --- a/lib/controllers/IApiProvider.dart +++ b/lib/controllers/IApiProvider.dart @@ -5,23 +5,23 @@ typedef TOnError = void Function(String); typedef TOnAuthError = void Function(String); typedef TOnContextData = void Function(Map); -enum ATTACHMENT { bytes, filename, mime, extension, mediatype } +enum ATTACHMENT { bytes, filename, mime, extension, mediatype, previewWidget } abstract class IApiProvider { - TOnError onError; - TOnAuthError onAuthError; - TOnContextData onContextData; + TOnError? onError; + TOnAuthError? onAuthError; + TOnContextData? onContextData; - Credentials getCredentials(); - Credentials setCredentials(Credentials val); + Credentials? getCredentials(); + Credentials? setCredentials(Credentials? val); Future login(String username); Future logout(); Future registerFcmToken(String token); Future fetchBookmarks(); Future fetchHistory(); - Future fetchDiscussion(int id, {int lastId, String user}); + Future fetchDiscussion(int id, {int? lastId, String? user, String? search}); Future fetchDiscussionHome(int id); - Future fetchMail({int lastId}); + Future fetchMail({int? lastId}); Future fetchNotices(); Future deleteFile(int id); Future fetchMailWaitingFiles(); @@ -29,6 +29,7 @@ abstract class IApiProvider { Future uploadFile(List> attachments, {int id}); Future sendMail(String recipient, String message); Future postDiscussionMessage(int id, String message); + Future deleteDiscussionMessage(int discussionId, int postId); Future setPostReminder(int discussionId, int postId, bool setReminder); Future giveRating(int discussionId, int postId, bool add, bool confirm, bool remove); Future votePoll(int discussionId, int postId, List votes); diff --git a/lib/controllers/NotificationsService.dart b/lib/controllers/NotificationsService.dart index 28a12ac..06d756d 100644 --- a/lib/controllers/NotificationsService.dart +++ b/lib/controllers/NotificationsService.dart @@ -1,37 +1,24 @@ import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:meta/meta.dart'; typedef ErrorCallback = Function(dynamic error); typedef TokenCallback = Function(String token); -typedef DiscussionCallback = Function({int discussionId, int postId}); +typedef DiscussionCallback = Function({int? discussionId, int? postId}); class NotificationService { - FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); - StreamSubscription _tokenStream; - Function onNewMail; - DiscussionCallback onNewPost; - ErrorCallback onError; + FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + late StreamSubscription _tokenStream; + Function? onNewMail; + DiscussionCallback? onNewPost; + ErrorCallback? onError; - TokenCallback _onToken; - TokenCallback _onTokenRefresh; + late TokenCallback _onToken; + late TokenCallback _onTokenRefresh; - NotificationService({@required TokenCallback onToken, @required TokenCallback onTokenRefresh}) + NotificationService({required TokenCallback onToken, required TokenCallback onTokenRefresh}) : this._onToken = onToken, this._onTokenRefresh = onTokenRefresh { - _firebaseMessaging.configure( - // This is triggered when the app is in foreground (active) - // onMessage: (Map message) async { - // _handleNotifications(message); - // }, - onLaunch: (Map message) async { - _handleNotifications(message); - }, - onResume: (Map message) async { - _handleNotifications(message); - }, - ); _tokenStream = _firebaseMessaging.onTokenRefresh.listen((fcmToken) { if (fcmToken is String && this._onTokenRefresh is TokenCallback) { @@ -45,27 +32,51 @@ class NotificationService { }); } + configure() async { + RemoteMessage? message = await _firebaseMessaging.getInitialMessage(); + + // Get any messages which caused the application to open from + // a terminated state. + if (message != null) { + _handleNotifications(message); + } + + // Also handle any interaction when the app is in the background via a + // Stream listener + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + _handleNotifications(message); + }); + } + request() { - return _firebaseMessaging.requestNotificationPermissions(); + return _firebaseMessaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); } - void _handleNotifications(Map message) { + void _handleNotifications(RemoteMessage message) { try { - if (message['type'] == 'new_mail') { - if (onNewMail is Function) { - onNewMail(); + if (message.data['type'] == 'new_mail') { + if (onNewMail != null) { + onNewMail!(); return; } } - if (message['type'] == 'reply') { - if (onNewPost is DiscussionCallback) { - onNewPost(discussionId: int.parse(message['discussion_id'] ?? '0'), postId: int.parse(message['post_id'] ?? '0')); + if (message.data['type'] == 'reply') { + if (onNewPost != null) { + onNewPost!(discussionId: int.parse(message.data['discussion_id'] ?? '0'), postId: int.parse(message.data['post_id'] ?? '0')); return; } } } catch (e) { - if (onError is ErrorCallback) { - onError(e); + if (onError != null) { + onError!(e); } } } diff --git a/lib/controllers/SettingsProvider.dart b/lib/controllers/SettingsProvider.dart index e6c5ba9..bf00303 100644 --- a/lib/controllers/SettingsProvider.dart +++ b/lib/controllers/SettingsProvider.dart @@ -1,15 +1,22 @@ import 'package:fyx/model/Settings.dart'; import 'package:fyx/model/enums/DefaultView.dart'; -import "package:hive/hive.dart"; -import "package:hive_flutter/hive_flutter.dart"; +import 'package:fyx/model/enums/ThemeEnum.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; class SettingsProvider { static final SettingsProvider _singleton = SettingsProvider._internal(); - Settings _settings; - Box _box; + late Settings _settings; + late Box _box; Box get box => _box; + ThemeEnum get theme => _settings.theme; + set theme(ThemeEnum theme) { + _box.put('theme', theme); + _settings.theme = theme; + } + DefaultView get defaultView => _settings.defaultView; set defaultView(DefaultView view) { _box.put('defaultView', view); @@ -40,18 +47,6 @@ class SettingsProvider { List get blockedUsers => _box.get('blockedUsers', defaultValue: Settings().blockedUsers); - int get photoQuality => _box.get('photoQuality', defaultValue: Settings().photoQuality); - set photoQuality(int quality) { - _box.put('photoQuality', quality); - _settings.photoQuality = quality; - } - - int get photoWidth => _box.get('photoWidth', defaultValue: Settings().photoWidth); - set photoWidth(int width) { - _box.put('photoWidth', width); - _settings.photoWidth = width; - } - factory SettingsProvider() { return _singleton; } @@ -61,15 +56,15 @@ class SettingsProvider { Future init() async { await Hive.initFlutter(); Hive.registerAdapter(DefaultViewAdapter()); + Hive.registerAdapter(ThemeEnumAdapter()); _box = await Hive.openBox('settings'); _settings = new Settings(); + _settings.theme = _box.get('theme', defaultValue: Settings().theme); _settings.defaultView = _box.get('defaultView', defaultValue: Settings().defaultView); _settings.latestView = _box.get('latestView', defaultValue: Settings().latestView); _settings.useCompactMode = _box.get('useCompactMode', defaultValue: Settings().useCompactMode); _settings.useAutocorrect = _box.get('useAutocorrect', defaultValue: Settings().useAutocorrect); - _settings.photoQuality = _box.get('photoQuality', defaultValue: Settings().photoQuality); - _settings.photoWidth = _box.get('photoWidth', defaultValue: Settings().photoWidth); return _singleton; } diff --git a/lib/libs/DeviceInfo.dart b/lib/libs/DeviceInfo.dart index 82d2358..f93a23f 100644 --- a/lib/libs/DeviceInfo.dart +++ b/lib/libs/DeviceInfo.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:device_info/device_info.dart'; class DeviceInfo { - String systemName; - String systemVersion; - String localizedModel; + late String systemName; + late String systemVersion; + late String localizedModel; static Future init() async { return Platform.isIOS diff --git a/lib/main.dart b/lib/main.dart index 4074bb4..0c26e1c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,24 +1,30 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:fyx/FyxApp.dart'; -import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await DotEnv().load('.env'); - final sentry = SentryClient(dsn: DotEnv().env['SENTRY_KEY'], environmentAttributes: const Event(environment: 'development')); + ByteData data = await PlatformAssetBundle().load('assets/lets-encrypt-r3.cer'); + SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); + await dotenv.load(); runZonedGuarded( () async { - await FyxApp.init(sentry); - return runApp(FyxApp()..setEnv(Environment.dev)); + await FyxApp.init(); + SentryFlutter.init((options) { + options.dsn = dotenv.env['SENTRY_KEY']; + options.environment = 'development'; + }, appRunner: () => runApp(FyxApp()..setEnv(Environment.dev))); }, (error, stackTrace) async { try { - await sentry.captureException( - exception: error, + await Sentry.captureException( + error, stackTrace: stackTrace, ); print('Error sent to sentry.io: $error'); diff --git a/lib/main_production.dart b/lib/main_production.dart index 96acdef..a7f9438 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,24 +1,30 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:fyx/FyxApp.dart'; -import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await DotEnv().load('.env'); - final sentry = SentryClient(dsn: DotEnv().env['SENTRY_KEY'], environmentAttributes: const Event(environment: 'production')); + ByteData data = await PlatformAssetBundle().load('assets/lets-encrypt-r3.cer'); + SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); + await dotenv.load(); runZonedGuarded( () async { - await FyxApp.init(sentry); - return runApp(FyxApp()..setEnv(Environment.production)); + await FyxApp.init(); + SentryFlutter.init((options) { + options.dsn = dotenv.env['SENTRY_KEY']; + options.environment = 'production'; + }, appRunner: () => runApp(FyxApp()..setEnv(Environment.production))); }, (error, stackTrace) async { try { - await sentry.captureException( - exception: error, + await Sentry.captureException( + error, stackTrace: stackTrace, ); print('Error sent to sentry.io: $error'); diff --git a/lib/model/AccessRights.dart b/lib/model/AccessRights.dart index b8ee3eb..c8b0ad1 100644 --- a/lib/model/AccessRights.dart +++ b/lib/model/AccessRights.dart @@ -1,13 +1,13 @@ class AccessRights { - int _discussionId; - bool _arRead; - bool _arWrite; - bool _arDelete; - bool _arEdit; - bool _arRights; - int _daysLeft; + int? _discussionId; + bool? _arRead; + bool? _arWrite; + bool? _arDelete; + bool? _arEdit; + bool? _arRights; + int? _daysLeft; - AccessRights({int discussionId, bool arRead, bool arWrite, bool arDelete, bool arEdit, bool arRights, int daysLeft}) { + AccessRights({discussionId, arRead, arWrite, arDelete, arEdit, arRights, daysLeft}) { this._discussionId = discussionId; this._arRead = arRead; this._arWrite = arWrite; @@ -17,19 +17,19 @@ class AccessRights { this._daysLeft = daysLeft; } - int get discussionId => _discussionId; + get discussionId => _discussionId; - bool get canRead => _arRead; + get canRead => _arRead; - bool get canWrite => _arWrite; + get canWrite => _arWrite; - bool get canDelete => _arDelete; + get canDelete => _arDelete; - bool get canEdit => _arEdit; + get canEdit => _arEdit; - bool get canRights => _arRights; + get canRights => _arRights; - int get daysLeft => _daysLeft; + get daysLeft => _daysLeft; AccessRights.fromJson(Map json) { _discussionId = json['discussion_id']; diff --git a/lib/model/Active.dart b/lib/model/Active.dart index 276c827..e942a06 100644 --- a/lib/model/Active.dart +++ b/lib/model/Active.dart @@ -1,9 +1,9 @@ // ignore_for_file: non_constant_identifier_names class Active { - int _time; - String _location; - String _location_url; + late int _time; + late String _location; + late String _location_url; Active.fromJson(Map json) { _time = DateTime.parse(json['last_activity'] ?? '0').millisecondsSinceEpoch; diff --git a/lib/model/BookmarkedDiscussion.dart b/lib/model/BookmarkedDiscussion.dart index b52b17b..c123571 100644 --- a/lib/model/BookmarkedDiscussion.dart +++ b/lib/model/BookmarkedDiscussion.dart @@ -1,13 +1,13 @@ // ignore_for_file: non_constant_identifier_names class BookmarkedDiscussion { - int _id_klub; - int _unread; - int _replies; - int _images; - int _links; - String _name; - int _last_visit; + late int _id_klub; + late int _unread; + late int _replies; + late int _images; + late int _links; + late String _name; + late int _last_visit; BookmarkedDiscussion.fromJson(Map json) { this._id_klub = json['discussion_id']; diff --git a/lib/model/Category.dart b/lib/model/Category.dart index 816c097..5ddefd9 100644 --- a/lib/model/Category.dart +++ b/lib/model/Category.dart @@ -1,9 +1,9 @@ // ignore_for_file: non_constant_identifier_names class Category { - int _id_cat; - String _jmeno; - int _sort_code; + late int _id_cat; + late String _jmeno; + late int _sort_code; Category.fromJson(Map json) { this._id_cat = int.parse(json['id_cat']); diff --git a/lib/model/Credentials.dart b/lib/model/Credentials.dart index 253dac3..2be2bb8 100644 --- a/lib/model/Credentials.dart +++ b/lib/model/Credentials.dart @@ -1,9 +1,9 @@ import 'package:fyx/theme/Helpers.dart'; class Credentials { - String _nickname; - String _token = ''; - String _fcmToken; + late String _nickname; + late String _token; + String? _fcmToken; Credentials(this._nickname, this._token, {fcmToken}) : this._fcmToken = fcmToken; @@ -23,11 +23,11 @@ class Credentials { String get token => _token; - String get fcmToken => _fcmToken; + String? get fcmToken => _fcmToken; String get nickname => _nickname.toUpperCase(); String get avatar => Helpers.avatarUrl(nickname); - bool get isValid => _token != null && _nickname != null; + bool get isValid => _token.isNotEmpty && _nickname.length >= 3; } diff --git a/lib/model/Discussion.dart b/lib/model/Discussion.dart index f6fc360..4fb5235 100644 --- a/lib/model/Discussion.dart +++ b/lib/model/Discussion.dart @@ -7,25 +7,25 @@ import 'package:fyx/model/enums/DiscussionTypeEnum.dart'; import 'package:fyx/model/post/content/Advertisement.dart'; class Discussion { - int _id_klub; + late int _id_klub; - String _name; - String _name_main; - String _name_sub; - String _discussion_type; - int _last_visit; - bool _has_home; - bool _has_header; - int _id_domain; + String _name = ''; + String _name_main = ''; + String _name_sub = ''; + String _discussion_type = ''; + int _last_visit = 0; + bool _has_home = false; + bool _has_header = false; + int _id_domain = 0; bool _accessDenied = false; - DiscussionRights _discussion_rights; - AccessRights _access_rights; - DiscussionOwner _owner; - ContentAdvertisement _advertisement; + late DiscussionRights _discussion_rights; + late AccessRights _access_rights; + DiscussionOwner? _owner; + ContentAdvertisement? _advertisement; - Discussion.fromJson(Map json) { + Discussion.fromJson(Map? json) { if (json == null) { this._accessDenied = true; return; @@ -45,7 +45,7 @@ class Discussion { // Global rights this._discussion_rights = DiscussionRights.fromJson(json['discussion']); - if (this._access_rights?.canRead != true && !this._discussion_rights.canRead) { + if (this._access_rights.canRead != true && !this._discussion_rights.canRead) { this._accessDenied = true; return; } @@ -94,9 +94,9 @@ class Discussion { bool get accessDenied => _accessDenied; - ContentAdvertisement get advertisement => _advertisement; + ContentAdvertisement? get advertisement => _advertisement; - DiscussionOwner get owner => _owner; + DiscussionOwner? get owner => _owner; AccessRights get accessRights => _access_rights; diff --git a/lib/model/DiscussionOwner.dart b/lib/model/DiscussionOwner.dart index 5fb2f49..7c93248 100644 --- a/lib/model/DiscussionOwner.dart +++ b/lib/model/DiscussionOwner.dart @@ -1,7 +1,7 @@ class DiscussionOwner { - String _username; + late String _username; - DiscussionOwner({String username}) { + DiscussionOwner({String username = ''}) { this._username = username; } diff --git a/lib/model/DiscussionRights.dart b/lib/model/DiscussionRights.dart index 2d15bd1..0822708 100644 --- a/lib/model/DiscussionRights.dart +++ b/lib/model/DiscussionRights.dart @@ -1,12 +1,12 @@ class DiscussionRights { - bool _arRead; - bool _arWrite; - bool _arDelete; - bool _arEdit; - bool _arRights; - bool _public; - - DiscussionRights({bool arRead, bool arWrite, bool arDelete, bool arEdit, bool arRights, bool public}) { + bool _arRead = false; + bool _arWrite = false; + bool _arDelete = false; + bool _arEdit = false; + bool _arRights = false; + bool _public = false; + + DiscussionRights({bool arRead = false, bool arWrite = false, bool arDelete = false, bool arEdit = false, bool arRights = false, bool public = false}) { this._arRead = arRead; this._arWrite = arWrite; this._arDelete = arDelete; diff --git a/lib/model/FileAttachment.dart b/lib/model/FileAttachment.dart index 3847e3d..1e5f2e0 100644 --- a/lib/model/FileAttachment.dart +++ b/lib/model/FileAttachment.dart @@ -1,36 +1,36 @@ class FileAttachment { - int _id; - String _fileType; - int _idSpecific; - int _idSpecific2; - String _filename; - int _size; - String _uploadedAt; - String _mimetype; - int _imageWidth; - int _imageHeight; - String _imageAvgColorHex; - bool _imageEmbed; - String _imageEmbedOption; - String _url; - String _thumbUrl; + late int _id; + late String _fileType; + late int _idSpecific; + late int _idSpecific2; + late String _filename; + late int _size; + late String _uploadedAt; + late String _mimetype; + late int _imageWidth; + late int _imageHeight; + late String _imageAvgColorHex; + late bool _imageEmbed; + late String _imageEmbedOption; + late String _url; + late String _thumbUrl; FileAttachment( - {int id, - String fileType, - int idSpecific, - int idSpecific2, - String filename, - int size, - String uploadedAt, - String mimetype, - int imageWidth, - int imageHeight, - String imageAvgColorHex, - bool imageEmbed, - String imageEmbedOption, - String url, - String thumbUrl}) { + {int id = 0, + String fileType = '', + int idSpecific = 0, + int idSpecific2 = 0, + String filename = '', + int size = 0, + String uploadedAt = '', + String mimetype = '', + int imageWidth = 0, + int imageHeight = 0, + String imageAvgColorHex = '', + bool imageEmbed = false, + String imageEmbedOption = '', + String url = '', + String thumbUrl = ''}) { this._id = id; this._fileType = fileType; this._idSpecific = idSpecific; @@ -54,7 +54,7 @@ class FileAttachment { int get idSpecific => _idSpecific; - Null get idSpecific2 => _idSpecific2; + int get idSpecific2 => _idSpecific2; String get filename => _filename; diff --git a/lib/model/Mail.dart b/lib/model/Mail.dart index 16de3d9..0ac1c5b 100644 --- a/lib/model/Mail.dart +++ b/lib/model/Mail.dart @@ -8,16 +8,16 @@ enum MailStatus { read, unread, unknown } class Mail { final bool isCompact; - int _id_mail; - String _other_nick; - int _time; - bool _direction; - Content _content; - MailStatus _message_status; - bool _new; - Map _active; - - Mail.fromJson(Map json, {this.isCompact}) { + int _id_mail = 0; + String _other_nick = ''; + int _time = 0; + bool _direction = false; + late Content _content; + late MailStatus _message_status; + bool _new = false; + Map? _active; + + Mail.fromJson(Map json, {this.isCompact = false}) { _id_mail = json['id']; _other_nick = json['username']; _time = DateTime.parse(json['inserted_at'] ?? '0').millisecondsSinceEpoch; @@ -32,7 +32,7 @@ class Mail { bool get isNew => _new; - Active get active => _active == null ? null : Active.fromJson(_active); + Active? get active => _active == null ? null : Active.fromJson(_active!); MailStatus get status => _message_status; diff --git a/lib/model/MainRepository.dart b/lib/model/MainRepository.dart index 41c7ff7..58f4e64 100644 --- a/lib/model/MainRepository.dart +++ b/lib/model/MainRepository.dart @@ -3,17 +3,15 @@ import 'package:fyx/controllers/SettingsProvider.dart'; import 'package:fyx/libs/DeviceInfo.dart'; import 'package:fyx/model/Credentials.dart'; import 'package:package_info/package_info.dart'; -import 'package:sentry/sentry.dart'; class MainRepository { // TODO: Refactor -> rename MainRepository to AppContext? static final MainRepository _singleton = MainRepository._internal(); - Credentials credentials; - PackageInfo packageInfo; - DeviceInfo deviceInfo; - SettingsProvider settings; - SentryClient sentry; - NotificationService notifications; + Credentials? credentials; + late PackageInfo packageInfo; + late DeviceInfo deviceInfo; + late SettingsProvider settings; + late NotificationService notifications; factory MainRepository() { return _singleton; diff --git a/lib/model/Post.dart b/lib/model/Post.dart index f01ff86..a3d75e7 100644 --- a/lib/model/Post.dart +++ b/lib/model/Post.dart @@ -1,5 +1,4 @@ // ignore_for_file: non_constant_identifier_names -import 'package:fyx/model/enums/PostTypeEnum.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/content/Advertisement.dart'; import 'package:fyx/model/post/content/Poll.dart'; @@ -7,28 +6,29 @@ import 'package:fyx/model/post/content/Regular.dart'; import 'package:fyx/theme/Helpers.dart'; class Post { + // TODO: Refactor all params to follow names from the new API like _id_wu -> id ... final bool isCompact; bool _canReply = true; - bool _isNew; - int idKlub; - int _id_wu; - String _nick; - int _time; - int _wu_rating; - String _wu_type; - String myRating; - bool _reminder; - bool _canBeRated; - bool _canBeDeleted; - bool _canBeReminded; - Content _content; - - Post.fromJson(Map json, this.idKlub, {this.isCompact}) { - this._id_wu = json['id']; - this._nick = json['username']; + bool _isNew = false; + int idKlub = 0; + int _id_wu = 0; + String _nick = ''; + int _time = 0; + int? rating; + String _wu_type = ''; + String myRating = ''; + bool _reminder = false; + bool _canBeRated = false; + bool _canBeDeleted = false; + bool _canBeReminded = false; + late Content _content; + + Post.fromJson(Map json, this.idKlub, {this.isCompact = false}) { + this._id_wu = json['id'] ?? 0; + this._nick = json['username'] ?? ''; this._time = DateTime.parse(json['inserted_at'] ?? '0').millisecondsSinceEpoch; - this._wu_rating = json['rating'] ?? 0; - this._wu_type = json['type']; + this.rating = json['rating']; + this._wu_type = json['type'] ?? ''; this._isNew = json['new'] ?? false; this.myRating = json['my_rating'] ?? 'none'; // positive / negative / negative_visible / none TODO: enums this._reminder = json['reminder'] ?? false; @@ -46,8 +46,9 @@ class Post { this._content = ContentAdvertisement.fromPostJson(json); break; default: - this._content = - ContentRegular('${json['content']}

Chyba: neošetřený druh příspěvku: "${json['content_raw']['type']}"', isCompact: this.isCompact); + this._content = ContentRegular( + '${json['content']}

Chyba: neošetřený druh příspěvku: "${json['content_raw']['type']}"', + isCompact: this.isCompact); break; } //TODO handle other cases @@ -56,14 +57,19 @@ class Post { } } + static String formatRating(int _rating) { + if (_rating == 0) { + return '±$_rating'; + } else if (_rating < 0) { + return _rating.toString(); + } + return '+$_rating'; + } + Content get content => _content; String get type => _wu_type; - int get rating => _wu_rating; - - set rating(val) => _wu_rating = val; - int get time => _time; String get avatar => Helpers.avatarUrl(nick); diff --git a/lib/model/ResponseContext.dart b/lib/model/ResponseContext.dart index 1bf119e..872a7a6 100644 --- a/lib/model/ResponseContext.dart +++ b/lib/model/ResponseContext.dart @@ -1,8 +1,8 @@ class ResponseContext { - User _user; - List _activeFriends; + late User _user; + List _activeFriends = []; - ResponseContext({User user, List activeFriends}) { + ResponseContext({required User user, List activeFriends = const []}) { this._user = user; this._activeFriends = activeFriends; } @@ -12,24 +12,23 @@ class ResponseContext { List get activeFriends => _activeFriends; ResponseContext.fromJson(Map json) { - _user = json['user'] != null ? new User.fromJson(json['user']) : null; + _user = json['user'] != null ? new User.fromJson(json['user']) : User(); if (json['active_friends'] != null) { - _activeFriends = new List(); json['active_friends'].forEach((v) { - _activeFriends.add(new ActiveFriends.fromJson(v)); + _activeFriends.add(ActiveFriends.fromJson(v)); }); } } } class User { - String _username; - int _mailUnread; - String _mailLastFrom; - int _notificationsUnread; - String _notificationsLastVisit; + String _username = ''; + int _mailUnread = 0; + String _mailLastFrom = ''; + int _notificationsUnread = 0; + String _notificationsLastVisit = ''; - User({String username, int mailUnread, String mailLastFrom, int notificationsUnread, String notificationsLastVisit}) { + User({String username = '', int mailUnread = 0, String mailLastFrom = '', int notificationsUnread = 0, String notificationsLastVisit = ''}) { this._username = username; this._mailUnread = mailUnread; this._mailLastFrom = mailLastFrom; @@ -57,14 +56,14 @@ class User { } class ActiveFriends { - String _username; - String _lastActivity; - String _lastAccessMethod; - String _statusDetails; - String _location; - String _locationUrl; - - ActiveFriends({String username, String lastActivity, String lastAccessMethod, String statusDetails, String location, String locationUrl}) { + String _username = ''; + String _lastActivity = ''; + String _lastAccessMethod = ''; + String _statusDetails = ''; + String _location = ''; + String _locationUrl = ''; + + ActiveFriends({String username = '', String lastActivity = '', String lastAccessMethod = '', String statusDetails = '', String location = '', String locationUrl = ''}) { this._username = username; this._lastActivity = lastActivity; this._lastAccessMethod = lastAccessMethod; @@ -86,11 +85,11 @@ class ActiveFriends { String get locationUrl => _locationUrl; ActiveFriends.fromJson(Map json) { - _username = json['username']; - _lastActivity = json['last_activity']; - _lastAccessMethod = json['last_access_method']; - _statusDetails = json['status_details']; - _location = json['location']; - _locationUrl = json['location_url']; + _username = json['username'] ?? ''; + _lastActivity = json['last_activity'] ?? ''; + _lastAccessMethod = json['last_access_method'] ?? ''; + _statusDetails = json['status_details'] ?? ''; + _location = json['location'] ?? ''; + _locationUrl = json['location_url'] ?? ''; } } diff --git a/lib/model/Settings.dart b/lib/model/Settings.dart index 364fa8e..af3ec92 100644 --- a/lib/model/Settings.dart +++ b/lib/model/Settings.dart @@ -1,4 +1,5 @@ import 'package:fyx/model/enums/DefaultView.dart'; +import 'package:fyx/model/enums/ThemeEnum.dart'; class Settings { bool useCompactMode = false; @@ -10,8 +11,5 @@ class Settings { List blockedPosts = []; List blockedMails = []; List blockedUsers = []; - // Quality of uploaded image - int photoQuality = 90; - // Width of uploaded image - int photoWidth = 640; + ThemeEnum theme = ThemeEnum.system; } diff --git a/lib/model/UserReferences.dart b/lib/model/UserReferences.dart index ef8d814..3aab8fa 100644 --- a/lib/model/UserReferences.dart +++ b/lib/model/UserReferences.dart @@ -1,8 +1,8 @@ class UserReferences { - int _positive; - int _negative; + late int _positive; + late int _negative; - UserReferences({int positive, int negative}) { + UserReferences({int positive = 0, int negative = 0}) { this._positive = positive; this._negative = negative; } diff --git a/lib/model/enums/DefaultView.g.dart b/lib/model/enums/DefaultView.g.dart index 2da8ed1..5882659 100644 --- a/lib/model/enums/DefaultView.g.dart +++ b/lib/model/enums/DefaultView.g.dart @@ -46,9 +46,6 @@ class DefaultViewAdapter extends TypeAdapter { case DefaultView.latest: writer.writeByte(4); break; - default: - writer.writeByte(0); - break; } } diff --git a/lib/model/enums/ThemeEnum.dart b/lib/model/enums/ThemeEnum.dart new file mode 100644 index 0000000..884c892 --- /dev/null +++ b/lib/model/enums/ThemeEnum.dart @@ -0,0 +1,13 @@ +import 'package:hive/hive.dart'; + +part 'ThemeEnum.g.dart'; + +@HiveType(typeId: 5) +enum ThemeEnum { + @HiveField(5) + light, + @HiveField(6) + dark, + @HiveField(7) + system +} \ No newline at end of file diff --git a/lib/model/enums/ThemeEnum.g.dart b/lib/model/enums/ThemeEnum.g.dart new file mode 100644 index 0000000..c270d63 --- /dev/null +++ b/lib/model/enums/ThemeEnum.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ThemeEnum.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ThemeEnumAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + ThemeEnum read(BinaryReader reader) { + switch (reader.readByte()) { + case 5: + return ThemeEnum.light; + case 6: + return ThemeEnum.dark; + case 7: + return ThemeEnum.system; + default: + return ThemeEnum.light; + } + } + + @override + void write(BinaryWriter writer, ThemeEnum obj) { + switch (obj) { + case ThemeEnum.light: + writer.writeByte(5); + break; + case ThemeEnum.dark: + writer.writeByte(6); + break; + case ThemeEnum.system: + writer.writeByte(7); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ThemeEnumAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/model/post/Content.dart b/lib/model/post/Content.dart index 9d40a36..2e98dce 100644 --- a/lib/model/post/Content.dart +++ b/lib/model/post/Content.dart @@ -8,7 +8,7 @@ abstract class Content { final bool isCompact; PostTypeEnum _contentType; - Content(this._contentType, { this.isCompact }); + Content(this._contentType, { this.isCompact = false }); bool get isNotEmpty => body.isNotEmpty; diff --git a/lib/model/post/Image.dart b/lib/model/post/Image.dart index 68301b2..4668ca0 100644 --- a/lib/model/post/Image.dart +++ b/lib/model/post/Image.dart @@ -1,18 +1,16 @@ import 'package:quiver/core.dart'; class Image { - final String _image; + final String image; final String _thumb; - Image(this._image, this._thumb) : assert(_image != ''); + Image(this.image, {thumb = ''}): _thumb = thumb; - String get thumb => _thumb.isEmpty ? _image : _thumb; - - String get image => _image; + String get thumb => _thumb.isEmpty ? image : _thumb; @override - int get hashCode => hash2(_image.hashCode, _thumb.hashCode); + int get hashCode => hash2(image.hashCode, _thumb.hashCode); @override - bool operator ==(other) => other is Image && other.thumb == _thumb && other.image == image; + bool operator ==(other) => other is Image && other._thumb == _thumb && other.image == image; } diff --git a/lib/model/post/Link.dart b/lib/model/post/Link.dart index 469bead..1b40359 100644 --- a/lib/model/post/Link.dart +++ b/lib/model/post/Link.dart @@ -2,9 +2,9 @@ class Link { final String url; final String _title; - Link(this.url, {String title}) : _title = title; + Link(this.url, {String title = ''}) : _title = title; - String get title => _title == null || _title == '' ? fancyUrl : _trimUrl(_title); + String get title => _title.isEmpty ? fancyUrl : _trimUrl(_title); String get fancyUrl => _trimUrl(url); diff --git a/lib/model/post/Video.dart b/lib/model/post/Video.dart index cbd2a01..a2ff363 100644 --- a/lib/model/post/Video.dart +++ b/lib/model/post/Video.dart @@ -4,17 +4,15 @@ enum VIDEO_TYPE { youtube } class Video { final VIDEO_TYPE type; - final String id; final String image; - final String thumb; - final Link _link; + final String id; + final String? thumb; + final Link? _link; - Video({this.type, this.id, this.image, this.thumb, String link}) - : _link = link != null && link != '' ? Link(link) : null, - assert(type != null), - assert(image != null); + Video({required this.id, required this.type, required this.image, this.thumb, String? link}) + : _link = link != null && link != '' ? Link(link) : null; - Link get link { + Link? get link { if (_link != null) { return _link; } diff --git a/lib/model/post/content/Advertisement.dart b/lib/model/post/content/Advertisement.dart index b01d0d1..fae435c 100644 --- a/lib/model/post/content/Advertisement.dart +++ b/lib/model/post/content/Advertisement.dart @@ -6,29 +6,29 @@ import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/content/Regular.dart'; class ContentAdvertisement extends Content { - ContentRegular contentRegular; - - int _discussion_id; - String _full_name; - List _parent_categories; - List _photo_ids; - String _ad_type; - int _price; - String _location; - String _currency; - String _state; - String _summary; - String _shipping; - String _description; - String _description_raw; - int _refreshed_at; - int _inserted_at; - int _posts_count; - List _parameters; - List fileAttachments; - UserReferences references; - - ContentAdvertisement.fromJson(Map json, {bool isCompact}) : super(PostTypeEnum.advertisement, isCompact: isCompact) { + ContentRegular? contentRegular; + + int _discussion_id = 0; + String _full_name = ''; + List _parent_categories = []; + List _photo_ids = []; + String _ad_type = ''; + int _price = 0; + String _location = ''; + String _currency = ''; + String _state = ''; + String _summary = ''; + String _shipping = ''; + String _description = ''; + String _description_raw = ''; + int _refreshed_at = 0; + int _inserted_at = 0; + int _posts_count = 0; + List _parameters = []; + List fileAttachments = []; + UserReferences? references; + + ContentAdvertisement.fromJson(Map json, {bool isCompact = false}) : super(PostTypeEnum.advertisement, isCompact: isCompact) { _discussion_id = json['discussion_id']; _full_name = json['full_name'] ?? ''; _parent_categories = List.castFrom(json['parent_categories'] ?? []); @@ -42,7 +42,7 @@ class ContentAdvertisement extends Content { _shipping = json['shipping'] ?? ''; _description = json['description'] ?? ''; _description_raw = json['description_raw'] ?? ''; - _posts_count = json['posts_count']; + _posts_count = json['posts_count'] ?? 0; _parameters = List.castFrom(json['parameters'] ?? []); try { @@ -58,7 +58,7 @@ class ContentAdvertisement extends Content { } } - factory ContentAdvertisement.fromDiscussionJson(Map json, {bool isCompact}) { + factory ContentAdvertisement.fromDiscussionJson(Map json, {bool isCompact = false}) { ContentAdvertisement ad = ContentAdvertisement.fromJson(json['advertisement'], isCompact: isCompact); if (json['attachments'] is List) { @@ -73,7 +73,7 @@ class ContentAdvertisement extends Content { return ad; } - factory ContentAdvertisement.fromPostJson(Map json, {bool isCompact}) { + factory ContentAdvertisement.fromPostJson(Map json, {bool isCompact = false}) { ContentAdvertisement ad = ContentAdvertisement.fromJson(json['content_raw']['data'], isCompact: isCompact); ad.contentRegular = ContentRegular(json['content']); return ad; diff --git a/lib/model/post/content/Poll.dart b/lib/model/post/content/Poll.dart index b64fd4c..611ea3d 100644 --- a/lib/model/post/content/Poll.dart +++ b/lib/model/post/content/Poll.dart @@ -4,17 +4,17 @@ import 'package:fyx/model/post/poll/PollAnswer.dart'; import 'package:fyx/model/post/poll/PollComputedValues.dart'; class ContentPoll extends Content { - int postId; - int discussionId; - - String _question; - String _instructions; - bool _publicResults; - int _allowedVotes; - int _allowAnswersUntil; - int _showAnswersAfter; - List _answers; - PollComputedValues _pollComputedValues; + int postId = 0; + int discussionId = 0; + + String _question = ''; + String _instructions = ''; + bool _publicResults = false; + int _allowedVotes = 0; + int _allowAnswersUntil = 0; + int _showAnswersAfter = 0; + List _answers = []; + PollComputedValues? _pollComputedValues; String get question => _question; @@ -30,28 +30,29 @@ class ContentPoll extends Content { List get answers => _answers; - PollComputedValues get pollComputedValues => _pollComputedValues; + PollComputedValues? get pollComputedValues => _pollComputedValues; bool get canVote { - bool _canVote = pollComputedValues != null && !pollComputedValues.userDidVote; - if (allowAnswersUntil != null) { + bool _canVote = pollComputedValues != null && !pollComputedValues!.userDidVote; + if (allowAnswersUntil > 0) { _canVote = _canVote && allowAnswersUntil > DateTime.now().millisecondsSinceEpoch; } return _canVote; } - ContentPoll.fromJson(Map json, {this.postId, this.discussionId}) : super(PostTypeEnum.poll, isCompact: false) { - _question = json['question']; - _instructions = json['instructions']; - _publicResults = json['public_results']; - _allowedVotes = json['allowed_votes']; - _allowAnswersUntil = json['allow_answers_until'] != null ? DateTime.parse(json['allow_answers_until']).millisecondsSinceEpoch : null; - _showAnswersAfter = json['show_answers_after'] != null ? DateTime.parse(json['show_answers_after']).millisecondsSinceEpoch : null; + ContentPoll.fromJson(Map json, {this.postId = 0, this.discussionId = 0}) : super(PostTypeEnum.poll, isCompact: false) { + _question = json['question'] ?? ''; + _instructions = json['instructions'] ?? ''; + _publicResults = json['public_results'] ?? false; + _allowedVotes = json['allowed_votes'] ?? 0; + _allowAnswersUntil = json['allow_answers_until'] != null ? DateTime.parse(json['allow_answers_until']).millisecondsSinceEpoch : 0; + _showAnswersAfter = json['show_answers_after'] != null ? DateTime.parse(json['show_answers_after']).millisecondsSinceEpoch : 0; if (json['answers'] != null) { - _answers = new List(); + _answers = []; (json['answers'] as Map).forEach((String key, dynamic answer) { - _answers.add(new PollAnswer.fromJson(answer as Map)); + _answers.add(new PollAnswer.fromJson(int.parse(key), answer as Map)); }); + _answers.sort((a, b) => a.id.compareTo(b.id)); } if (json['computed_values'] != null) { _pollComputedValues = PollComputedValues.fromJson(json['computed_values']); diff --git a/lib/model/post/content/Regular.dart b/lib/model/post/content/Regular.dart index 98c2547..6ffe553 100644 --- a/lib/model/post/content/Regular.dart +++ b/lib/model/post/content/Regular.dart @@ -9,10 +9,11 @@ import 'package:fyx/theme/T.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart'; import 'package:html_unescape/html_unescape.dart'; +import 'package:sentry/sentry.dart'; class ContentRegular extends Content { String _body; - String _rawBody; + late String _rawBody; /// If the post have consecutive images = ON /// Consecutive images means there are now characters other than @@ -23,7 +24,7 @@ class ContentRegular extends Content { List _emptyLinks = []; List