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 _videos = [];
- ContentRegular(this._body, {bool isCompact}) : super(PostTypeEnum.text, isCompact: isCompact) {
+ ContentRegular(this._body, {bool isCompact = false}) : super(PostTypeEnum.text, isCompact: isCompact) {
_rawBody = _body;
_rawBody = this._tagAllImageLinks(_rawBody); // This updates the raw body.
_body = this._tagAllImageLinks(_body); // This updates the raw body.
@@ -109,7 +110,7 @@ class ContentRegular extends Content {
var trailingBr = RegExp(r'(((\s*)<\s*br\s*\/?\s*>(\s*))*)$', caseSensitive: false);
_body = _body.replaceAll(trailingBr, '');
} catch (error) {
- MainRepository().sentry.captureException(exception: error, stackTrace: StackTrace.current);
+ Sentry.captureException(error, stackTrace: StackTrace.current);
}
}
@@ -121,13 +122,13 @@ class ContentRegular extends Content {
var youtubes = document.querySelectorAll('div[data-embed-type="youtube"]');
youtubes.forEach((el) {
// If the video does not have preview, it's invalid Nyx attachment, therefore we skip it and handle it as a normal post.
- Element img = el.querySelector('img');
+ Element? img = el.querySelector('img');
if (img == null) {
return;
}
var video = Video(
- id: el.attributes['data-embed-value'], type: Video.findVideoType(el.attributes['data-embed-type']), image: img.attributes['src'], thumb: img.attributes['data-thumb']);
+ id: el.attributes['data-embed-value'] ?? '', type: Video.findVideoType(el.attributes['data-embed-type'] ?? ''), image: img.attributes['src'] ?? '', thumb: img.attributes['data-thumb']);
// Remove the video element from the content.
this._videos.add(video);
@@ -137,7 +138,7 @@ class ContentRegular extends Content {
el.remove();
}
});
- _body = document.body.innerHtml;
+ _body = document.body!.innerHtml;
} catch (error) {
T.error(error.toString());
}
@@ -154,26 +155,26 @@ class ContentRegular extends Content {
_consecutiveImages = reg.hasMatch(_body);
document.querySelectorAll('img[src]').forEach((Element el) {
- var image = el.attributes['src'];
+ var image = el.attributes['src'] ?? '';
var thumb = el.attributes['data-thumb'] ?? '';
- _images.add(Image(image, thumb));
+ _images.add(Image(image, thumb: thumb));
if (_consecutiveImages) {
el.remove();
}
});
- _body = document.body.innerHtml;
+ _body = document.body!.innerHtml;
} catch (error) {
- MainRepository().sentry.captureException(exception: error, stackTrace: StackTrace.current);
+ Sentry.captureException(error, stackTrace: StackTrace.current);
}
}
String _tagAllImageLinks(String source) {
Document document = parse(source);
document.querySelectorAll('img').forEach((Element el) {
- el.parent.classes.add('image-link');
+ el.parent?.classes.add('image-link');
});
- return document.body.innerHtml;
+ return document.body!.innerHtml;
}
///
@@ -187,16 +188,16 @@ class ContentRegular extends Content {
try {
RegExp r = RegExp(r']*?>\s*<\/a>', caseSensitive: false, multiLine: true);
r.allMatches(_body).forEach((match) {
- String element = match.group(0);
+ String? element = match.group(0);
Document html = parse(element);
- String url = html.querySelector('a').attributes['href'];
- if (url != null) {
+ String? url = html.querySelector('a')?.attributes['href'];
+ if (url != null && element != null) {
_emptyLinks.add(Link(url));
_body = _body.replaceFirst(element, '');
}
});
} catch (error) {
- MainRepository().sentry.captureException(exception: error, stackTrace: StackTrace.current);
+ Sentry.captureException(error, stackTrace: StackTrace.current);
}
}
}
diff --git a/lib/model/post/poll/PollAnswer.dart b/lib/model/post/poll/PollAnswer.dart
index 7a90104..d2eb043 100644
--- a/lib/model/post/poll/PollAnswer.dart
+++ b/lib/model/post/poll/PollAnswer.dart
@@ -1,16 +1,20 @@
import 'package:fyx/model/post/poll/PollResult.dart';
class PollAnswer {
- String _answer;
- PollResult _result;
+ late int _id;
+ late String _answer;
+ late PollResult _result;
+
+ int get id => _id;
String get answer => _answer;
PollResult get result => _result;
- PollAnswer.fromJson(Map json) {
- _answer = json['answer'];
- _result = json['result'] != null ? PollResult.fromJson(json['result']) : null;
+ PollAnswer.fromJson(int id, Map json) {
+ _id = id;
+ _answer = json['answer'] ?? '';
+ _result = json['result'] != null ? PollResult.fromJson(json['result']) : PollResult();
}
}
diff --git a/lib/model/post/poll/PollComputedValues.dart b/lib/model/post/poll/PollComputedValues.dart
index d65a632..a8020f2 100644
--- a/lib/model/post/poll/PollComputedValues.dart
+++ b/lib/model/post/poll/PollComputedValues.dart
@@ -1,18 +1,18 @@
class PollComputedValues {
- bool _canModify;
- bool _userDidVote;
- int _totalVotes;
- int _totalRespondents;
- int _maximumAnswerVotes;
+ bool _canModify = false;
+ bool _userDidVote = false;
+ int _totalVotes = 0;
+ int _totalRespondents = 0;
+ int _maximumAnswerVotes = 0;
PollComputedValues.fromJson(Map